From ad550d318ccf1d3a6e74c9bbaf8fbf699690eddf Mon Sep 17 00:00:00 2001 From: SudhansuBandha Date: Sat, 21 Mar 2026 22:49:58 +0530 Subject: [PATCH 1/7] watch: track worker 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 watch mode, ensuring restarts when worker files change. Fixes: https://github.com/nodejs/node/issues/62275 Signed-off-by: SudhansuBandha --- 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 33b34eb98b45e7..228ec507af3c78 100644 --- a/lib/internal/watch_mode/files_watcher.js +++ b/lib/internal/watch_mode/files_watcher.js @@ -181,6 +181,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 0b0d91171f0ba5..27f0ff5c4c6134 100644 --- a/lib/internal/worker.js +++ b/lib/internal/worker.js @@ -201,6 +201,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'); @@ -281,6 +292,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 fa012196531b447c7c725f0b7edf8b233716a195 Mon Sep 17 00:00:00 2001 From: SudhansuBandha Date: Sat, 21 Mar 2026 23:23:11 +0530 Subject: [PATCH 2/7] 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 ad191722150999e40714887e15d4720ee8c6986b Mon Sep 17 00:00:00 2001 From: SudhansuBandha Date: Thu, 26 Mar 2026 10:17:54 +0530 Subject: [PATCH 3/7] watch: track worker thread dependencies in --watch mode for cjs files --- lib/internal/modules/cjs/loader.js | 24 ++++++++++++++++++++++ lib/internal/watch_mode/files_watcher.js | 3 --- lib/internal/worker.js | 26 +++++++++--------------- 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index d7df00b3fdc96b..0b59d7e7a0f911 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -329,6 +329,28 @@ function reportModuleNotFoundToWatchMode(basePath, extensions) { } } +/** + * Tell the watch mode that a module was required, from within a worker thread. + * @param {string} filename Absolute path of the module + * @returns {void} + */ +function reportModuleToWatchModeFromWorker(filename) { + if (!shouldReportRequiredModules()) { + return; + } + const { isMainThread } = internalBinding('worker'); + if (isMainThread) { + return; + } + // Lazy require to avoid circular dependency: worker_threads is loaded after + // the CJS loader is fully set up. + const { parentPort } = require('worker_threads'); + if (!parentPort) { + return; + } + parentPort.postMessage({ 'watch:require': [filename] }); +} + /** * Create a new module instance. * @param {string} id @@ -1245,6 +1267,7 @@ Module._load = function(request, parent, isMain, internalResolveOptions = kEmpty relResolveCacheIdentifier = `${parent.path}\x00${request}`; const filename = relativeResolveCache[relResolveCacheIdentifier]; reportModuleToWatchMode(filename); + reportModuleToWatchModeFromWorker(filename); if (filename !== undefined) { const cachedModule = Module._cache[filename]; if (cachedModule !== undefined) { @@ -1335,6 +1358,7 @@ Module._load = function(request, parent, isMain, internalResolveOptions = kEmpty } reportModuleToWatchMode(filename); + reportModuleToWatchModeFromWorker(filename); Module._cache[filename] = module; module[kIsCachedByESMLoader] = false; // If there are resolve hooks, carry the context information into the diff --git a/lib/internal/watch_mode/files_watcher.js b/lib/internal/watch_mode/files_watcher.js index 228ec507af3c78..33b34eb98b45e7 100644 --- a/lib/internal/watch_mode/files_watcher.js +++ b/lib/internal/watch_mode/files_watcher.js @@ -181,9 +181,6 @@ 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 27f0ff5c4c6134..33a05a526c8a49 100644 --- a/lib/internal/worker.js +++ b/lib/internal/worker.js @@ -1,6 +1,7 @@ 'use strict'; const { + ArrayIsArray, ArrayPrototypeForEach, ArrayPrototypeMap, ArrayPrototypePush, @@ -201,17 +202,6 @@ 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'); @@ -292,11 +282,6 @@ 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, @@ -355,6 +340,15 @@ class Worker extends EventEmitter { this[kPublicPort].on(event, (message) => this.emit(event, message)); }); setupPortReferencing(this[kPublicPort], this, 'message'); + + // relay events from worker thread to watcher + if (process.env.WATCH_REPORT_DEPENDENCIES && process.send) { + this[kPublicPort].on('message', (message) => { + if (ArrayIsArray(message?.['watch:require'])) { + process.send({ 'watch:require': message['watch:require'] }); + } + }); + } this[kPort].postMessage({ argv, type: messageTypes.LOAD_SCRIPT, From 74e86f9aff4800820fcfce57c45e3b02eb63c094 Mon Sep 17 00:00:00 2001 From: SudhansuBandha Date: Thu, 26 Mar 2026 13:33:32 +0530 Subject: [PATCH 4/7] watch: track worker thread dependencies in --watch mode for esm modules --- lib/internal/modules/esm/loader.js | 13 +++++++++++++ lib/internal/worker.js | 3 +++ 2 files changed, 16 insertions(+) diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index 876ea6535f2187..b1cf4a68ef4285 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -518,6 +518,19 @@ class ModuleLoader { const type = requestType === kRequireInImportedCJS ? 'require' : 'import'; process.send({ [`watch:${type}`]: [url] }); } + + // Relay Events from worker to main thread + if (process.env.WATCH_REPORT_DEPENDENCIES && !process.send) { + const { isMainThread } = internalBinding('worker'); + if (isMainThread) { + return; + } + const { parentPort } = require('worker_threads'); + if (!parentPort) { + return; + } + parentPort.postMessage({ 'watch:import': [url] }); + } // TODO(joyeecheung): update the module requests to use importAttributes as property names. const importAttributes = resolveResult.importAttributes ?? request.attributes; diff --git a/lib/internal/worker.js b/lib/internal/worker.js index 33a05a526c8a49..6163182be6f3e1 100644 --- a/lib/internal/worker.js +++ b/lib/internal/worker.js @@ -347,6 +347,9 @@ class Worker extends EventEmitter { if (ArrayIsArray(message?.['watch:require'])) { process.send({ 'watch:require': message['watch:require'] }); } + if (ArrayIsArray(message?.['watch:import'])) { + process.send({ 'watch:import': message['watch:import'] }); + } }); } this[kPort].postMessage({ From 469de7f8dafffa456da14016c04a9f21d5a85e78 Mon Sep 17 00:00:00 2001 From: SudhansuBandha Date: Tue, 31 Mar 2026 15:12:08 +0530 Subject: [PATCH 5/7] watch: added tests for worker in -- watch mode to include worker file and nested dependencies --- test/parallel/test-watch-mode-worker.mjs | 67 ------- test/sequential/test-watch-mode.mjs | 236 ++++++++++++++++++++++- 2 files changed, 235 insertions(+), 68 deletions(-) delete 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 deleted file mode 100644 index 5213d34bc6dc6e..00000000000000 --- a/test/parallel/test-watch-mode-worker.mjs +++ /dev/null @@ -1,67 +0,0 @@ -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(); - }); -}); diff --git a/test/sequential/test-watch-mode.mjs b/test/sequential/test-watch-mode.mjs index 707210a021f944..3e3e2ea351081d 100644 --- a/test/sequential/test-watch-mode.mjs +++ b/test/sequential/test-watch-mode.mjs @@ -943,4 +943,238 @@ process.on('message', (message) => { await done(); } }); -}); + + it('should watch changes to worker - cjs', async () => { + const dir = tmpdir.resolve(`watch-worker-cjs-${Date.now()}`); + mkdirSync(dir); + + const worker = path.join(dir, 'worker.js'); + + writeFileSync(worker, ` + console.log("worker running"); + `); + + const file = createTmpFile(` + const { Worker } = require('node:worker_threads'); + + const w = new Worker(${JSON.stringify(worker)}); + w.on('exit', () => { + console.log('running'); + }); + `, '.js', dir); + + const { stderr, stdout } = await runWriteSucceed({ + file, + watchedFile: worker, + }); + + assert.strictEqual(stderr, ''); + assert.deepStrictEqual(stdout, [ + 'worker running', + 'running', + `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, + `Restarting ${inspect(file)}`, + 'worker running', + 'running', + `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, + ]); + }); + + it('should watch changes to worker dependencies - cjs', async () => { + const dir = tmpdir.resolve(`watch-worker-dep-cjs-${Date.now()}`); + mkdirSync(dir); + + const dep = path.join(dir, 'dep.js'); + const worker = path.join(dir, 'worker.js'); + + writeFileSync(dep, ` + module.exports = 'dep v1'; + `); + + writeFileSync(worker, ` + const dep = require('./dep.js'); + console.log(dep); + `); + + const file = createTmpFile(` + const { Worker } = require('node:worker_threads'); + + const w = new Worker(${JSON.stringify(worker)}); + w.on('exit', () => { + console.log('running'); + }); + `, '.js', dir); + + const { stderr, stdout } = await runWriteSucceed({ + file, + watchedFile: dep, + }); + + assert.strictEqual(stderr, ''); + assert.deepStrictEqual(stdout, [ + 'dep v1', + 'running', + `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, + `Restarting ${inspect(file)}`, + 'dep v1', + 'running', + `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, + ]); + }); + + it('should watch changes to nested worker dependencies - cjs', async () => { + const dir = tmpdir.resolve(`watch-worker-dep-cjs-${Date.now()}`); + mkdirSync(dir); + + const subDep = path.join(dir, 'sub-dep.js'); + const dep = path.join(dir, 'dep.js'); + const worker = path.join(dir, 'worker.js'); + + writeFileSync(subDep, ` + module.exports = 'sub-dep v1'; + `); + + writeFileSync(dep, ` + const subDep = require('./sub-dep.js'); + console.log(subDep); + module.exports = 'dep v1'; + `); + + writeFileSync(worker, ` + const dep = require('./dep.js'); + `); + + const file = createTmpFile(` + const { Worker } = require('node:worker_threads'); + + const w = new Worker(${JSON.stringify(worker)}); + w.on('exit', () => { + console.log('running'); + }); + `, '.js', dir); + + const { stderr, stdout } = await runWriteSucceed({ + file, + watchedFile: subDep, + }); + + assert.strictEqual(stderr, ''); + assert.deepStrictEqual(stdout, [ + 'sub-dep v1', + 'running', + `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, + `Restarting ${inspect(file)}`, + 'sub-dep v1', + 'running', + `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, + ]); + }); + + it('should watch changes to worker - esm', async () => { + const dir = tmpdir.resolve(`watch-worker-esm-${Date.now()}`); + mkdirSync(dir); + + const worker = path.join(dir, 'worker.mjs'); + + writeFileSync(worker, ` + console.log("worker running"); + `); + + const file = createTmpFile(` + import { Worker } from 'node:worker_threads'; + new Worker(new URL(${JSON.stringify(pathToFileURL(worker))})); + `, '.mjs', dir); + + const { stderr, stdout } = await runWriteSucceed({ + file, + watchedFile: worker, + }); + + assert.strictEqual(stderr, ''); + assert.deepStrictEqual(stdout, [ + 'worker running', + `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, + `Restarting ${inspect(file)}`, + 'worker running', + `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, + ]); + }); + + it('should watch changes to worker dependencies - esm', async () => { + const dir = tmpdir.resolve(`watch-worker-dep-esm-${Date.now()}`); + mkdirSync(dir); + + const dep = path.join(dir, 'dep.mjs'); + const worker = path.join(dir, 'worker.mjs'); + + writeFileSync(dep, ` + export default 'dep v1'; + `); + + writeFileSync(worker, ` + import dep from ${JSON.stringify(pathToFileURL(dep))}; + console.log(dep); + `); + + const file = createTmpFile(` + import { Worker } from 'node:worker_threads'; + new Worker(new URL(${JSON.stringify(pathToFileURL(worker))})); + `, '.mjs', dir); + + const { stderr, stdout } = await runWriteSucceed({ + file, + watchedFile: dep, + }); + + assert.strictEqual(stderr, ''); + assert.deepStrictEqual(stdout, [ + 'dep v1', + `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, + `Restarting ${inspect(file)}`, + 'dep v1', + `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, + ]); + }); + + it('should watch changes to nested worker dependencies - esm', async () => { + const dir = tmpdir.resolve(`watch-worker-dep-esm-${Date.now()}`); + mkdirSync(dir); + + const subDep = path.join(dir, 'sub-dep.mjs'); + const dep = path.join(dir, 'dep.mjs'); + const worker = path.join(dir, 'worker.mjs'); + + writeFileSync(subDep, ` + export default 'sub-dep v1'; + `); + + writeFileSync(dep, ` + import subDep from ${JSON.stringify(pathToFileURL(subDep))}; + console.log(subDep); + export default 'dep v1'; + `); + + writeFileSync(worker, ` + import dep from ${JSON.stringify(pathToFileURL(dep))}; + `); + + const file = createTmpFile(` + import { Worker } from 'node:worker_threads'; + new Worker(new URL(${JSON.stringify(pathToFileURL(worker))})); + `, '.mjs', dir); + + const { stderr, stdout } = await runWriteSucceed({ + file, + watchedFile: subDep, + }); + + assert.strictEqual(stderr, ''); + assert.deepStrictEqual(stdout, [ + 'sub-dep v1', + `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, + `Restarting ${inspect(file)}`, + 'sub-dep v1', + `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, + ]); + }); +}); \ No newline at end of file From bd505998756518c43e4113a30af332ed141f4108 Mon Sep 17 00:00:00 2001 From: SudhansuBandha Date: Thu, 2 Apr 2026 15:11:09 +0530 Subject: [PATCH 6/7] test: add watch mode coverage for worker threads and dependencies --- lib/internal/modules/esm/loader.js | 13 +- lib/internal/worker.js | 2 +- test/sequential/test-watch-mode-worker.mjs | 281 +++++++++++++++++++++ test/sequential/test-watch-mode.mjs | 236 +---------------- 4 files changed, 288 insertions(+), 244 deletions(-) create mode 100644 test/sequential/test-watch-mode-worker.mjs diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index b1cf4a68ef4285..e12cb7f01baa7d 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -518,18 +518,15 @@ class ModuleLoader { const type = requestType === kRequireInImportedCJS ? 'require' : 'import'; process.send({ [`watch:${type}`]: [url] }); } - // Relay Events from worker to main thread if (process.env.WATCH_REPORT_DEPENDENCIES && !process.send) { const { isMainThread } = internalBinding('worker'); - if (isMainThread) { - return; - } - const { parentPort } = require('worker_threads'); - if (!parentPort) { - return; + if (!isMainThread) { + const { parentPort } = require('worker_threads'); + if (parentPort) { + parentPort.postMessage({ 'watch:import': [url] }); + } } - parentPort.postMessage({ 'watch:import': [url] }); } // TODO(joyeecheung): update the module requests to use importAttributes as property names. diff --git a/lib/internal/worker.js b/lib/internal/worker.js index 6163182be6f3e1..37c44b27397456 100644 --- a/lib/internal/worker.js +++ b/lib/internal/worker.js @@ -341,7 +341,7 @@ class Worker extends EventEmitter { }); setupPortReferencing(this[kPublicPort], this, 'message'); - // relay events from worker thread to watcher + // Relay events from worker thread to watcher if (process.env.WATCH_REPORT_DEPENDENCIES && process.send) { this[kPublicPort].on('message', (message) => { if (ArrayIsArray(message?.['watch:require'])) { diff --git a/test/sequential/test-watch-mode-worker.mjs b/test/sequential/test-watch-mode-worker.mjs new file mode 100644 index 00000000000000..a198f00c7aa961 --- /dev/null +++ b/test/sequential/test-watch-mode-worker.mjs @@ -0,0 +1,281 @@ +import * as common from '../common/index.mjs'; +import tmpdir from '../common/tmpdir.js'; +import assert from 'node:assert'; +import path from 'node:path'; +import { execPath } from 'node:process'; +import { describe, it } from 'node:test'; +import { spawn } from 'node:child_process'; +import { writeFileSync, readFileSync } from 'node:fs'; +import { inspect } from 'node:util'; +import { pathToFileURL } from 'node:url'; +import { createInterface } from 'node:readline'; + +if (common.isIBMi) + common.skip('IBMi does not support `fs.watch()`'); + +function restart(file, content = readFileSync(file)) { + writeFileSync(file, content); + const timer = setInterval(() => writeFileSync(file, content), common.platformTimeout(2500)); + return () => clearInterval(timer); +} + +let tmpFiles = 0; +function createTmpFile(content = 'console.log(\'running\');', ext = '.js', basename = tmpdir.path) { + const file = path.join(basename, `${tmpFiles++}${ext}`); + writeFileSync(file, content); + return file; +} + +async function runWriteSucceed({ + file, + watchedFile, + watchFlag = '--watch', + args = [file], + completed = 'Completed running', + restarts = 2, + options = {}, + shouldFail = false, +}) { + args.unshift('--no-warnings'); + if (watchFlag !== null) args.unshift(watchFlag); + + const child = spawn(execPath, args, { encoding: 'utf8', stdio: 'pipe', ...options }); + + let completes = 0; + let cancelRestarts = () => {}; + let stderr = ''; + const stdout = []; + + child.stderr.on('data', (data) => { + stderr += data; + }); + + try { + for await (const data of createInterface({ input: child.stdout })) { + if (!data.startsWith('Waiting for graceful termination') && + !data.startsWith('Gracefully restarted')) { + stdout.push(data); + } + + if (data.startsWith(completed)) { + completes++; + + if (completes === restarts) break; + + if (completes === 1) { + cancelRestarts = restart(watchedFile); + } + } + + if (!shouldFail && data.startsWith('Failed running')) break; + } + } finally { + child.kill(); + cancelRestarts(); + } + + return { stdout, stderr, pid: child.pid }; +} + +tmpdir.refresh(); +const dir = tmpdir.path; + +describe('watch mode', { concurrency: !process.env.TEST_PARALLEL, timeout: 60_000 }, () => { + it('should watch changes to worker - cjs', async () => { + const worker = path.join(dir, 'worker.js'); + + writeFileSync(worker, ` +console.log('worker running'); +`); + + const file = createTmpFile(` +const { Worker } = require('node:worker_threads'); +const w = new Worker(${JSON.stringify(worker)}); +`, '.js', dir); + + const { stderr, stdout } = await runWriteSucceed({ + file, + watchedFile: worker, + }); + + assert.strictEqual(stderr, ''); + assert.deepStrictEqual(stdout, [ + 'worker running', + `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, + `Restarting ${inspect(file)}`, + 'worker running', + `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, + ]); + }); + + it('should watch changes to worker dependencies - cjs', async () => { + const dep = path.join(dir, 'dep.js'); + const worker = path.join(dir, 'worker.js'); + + writeFileSync(dep, ` +module.exports = 'dep v1'; +`); + + writeFileSync(worker, ` +const dep = require('./dep.js'); +console.log(dep); +`); + + const file = createTmpFile(` +const { Worker } = require('node:worker_threads'); +const w = new Worker(${JSON.stringify(worker)}); +`, '.js', dir); + + const { stderr, stdout } = await runWriteSucceed({ + file, + watchedFile: dep, + }); + + assert.strictEqual(stderr, ''); + assert.deepStrictEqual(stdout, [ + 'dep v1', + `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, + `Restarting ${inspect(file)}`, + 'dep v1', + `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, + ]); + }); + + it('should watch changes to nested worker dependencies - cjs', async () => { + const subDep = path.join(dir, 'sub-dep.js'); + const dep = path.join(dir, 'dep.js'); + const worker = path.join(dir, 'worker.js'); + + writeFileSync(subDep, ` +module.exports = 'sub-dep v1'; +`); + + writeFileSync(dep, ` +const subDep = require('./sub-dep.js'); +console.log(subDep); +module.exports = 'dep v1'; +`); + + writeFileSync(worker, ` +const dep = require('./dep.js'); +`); + + const file = createTmpFile(` +const { Worker } = require('node:worker_threads'); +const w = new Worker(${JSON.stringify(worker)}); +`, '.js', dir); + + const { stderr, stdout } = await runWriteSucceed({ + file, + watchedFile: subDep, + }); + + assert.strictEqual(stderr, ''); + assert.deepStrictEqual(stdout, [ + 'sub-dep v1', + `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, + `Restarting ${inspect(file)}`, + 'sub-dep v1', + `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, + ]); + }); + + it('should watch changes to worker - esm', async () => { + const worker = path.join(dir, 'worker.mjs'); + + writeFileSync(worker, ` +console.log('worker running'); +`); + + const file = createTmpFile(` +import { Worker } from 'node:worker_threads'; +new Worker(new URL(${JSON.stringify(pathToFileURL(worker))})); +`, '.mjs', dir); + + const { stderr, stdout } = await runWriteSucceed({ + file, + watchedFile: worker, + }); + + assert.strictEqual(stderr, ''); + assert.deepStrictEqual(stdout, [ + 'worker running', + `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, + `Restarting ${inspect(file)}`, + 'worker running', + `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, + ]); + }); + + it('should watch changes to worker dependencies - esm', async () => { + const dep = path.join(dir, 'dep.mjs'); + const worker = path.join(dir, 'worker.mjs'); + + writeFileSync(dep, ` +export default 'dep v1'; +`); + + writeFileSync(worker, ` +import dep from ${JSON.stringify(pathToFileURL(dep))}; +console.log(dep); +`); + + const file = createTmpFile(` +import { Worker } from 'node:worker_threads'; +new Worker(new URL(${JSON.stringify(pathToFileURL(worker))})); +`, '.mjs', dir); + + const { stderr, stdout } = await runWriteSucceed({ + file, + watchedFile: dep, + }); + + assert.strictEqual(stderr, ''); + assert.deepStrictEqual(stdout, [ + 'dep v1', + `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, + `Restarting ${inspect(file)}`, + 'dep v1', + `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, + ]); + }); + + it('should watch changes to nested worker dependencies - esm', async () => { + const subDep = path.join(dir, 'sub-dep.mjs'); + const dep = path.join(dir, 'dep.mjs'); + const worker = path.join(dir, 'worker.mjs'); + + writeFileSync(subDep, ` +export default 'sub-dep v1'; +`); + + writeFileSync(dep, ` +import subDep from ${JSON.stringify(pathToFileURL(subDep))}; +console.log(subDep); +export default 'dep v1'; +`); + + writeFileSync(worker, ` +import dep from ${JSON.stringify(pathToFileURL(dep))}; +`); + + const file = createTmpFile(` +import { Worker } from 'node:worker_threads'; +new Worker(new URL(${JSON.stringify(pathToFileURL(worker))})); +`, '.mjs', dir); + + const { stderr, stdout } = await runWriteSucceed({ + file, + watchedFile: subDep, + }); + + assert.strictEqual(stderr, ''); + assert.deepStrictEqual(stdout, [ + 'sub-dep v1', + `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, + `Restarting ${inspect(file)}`, + 'sub-dep v1', + `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, + ]); + }); +}); diff --git a/test/sequential/test-watch-mode.mjs b/test/sequential/test-watch-mode.mjs index 3e3e2ea351081d..707210a021f944 100644 --- a/test/sequential/test-watch-mode.mjs +++ b/test/sequential/test-watch-mode.mjs @@ -943,238 +943,4 @@ process.on('message', (message) => { await done(); } }); - - it('should watch changes to worker - cjs', async () => { - const dir = tmpdir.resolve(`watch-worker-cjs-${Date.now()}`); - mkdirSync(dir); - - const worker = path.join(dir, 'worker.js'); - - writeFileSync(worker, ` - console.log("worker running"); - `); - - const file = createTmpFile(` - const { Worker } = require('node:worker_threads'); - - const w = new Worker(${JSON.stringify(worker)}); - w.on('exit', () => { - console.log('running'); - }); - `, '.js', dir); - - const { stderr, stdout } = await runWriteSucceed({ - file, - watchedFile: worker, - }); - - assert.strictEqual(stderr, ''); - assert.deepStrictEqual(stdout, [ - 'worker running', - 'running', - `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, - `Restarting ${inspect(file)}`, - 'worker running', - 'running', - `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, - ]); - }); - - it('should watch changes to worker dependencies - cjs', async () => { - const dir = tmpdir.resolve(`watch-worker-dep-cjs-${Date.now()}`); - mkdirSync(dir); - - const dep = path.join(dir, 'dep.js'); - const worker = path.join(dir, 'worker.js'); - - writeFileSync(dep, ` - module.exports = 'dep v1'; - `); - - writeFileSync(worker, ` - const dep = require('./dep.js'); - console.log(dep); - `); - - const file = createTmpFile(` - const { Worker } = require('node:worker_threads'); - - const w = new Worker(${JSON.stringify(worker)}); - w.on('exit', () => { - console.log('running'); - }); - `, '.js', dir); - - const { stderr, stdout } = await runWriteSucceed({ - file, - watchedFile: dep, - }); - - assert.strictEqual(stderr, ''); - assert.deepStrictEqual(stdout, [ - 'dep v1', - 'running', - `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, - `Restarting ${inspect(file)}`, - 'dep v1', - 'running', - `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, - ]); - }); - - it('should watch changes to nested worker dependencies - cjs', async () => { - const dir = tmpdir.resolve(`watch-worker-dep-cjs-${Date.now()}`); - mkdirSync(dir); - - const subDep = path.join(dir, 'sub-dep.js'); - const dep = path.join(dir, 'dep.js'); - const worker = path.join(dir, 'worker.js'); - - writeFileSync(subDep, ` - module.exports = 'sub-dep v1'; - `); - - writeFileSync(dep, ` - const subDep = require('./sub-dep.js'); - console.log(subDep); - module.exports = 'dep v1'; - `); - - writeFileSync(worker, ` - const dep = require('./dep.js'); - `); - - const file = createTmpFile(` - const { Worker } = require('node:worker_threads'); - - const w = new Worker(${JSON.stringify(worker)}); - w.on('exit', () => { - console.log('running'); - }); - `, '.js', dir); - - const { stderr, stdout } = await runWriteSucceed({ - file, - watchedFile: subDep, - }); - - assert.strictEqual(stderr, ''); - assert.deepStrictEqual(stdout, [ - 'sub-dep v1', - 'running', - `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, - `Restarting ${inspect(file)}`, - 'sub-dep v1', - 'running', - `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, - ]); - }); - - it('should watch changes to worker - esm', async () => { - const dir = tmpdir.resolve(`watch-worker-esm-${Date.now()}`); - mkdirSync(dir); - - const worker = path.join(dir, 'worker.mjs'); - - writeFileSync(worker, ` - console.log("worker running"); - `); - - const file = createTmpFile(` - import { Worker } from 'node:worker_threads'; - new Worker(new URL(${JSON.stringify(pathToFileURL(worker))})); - `, '.mjs', dir); - - const { stderr, stdout } = await runWriteSucceed({ - file, - watchedFile: worker, - }); - - assert.strictEqual(stderr, ''); - assert.deepStrictEqual(stdout, [ - 'worker running', - `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, - `Restarting ${inspect(file)}`, - 'worker running', - `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, - ]); - }); - - it('should watch changes to worker dependencies - esm', async () => { - const dir = tmpdir.resolve(`watch-worker-dep-esm-${Date.now()}`); - mkdirSync(dir); - - const dep = path.join(dir, 'dep.mjs'); - const worker = path.join(dir, 'worker.mjs'); - - writeFileSync(dep, ` - export default 'dep v1'; - `); - - writeFileSync(worker, ` - import dep from ${JSON.stringify(pathToFileURL(dep))}; - console.log(dep); - `); - - const file = createTmpFile(` - import { Worker } from 'node:worker_threads'; - new Worker(new URL(${JSON.stringify(pathToFileURL(worker))})); - `, '.mjs', dir); - - const { stderr, stdout } = await runWriteSucceed({ - file, - watchedFile: dep, - }); - - assert.strictEqual(stderr, ''); - assert.deepStrictEqual(stdout, [ - 'dep v1', - `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, - `Restarting ${inspect(file)}`, - 'dep v1', - `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, - ]); - }); - - it('should watch changes to nested worker dependencies - esm', async () => { - const dir = tmpdir.resolve(`watch-worker-dep-esm-${Date.now()}`); - mkdirSync(dir); - - const subDep = path.join(dir, 'sub-dep.mjs'); - const dep = path.join(dir, 'dep.mjs'); - const worker = path.join(dir, 'worker.mjs'); - - writeFileSync(subDep, ` - export default 'sub-dep v1'; - `); - - writeFileSync(dep, ` - import subDep from ${JSON.stringify(pathToFileURL(subDep))}; - console.log(subDep); - export default 'dep v1'; - `); - - writeFileSync(worker, ` - import dep from ${JSON.stringify(pathToFileURL(dep))}; - `); - - const file = createTmpFile(` - import { Worker } from 'node:worker_threads'; - new Worker(new URL(${JSON.stringify(pathToFileURL(worker))})); - `, '.mjs', dir); - - const { stderr, stdout } = await runWriteSucceed({ - file, - watchedFile: subDep, - }); - - assert.strictEqual(stderr, ''); - assert.deepStrictEqual(stdout, [ - 'sub-dep v1', - `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, - `Restarting ${inspect(file)}`, - 'sub-dep v1', - `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, - ]); - }); -}); \ No newline at end of file +}); From a48176d0b96eb6bc4a6efd76c20fec83dc4b2fa8 Mon Sep 17 00:00:00 2001 From: SudhansuBandha Date: Mon, 6 Apr 2026 16:45:46 +0530 Subject: [PATCH 7/7] test: reduce timer interval to 250ms for faster execution --- test/sequential/test-watch-mode-worker.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/sequential/test-watch-mode-worker.mjs b/test/sequential/test-watch-mode-worker.mjs index a198f00c7aa961..b7bc1a94a87ba4 100644 --- a/test/sequential/test-watch-mode-worker.mjs +++ b/test/sequential/test-watch-mode-worker.mjs @@ -15,7 +15,7 @@ if (common.isIBMi) function restart(file, content = readFileSync(file)) { writeFileSync(file, content); - const timer = setInterval(() => writeFileSync(file, content), common.platformTimeout(2500)); + const timer = setInterval(() => writeFileSync(file, content), common.platformTimeout(250)); return () => clearInterval(timer); }