From 43abe7fee81481ca235cc8fb902871e17035bf3c 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 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 718ac026673403a62d382722517ada7093261daa 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 d8a045065fdb0db39376e771d923024dc1267cdb Mon Sep 17 00:00:00 2001 From: SudhansuBandha Date: Thu, 26 Mar 2026 10:17:54 +0530 Subject: [PATCH 3/6] 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 ae6ae367141834d3256beff6d78236b6860dccce 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 25b0954748eb6e5dfd07d80fc292fae18f01ab90 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 f7a05a31f8d70bfae24d5697adb9ee4b9ee40795 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 +});