From 48e5a4a7174e31a813ac1ca0cd37c9f9671b3ac6 Mon Sep 17 00:00:00 2001 From: SudhansuBandha Date: Sat, 21 Mar 2026 22:49:58 +0530 Subject: [PATCH 1/6] 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 06d754b2a9a95b5542cb4f3e1ff3f99fa88ca929 Mon Sep 17 00:00:00 2001 From: SudhansuBandha Date: Sat, 21 Mar 2026 23:23:11 +0530 Subject: [PATCH 2/6] 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 db4875c654b32d068eaf941d1596f0691dee473f Mon Sep 17 00:00:00 2001 From: SudhansuBandha Date: Thu, 26 Mar 2026 10:17:54 +0530 Subject: [PATCH 3/6] wacth: 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 c14648548b514593ad1fa9b66051a7f045aa38b6 Mon Sep 17 00:00:00 2001 From: SudhansuBandha Date: Thu, 26 Mar 2026 13:33:32 +0530 Subject: [PATCH 4/6] 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 5486d3f5031dddb0d644ceb81e29c72d9d2247ef Mon Sep 17 00:00:00 2001 From: SudhansuBandha Date: Tue, 31 Mar 2026 15:12:08 +0530 Subject: [PATCH 5/6] 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 9a661cddda8cd81ab21da800895e3170e0c13661 Mon Sep 17 00:00:00 2001 From: SudhansuBandha Date: Thu, 2 Apr 2026 15:11:09 +0530 Subject: [PATCH 6/6] 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 +});