From f3dd040220eff50f272bedd7afbdc6c43cec7d5f 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 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 81a7e6354ce3b35424d92a0d9054c09fdf9a73b0 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 b350212a5b83616e09036c0756a133b6ac39c323 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 827655bedb65bf..d1ab0bf54d0a2b 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 fbaba9c34dd3ee..9c0eb1ed817c29 100644 --- a/lib/internal/watch_mode/files_watcher.js +++ b/lib/internal/watch_mode/files_watcher.js @@ -179,9 +179,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 20323b49449086..00949d4bd53a97 100644 --- a/lib/internal/worker.js +++ b/lib/internal/worker.js @@ -1,6 +1,7 @@ 'use strict'; const { + ArrayIsArray, ArrayPrototypeForEach, ArrayPrototypeMap, ArrayPrototypePush, @@ -195,17 +196,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'); @@ -286,11 +276,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, @@ -349,6 +334,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 f04d1b164a04847ffb2d5aa3fb3262db9ee064b5 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 8ae6761fba571a..a85ac6cd6174e8 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -534,6 +534,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 00949d4bd53a97..a173fc466ceb54 100644 --- a/lib/internal/worker.js +++ b/lib/internal/worker.js @@ -341,6 +341,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 d910fe41cb8ffb8221b50fa18bfe2613fd816269 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 a5cac129ad1c21..12705573396376 100644 --- a/test/sequential/test-watch-mode.mjs +++ b/test/sequential/test-watch-mode.mjs @@ -922,4 +922,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 9376a05c768163d439fa7e671e13e9178535952b 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 a85ac6cd6174e8..c016796accb1be 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -534,18 +534,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 a173fc466ceb54..f457f0ee30a7c3 100644 --- a/lib/internal/worker.js +++ b/lib/internal/worker.js @@ -335,7 +335,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 12705573396376..a5cac129ad1c21 100644 --- a/test/sequential/test-watch-mode.mjs +++ b/test/sequential/test-watch-mode.mjs @@ -922,238 +922,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 e56a31cfb0624b44bf93cd2dd547628a9a406c1b 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); }