Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions lib/internal/modules/esm/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,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;
Expand Down
13 changes: 13 additions & 0 deletions lib/internal/worker.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

const {
ArrayIsArray,
ArrayPrototypeForEach,
ArrayPrototypeMap,
ArrayPrototypePush,
Expand Down Expand Up @@ -333,6 +334,18 @@ 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'] });
}
if (ArrayIsArray(message?.['watch:import'])) {
process.send({ 'watch:import': message['watch:import'] });
}
});
}
this[kPort].postMessage({
argv,
type: messageTypes.LOAD_SCRIPT,
Expand Down
67 changes: 67 additions & 0 deletions test/parallel/test-watch-mode-worker.mjs
Original file line number Diff line number Diff line change
@@ -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();
});
});