From f9beab083c81213050e361532415b99e5372d3b3 Mon Sep 17 00:00:00 2001 From: SudhansuBandha Date: Sat, 21 Mar 2026 22:49:58 +0530 Subject: [PATCH 1/3] watch: track worker thread entry files in --watch mode Currently, --watch mode only tracks dependencies from the main module graph (require/import). Worker thread entry points created via new Worker() are not included, so changes to worker files do not trigger restarts. This change hooks into Worker initialization and registers the worker entry file with the watch mode, ensuring restarts when worker files change. Fixes: https://github.com/nodejs/node/issues/62275 --- lib/internal/watch_mode/files_watcher.js | 3 +++ lib/internal/worker.js | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/lib/internal/watch_mode/files_watcher.js b/lib/internal/watch_mode/files_watcher.js index 9c0eb1ed817c29..fbaba9c34dd3ee 100644 --- a/lib/internal/watch_mode/files_watcher.js +++ b/lib/internal/watch_mode/files_watcher.js @@ -179,6 +179,9 @@ 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'], (file) => this.filterFile(file, 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, From ba11d26793b28cee1e7a22083ea217aa9778ebbb Mon Sep 17 00:00:00 2001 From: SudhansuBandha Date: Sat, 21 Mar 2026 23:23:11 +0530 Subject: [PATCH 2/3] test: add test coverage for worker entry files in --watch mode --- test/parallel/test-watch-mode-worker.mjs | 67 ++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 test/parallel/test-watch-mode-worker.mjs 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(); + }); +}); From d26b72738722cb11fedc187ecbb66c49adcbf724 Mon Sep 17 00:00:00 2001 From: SudhansuBandha Date: Mon, 23 Mar 2026 08:32:47 +0530 Subject: [PATCH 3/3] watch: re-run worker when its dependencies change --- lib/internal/watch_mode/files_watcher.js | 37 ++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/lib/internal/watch_mode/files_watcher.js b/lib/internal/watch_mode/files_watcher.js index fbaba9c34dd3ee..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); @@ -180,7 +203,17 @@ class FilesWatcher extends EventEmitter { ArrayPrototypeForEach(message['watch:import'], (file) => this.filterFile(fileURLToPath(file), key)); } if (ArrayIsArray(message['watch:worker'])) { - ArrayPrototypeForEach(message['watch:worker'], (file) => this.filterFile(file, key)); + 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