diff --git a/lib/internal/watch_mode/files_watcher.js b/lib/internal/watch_mode/files_watcher.js index 9c0eb1ed817c29..bc736c6c381b29 100644 --- a/lib/internal/watch_mode/files_watcher.js +++ b/lib/internal/watch_mode/files_watcher.js @@ -17,7 +17,7 @@ const { TIMEOUT_MAX } = require('internal/timers'); const EventEmitter = require('events'); const { addAbortListener } = require('internal/events/abort_listener'); -const { watch } = require('fs'); +const { watch, readFileSync } = require('fs'); const { fileURLToPath } = require('internal/url'); const { resolve, dirname, sep } = require('path'); const { setTimeout, clearTimeout } = require('timers'); @@ -166,6 +166,29 @@ class FilesWatcher extends EventEmitter { } } + #parseWorkerImports(workerFile) { + try { + const content = readFileSync(workerFile, 'utf8'); + const imports = []; + // Match: import x from 'path', import('path'), require('path') + const importRegex = /(?:import\s+(?:.*?\s+from\s+)?['"`]([^'"`]+)['"`]|import\(\s*['"`]([^'"`]+)['"`]\s*\)|require\(\s*['"`]([^'"`]+)['"`]\s*\))/g; + let match; + while ((match = importRegex.exec(content)) !== null) { + const importPath = match[1] || match[2] || match[3]; + if (importPath && !importPath.startsWith('node:') && !StringPrototypeStartsWith(importPath, '.')) { + // Skip Node.js built-ins and only process relative paths + continue; + } + if (importPath) { + imports.push(importPath); + } + } + return imports; + } catch (err) { + return []; + } + } + watchChildProcessModules(child, key = null) { if (this.#passthroughIPC) { this.#setupIPC(child); @@ -179,6 +202,19 @@ class FilesWatcher extends EventEmitter { if (ArrayIsArray(message['watch:import'])) { ArrayPrototypeForEach(message['watch:import'], (file) => this.filterFile(fileURLToPath(file), key)); } + if (ArrayIsArray(message['watch:worker'])) { + ArrayPrototypeForEach(message['watch:worker'], (workerFile) => { + // Add worker file itself + this.filterFile(workerFile, key); + + // Parse and watch worker dependencies + const imports = this.#parseWorkerImports(workerFile); + ArrayPrototypeForEach(imports, (importPath) => { + const resolvedPath = resolve(dirname(workerFile), importPath); + this.filterFile(resolvedPath, key); + }); + }); + } } catch { // Failed watching file. ignore } diff --git a/lib/internal/worker.js b/lib/internal/worker.js index 2a4caed82cf7c5..20323b49449086 100644 --- a/lib/internal/worker.js +++ b/lib/internal/worker.js @@ -195,6 +195,17 @@ class HeapProfileHandle { } } +/** + * Tell the watch mode that a worker file was instantiated. + * @param {string} filename Absolute path of the worker file + * @returns {void} + */ +function reportWorkerToWatchMode(filename) { + if (process.env.WATCH_REPORT_DEPENDENCIES && process.send) { + process.send({ 'watch:worker': [filename] }); + } +} + class Worker extends EventEmitter { constructor(filename, options = kEmptyObject) { throwIfBuildingSnapshot('Creating workers'); @@ -275,6 +286,11 @@ class Worker extends EventEmitter { name = StringPrototypeTrim(options.name); } + // Report to watch mode if this is a regular file (not eval, internal, or data URL) + if (!isInternal && doEval === false) { + reportWorkerToWatchMode(filename); + } + debug('instantiating Worker.', `url: ${url}`, `doEval: ${doEval}`); // Set up the C++ handle for the worker, as well as some internal wiring. this[kHandle] = new WorkerImpl(url, diff --git a/test/parallel/test-watch-mode-worker.mjs b/test/parallel/test-watch-mode-worker.mjs new file mode 100644 index 00000000000000..5213d34bc6dc6e --- /dev/null +++ b/test/parallel/test-watch-mode-worker.mjs @@ -0,0 +1,67 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert'; +import { Worker } from 'node:worker_threads'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { writeFileSync, unlinkSync } from 'node:fs'; + +describe('watch:worker event system', () => { + it('should report worker files to parent process', async () => { + const testDir = tmpdir(); + const workerFile = join(testDir, `test-worker-${Date.now()}.js`); + + try { + // Create a simple worker that reports itself + writeFileSync(workerFile, ` + const { Worker } = require('node:worker_threads'); + module.exports = { test: true }; + `); + + // Create a worker that requires the file + const worker = new Worker(workerFile); + + await new Promise((resolve) => { + worker.on('online', () => { + worker.terminate(); + resolve(); + }); + }); + } finally { + try { unlinkSync(workerFile); } catch {} + } + }); + + it('should not report eval workers', (t, done) => { + // Eval workers should be filtered out + // This is a unit test that validates the condition logic + const isInternal = false; + const doEval = true; + + // Condition: !isInternal && doEval === false + const shouldReport = !isInternal && doEval === false; + assert.strictEqual(shouldReport, false, 'Eval workers should not be reported'); + done(); + }); + + it('should not report internal workers', (t, done) => { + // Internal workers should be filtered out + const isInternal = true; + const doEval = false; + + // Condition: !isInternal && doEval === false + const shouldReport = !isInternal && doEval === false; + assert.strictEqual(shouldReport, false, 'Internal workers should not be reported'); + done(); + }); + + it('should report regular workers', (t, done) => { + // Regular workers should be reported + const isInternal = false; + const doEval = false; + + // Condition: !isInternal && doEval === false + const shouldReport = !isInternal && doEval === false; + assert.strictEqual(shouldReport, true, 'Regular workers should be reported'); + done(); + }); +});