diff --git a/crates/node/src/lib.rs b/crates/node/src/lib.rs index fb32a818c..6a2ad130e 100644 --- a/crates/node/src/lib.rs +++ b/crates/node/src/lib.rs @@ -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 { diff --git a/integrations/oxide/workers.test.ts b/integrations/oxide/workers.test.ts index 54d85f271..343de46e5 100644 --- a/integrations/oxide/workers.test.ts +++ b/integrations/oxide/workers.test.ts @@ -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: '
', + 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') }, )