fix(oxide): retain Windows addon after worker exit

This commit is contained in:
Aaron Queen 2026-09-08 06:20:29 -06:00
parent f723e834ad
commit 64a8dcd660
2 changed files with 62 additions and 8 deletions

View file

@ -5,6 +5,38 @@ extern crate napi_derive;
mod utf16;
#[cfg(windows)]
#[napi(module_exports)]
fn retain_native_library() -> napi::Result<()> {
use std::ffi::c_void;
#[link(name = "kernel32")]
extern "system" {
fn GetModuleHandleExW(flags: u32, address: *const u16, module: *mut *mut c_void) -> i32;
}
const FROM_ADDRESS: u32 = 0x00000004;
const PIN: u32 = 0x00000001;
let mut module = std::ptr::null_mut();
// Rayon threads may outlive a Node worker. Keep their code mapped until process exit.
// SAFETY: FROM_ADDRESS treats this function's address as a module location, not a string.
// The module is executing this registration hook and `module` is a valid output pointer.
let retained = unsafe {
GetModuleHandleExW(
FROM_ADDRESS | PIN,
retain_native_library as *const () as *const u16,
&mut module,
)
};
if retained == 0 {
return Err(napi::Error::from_reason(format!(
"Failed to retain the Oxide native library: {}",
std::io::Error::last_os_error()
)));
}
Ok(())
}
#[derive(Debug, Clone)]
#[napi(object)]
pub struct ChangedContent {

View file

@ -1,7 +1,7 @@
import { js, json, test } from '../utils'
test(
'@tailwindcss/oxide can be loaded into a Node.js worker thread',
'@tailwindcss/oxide can scan in a Node.js worker and survive worker exit',
{
fs: {
'package.json': json`
@ -12,21 +12,43 @@ test(
}
`,
'start.js': js`
let assert = require('node:assert/strict')
let { Worker } = require('worker_threads')
new Worker('./worker.js')
let worker = new Worker('./worker.js')
worker.on('error', (error) => {
throw error
})
worker.on('exit', (code) => {
assert.equal(code, 0)
if (process.platform === 'win32') {
assert.ok(
process.report
.getReport()
.sharedObjects.some((file) => /tailwindcss-oxide.*[.]node$/.test(file)),
'Oxide must remain mapped after the worker exits',
)
}
setTimeout(() => console.log('parent survived worker exit'), 500)
})
`,
'worker.js': js`
require('@tailwindcss/oxide')
let assert = require('node:assert/strict')
let { Scanner } = require('@tailwindcss/oxide')
let scanner = new Scanner({ sources: [] })
let candidates = scanner.scanFiles(
Array.from({ length: 100 }, () => ({
content: '<div class="flex p-4 text-red-500"></div>',
extension: 'html',
})),
)
assert.ok(candidates.includes('flex'))
process.on('exit', () => console.log('worker thread exited'))
`,
},
},
async ({ exec, expect }) => {
let output = await exec('node ./start.js').then(
(out) => out.trim(),
(err) => `${err}`,
)
let output = await exec('node ./start.js').then((out) => out.trim())
expect(output).toEqual('worker thread exited')
expect(output).toEqual('worker thread exited\nparent survived worker exit')
},
)