From 31caef8823a2574aab2ac8d1ca8f52809f2badeb Mon Sep 17 00:00:00 2001 From: Xavier Stouder Date: Thu, 1 Jan 2026 01:36:28 +0100 Subject: [PATCH 1/6] esm: ensure watch mode restarts after syntax errors Move watch dependency reporting earlier in module resolution to ensure file dependencies are tracked even when parsing fails. Fixes: https://github.com/nodejs/node/issues/61153 --- lib/internal/modules/esm/loader.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index 40dafb59687b28..0bff0763fcf58f 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -542,6 +542,12 @@ class ModuleLoader { */ #getOrCreateModuleJobAfterResolve(parentURL, resolveResult, request, requestType) { const { url, format } = resolveResult; + + if (process.env.WATCH_REPORT_DEPENDENCIES && process.send) { + const type = requestType === kRequireInImportedCJS ? 'require' : 'import'; + process.send({ [`watch:${type}`]: [url] }); + } + // TODO(joyeecheung): update the module requests to use importAttributes as property names. const importAttributes = resolveResult.importAttributes ?? request.attributes; let job = this.loadCache.get(url, importAttributes.type); @@ -570,11 +576,6 @@ class ModuleLoader { assert(moduleOrModulePromise instanceof ModuleWrap, `Expected ModuleWrap for loading ${url}`); } - if (process.env.WATCH_REPORT_DEPENDENCIES && process.send) { - const type = requestType === kRequireInImportedCJS ? 'require' : 'import'; - process.send({ [`watch:${type}`]: [url] }); - } - const { ModuleJob, ModuleJobSync } = require('internal/modules/esm/module_job'); // TODO(joyeecheung): use ModuleJobSync for kRequireInImportedCJS too. const ModuleJobCtor = (requestType === kImportInRequiredESM ? ModuleJobSync : ModuleJob); From 0e69c4da38bcbdf9b6ed5e1fae9e0e813542a6a6 Mon Sep 17 00:00:00 2001 From: Xavier Stouder Date: Fri, 2 Jan 2026 15:43:35 +0100 Subject: [PATCH 2/6] test --- test/sequential/test-watch-mode.mjs | 47 +++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/test/sequential/test-watch-mode.mjs b/test/sequential/test-watch-mode.mjs index b9e57cb30bedc9..c8ff0b43b39f82 100644 --- a/test/sequential/test-watch-mode.mjs +++ b/test/sequential/test-watch-mode.mjs @@ -884,4 +884,51 @@ process.on('message', (message) => { await done(); } }); + + it('should watch changes even when there is syntax errors during esm loading', async () => { + // Create initial file with valid code + const initialContent = `console.log('hello, world');`; + const file = createTmpFile(initialContent, '.mjs'); + + const { done, restart } = runInBackground({ + args: ['--watch', file], + completed: 'Completed running', + shouldFail: true, + }); + + try { + const { stdout, stderr } = await restart(); + assert.strictEqual(stderr, ''); + assert.deepStrictEqual(stdout, [ + 'hello, world', + `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, + ]) + + // Update file with syntax error + const syntaxErrorContent = `console.log('hello, wor`; + writeFileSync(file, syntaxErrorContent); + + // Wait for the failed restart + const { stderr: stderr2, stdout: stdout2 } = await restart(); + assert.match(stderr2, /SyntaxError: Invalid or unexpected token/); + assert.deepStrictEqual(stdout2, [ + `Restarting ${inspect(file)}`, + `Failed running ${inspect(file)}. Waiting for file changes before restarting...`, + ]) + + writeFileSync(file, `console.log('hello again, world');`); + + const { stderr: stderr3, stdout: stdout3 } = await restart(); + + // Verify it recovered and ran successfully + assert.strictEqual(stderr3, ''); + assert.deepStrictEqual(stdout3, [ + `Restarting ${inspect(file)}`, + 'hello again, world', + `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, + ]); + } finally { + await done(); + } + }); }); From 91fddbe030bab74a6bbe6b8fac92bdd1ce067430 Mon Sep 17 00:00:00 2001 From: Xavier Stouder Date: Fri, 2 Jan 2026 16:30:38 +0100 Subject: [PATCH 3/6] lint --- test/sequential/test-watch-mode.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/sequential/test-watch-mode.mjs b/test/sequential/test-watch-mode.mjs index c8ff0b43b39f82..a141c3c9126eb3 100644 --- a/test/sequential/test-watch-mode.mjs +++ b/test/sequential/test-watch-mode.mjs @@ -902,7 +902,7 @@ process.on('message', (message) => { assert.deepStrictEqual(stdout, [ 'hello, world', `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, - ]) + ]); // Update file with syntax error const syntaxErrorContent = `console.log('hello, wor`; @@ -914,7 +914,7 @@ process.on('message', (message) => { assert.deepStrictEqual(stdout2, [ `Restarting ${inspect(file)}`, `Failed running ${inspect(file)}. Waiting for file changes before restarting...`, - ]) + ]); writeFileSync(file, `console.log('hello again, world');`); From 4b26149d9fd5d73e58f6c93c20bad322a6b0c9bb Mon Sep 17 00:00:00 2001 From: Xavier Stouder Date: Sun, 4 Jan 2026 01:11:11 +0100 Subject: [PATCH 4/6] separate test --- ...t-watch-mode-restart-esm-loading-error.mjs | 127 ++++++++++++++++++ test/sequential/test-watch-mode.mjs | 43 ------ 2 files changed, 127 insertions(+), 43 deletions(-) create mode 100644 test/sequential/test-watch-mode-restart-esm-loading-error.mjs diff --git a/test/sequential/test-watch-mode-restart-esm-loading-error.mjs b/test/sequential/test-watch-mode-restart-esm-loading-error.mjs new file mode 100644 index 00000000000000..7bc90e4e413492 --- /dev/null +++ b/test/sequential/test-watch-mode-restart-esm-loading-error.mjs @@ -0,0 +1,127 @@ +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 { spawn } from 'node:child_process'; +import { writeFileSync } from 'node:fs'; +import { inspect } from 'node:util'; +import { createInterface } from 'node:readline'; +import { it, before } from 'node:test'; + +if (common.isIBMi) + common.skip('IBMi does not support `fs.watch()`'); + +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; +} + +function runInBackground({ args = [], options = {}, completed = 'Completed running', shouldFail = false }) { + let future = Promise.withResolvers(); + let child; + let stderr = ''; + let stdout = []; + + const run = () => { + args.unshift('--no-warnings'); + child = spawn(execPath, args, { encoding: 'utf8', stdio: 'pipe', ...options }); + + child.stderr.on('data', (data) => { + stderr += data; + }); + + const rl = createInterface({ input: child.stdout }); + rl.on('line', (data) => { + if (!data.startsWith('Waiting for graceful termination') && !data.startsWith('Gracefully restarted')) { + stdout.push(data); + if (data.startsWith(completed)) { + future.resolve({ stderr, stdout }); + future = Promise.withResolvers(); + stdout = []; + stderr = ''; + } else if (data.startsWith('Failed running')) { + if (shouldFail) { + future.resolve({ stderr, stdout }); + } else { + future.reject({ stderr, stdout }); + } + future = Promise.withResolvers(); + stdout = []; + stderr = ''; + } + } + }); + }; + + return { + async done() { + child?.kill(); + future.resolve(); + return { stdout, stderr }; + }, + restart(timeout = 1000) { + if (!child) { + run(); + } + const timer = setTimeout(() => { + if (!future.resolved) { + child.kill(); + future.reject(new Error('Timed out waiting for restart')); + } + }, timeout); + return future.promise.finally(() => { + clearTimeout(timer); + }); + }, + }; +} + +tmpdir.refresh(); + +// Create initial file with valid code +const initialContent = `console.log('hello, world');`; +const file = createTmpFile(initialContent, '.mjs'); + +const { done, restart } = runInBackground({ + args: ['--watch', file], + completed: 'Completed running', + shouldFail: true, +}); + +try { + const { stdout, stderr } = await restart(); + assert.strictEqual(stderr, ''); + assert.deepStrictEqual(stdout, [ + 'hello, world', + `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, + ]); + + // Update file with syntax error + const syntaxErrorContent = `console.log('hello, wor`; + writeFileSync(file, syntaxErrorContent); + + // Wait for the failed restart + const { stderr: stderr2, stdout: stdout2 } = await restart(); + assert.match(stderr2, /SyntaxError: Invalid or unexpected token/); + assert.deepStrictEqual(stdout2, [ + `Restarting ${inspect(file)}`, + `Failed running ${inspect(file)}. Waiting for file changes before restarting...`, + ]); + + writeFileSync(file, `console.log('hello again, world');`); + + const { stderr: stderr3, stdout: stdout3 } = await restart(); + + // Verify it recovered and ran successfully + assert.strictEqual(stderr3, ''); + assert.deepStrictEqual(stdout3, [ + `Restarting ${inspect(file)}`, + 'hello again, world', + `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, + ]); +} finally { + await done(); +} \ No newline at end of file diff --git a/test/sequential/test-watch-mode.mjs b/test/sequential/test-watch-mode.mjs index a141c3c9126eb3..3f657a486c9fb9 100644 --- a/test/sequential/test-watch-mode.mjs +++ b/test/sequential/test-watch-mode.mjs @@ -886,49 +886,6 @@ process.on('message', (message) => { }); it('should watch changes even when there is syntax errors during esm loading', async () => { - // Create initial file with valid code - const initialContent = `console.log('hello, world');`; - const file = createTmpFile(initialContent, '.mjs'); - const { done, restart } = runInBackground({ - args: ['--watch', file], - completed: 'Completed running', - shouldFail: true, - }); - - try { - const { stdout, stderr } = await restart(); - assert.strictEqual(stderr, ''); - assert.deepStrictEqual(stdout, [ - 'hello, world', - `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, - ]); - - // Update file with syntax error - const syntaxErrorContent = `console.log('hello, wor`; - writeFileSync(file, syntaxErrorContent); - - // Wait for the failed restart - const { stderr: stderr2, stdout: stdout2 } = await restart(); - assert.match(stderr2, /SyntaxError: Invalid or unexpected token/); - assert.deepStrictEqual(stdout2, [ - `Restarting ${inspect(file)}`, - `Failed running ${inspect(file)}. Waiting for file changes before restarting...`, - ]); - - writeFileSync(file, `console.log('hello again, world');`); - - const { stderr: stderr3, stdout: stdout3 } = await restart(); - - // Verify it recovered and ran successfully - assert.strictEqual(stderr3, ''); - assert.deepStrictEqual(stdout3, [ - `Restarting ${inspect(file)}`, - 'hello again, world', - `Completed running ${inspect(file)}. Waiting for file changes before restarting...`, - ]); - } finally { - await done(); - } }); }); From d112842c16c358c21a91f98616640219646b6a64 Mon Sep 17 00:00:00 2001 From: Xavier Stouder Date: Sun, 4 Jan 2026 01:17:43 +0100 Subject: [PATCH 5/6] lint --- test/sequential/test-watch-mode-restart-esm-loading-error.mjs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/sequential/test-watch-mode-restart-esm-loading-error.mjs b/test/sequential/test-watch-mode-restart-esm-loading-error.mjs index 7bc90e4e413492..9d937aa2ed93cb 100644 --- a/test/sequential/test-watch-mode-restart-esm-loading-error.mjs +++ b/test/sequential/test-watch-mode-restart-esm-loading-error.mjs @@ -7,7 +7,6 @@ import { spawn } from 'node:child_process'; import { writeFileSync } from 'node:fs'; import { inspect } from 'node:util'; import { createInterface } from 'node:readline'; -import { it, before } from 'node:test'; if (common.isIBMi) common.skip('IBMi does not support `fs.watch()`'); @@ -124,4 +123,4 @@ try { ]); } finally { await done(); -} \ No newline at end of file +} From 9c147faa28d47f835b27e85271899bc2e03aa764 Mon Sep 17 00:00:00 2001 From: Xavier Stouder Date: Sun, 4 Jan 2026 17:53:08 +0100 Subject: [PATCH 6/6] forgotten code --- test/sequential/test-watch-mode.mjs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/sequential/test-watch-mode.mjs b/test/sequential/test-watch-mode.mjs index 3f657a486c9fb9..b9e57cb30bedc9 100644 --- a/test/sequential/test-watch-mode.mjs +++ b/test/sequential/test-watch-mode.mjs @@ -884,8 +884,4 @@ process.on('message', (message) => { await done(); } }); - - it('should watch changes even when there is syntax errors during esm loading', async () => { - - }); });