From c24801bd65ae80dd4e2eef34a67c0759b211b3f7 Mon Sep 17 00:00:00 2001 From: HitkoDev Date: Fri, 15 Sep 2023 18:25:39 +0200 Subject: [PATCH 1/5] Fix ESM loader to work with Node 20 `process.send` is no longer accessible from ESM loader in Node 20, therefore loader needs to use `context.port` and a proxy function to send messages to the parent. See https://nodejs.org/api/esm.html#globalpreload --- .travis.yml | 1 + lib/loaders/ipc.mjs | 4 ++++ lib/loaders/load.mjs | 20 +++++++++++++++++-- .../index.mjs | 1 - test/fixture/resolution.mjs | 7 ------- test/spawn/esmodule.js | 14 ------------- 6 files changed, 23 insertions(+), 24 deletions(-) delete mode 100644 test/fixture/experimental-specifier-resolution/index.mjs delete mode 100644 test/fixture/resolution.mjs diff --git a/.travis.yml b/.travis.yml index 25d33de..a4ebd4d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ jobs: script: npm run lint language: node_js node_js: + - 20 - 18 - 16 - 14 diff --git a/lib/loaders/ipc.mjs b/lib/loaders/ipc.mjs index 61c48c4..8fc1aad 100644 --- a/lib/loaders/ipc.mjs +++ b/lib/loaders/ipc.mjs @@ -3,3 +3,7 @@ const cmd = 'NODE_DEV'; export const send = m => { if (process.connected) process.send({ ...m, cmd }); }; + +export const sendPort = (port, m) => { + if (port) port.postMessage({ ...m, cmd }); +}; diff --git a/lib/loaders/load.mjs b/lib/loaders/load.mjs index 2772f60..a1c4fb4 100644 --- a/lib/loaders/load.mjs +++ b/lib/loaders/load.mjs @@ -1,13 +1,17 @@ import { createRequire } from 'module'; import { fileURLToPath } from 'url'; -import { send } from './ipc.mjs'; +import { sendPort } from './ipc.mjs'; const require = createRequire(import.meta.url); +// Port used for communication between the loader and ESM modules +// https://nodejs.org/api/esm.html#globalpreload +let port; + export async function load(url, context, defaultLoad) { const required = url.startsWith('file://') ? fileURLToPath(url) : url; - send({ required }); + sendPort(port, { required }); try { return await defaultLoad(url, context, defaultLoad); @@ -19,3 +23,15 @@ export async function load(url, context, defaultLoad) { }); } } + +export const globalPreload = (context) => { + // Store port + port = context.port; + + // Inject code to forward loader events to the parent + return ` +port.on('message', (m) => { + if (process.connected) process.send(m); +}); + `; +}; diff --git a/test/fixture/experimental-specifier-resolution/index.mjs b/test/fixture/experimental-specifier-resolution/index.mjs deleted file mode 100644 index 0ac70ad..0000000 --- a/test/fixture/experimental-specifier-resolution/index.mjs +++ /dev/null @@ -1 +0,0 @@ -export default 'experimental-specifier-resolution'; diff --git a/test/fixture/resolution.mjs b/test/fixture/resolution.mjs deleted file mode 100644 index c4d373c..0000000 --- a/test/fixture/resolution.mjs +++ /dev/null @@ -1,7 +0,0 @@ -import resolution from './experimental-specifier-resolution'; -import message from './message'; - -setTimeout(() => {}, 10000); - -console.log(resolution); -console.log(message); diff --git a/test/spawn/esmodule.js b/test/spawn/esmodule.js index 1b6b816..1fa2c5f 100644 --- a/test/spawn/esmodule.js +++ b/test/spawn/esmodule.js @@ -2,20 +2,6 @@ const tap = require('tap'); const { spawn, touchFile } = require('../utils'); -tap.test('Supports ECMAScript modules with experimental-specifier-resolution', t => { - spawn('--experimental-specifier-resolution=node resolution.mjs', out => { - if (out.match(/touch message.js/)) { - touchFile('message.js'); - return out2 => { - if (out2.match(/Restarting/)) { - t.match(out2, /\[INFO\] \d{2}:\d{2}:\d{2} Restarting/); - return { exit: t.end.bind(t) }; - } - }; - } - }); -}); - tap.test('Supports ECMAScript modules', t => { spawn('ecma-script-modules.mjs', out => { if (out.match(/touch message.mjs/)) { From fd7abff3b237329a14c92c7779eb54248b6c0ee2 Mon Sep 17 00:00:00 2001 From: HitkoDev Date: Sat, 16 Sep 2023 20:11:10 +0200 Subject: [PATCH 2/5] Run test on node 20 --- .appveyor.yml | 1 + .github/workflows/nodejs.yml | 4 ++-- lib/loaders/load.mjs | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index bc20403..071d68c 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -2,6 +2,7 @@ build: off environment: matrix: + - nodejs_version: '20' - nodejs_version: '18' - nodejs_version: '16' - nodejs_version: '14' diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 8073209..e7593c3 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: - node-version: ["14.x", "16.x", "18.x"] + node-version: ["14.x", "16.x", "18.x", "20.x"] steps: - uses: actions/checkout@v2 @@ -29,7 +29,7 @@ jobs: strategy: matrix: - node-version: ["14.x", "16.x", "18.x"] + node-version: ["14.x", "16.x", "18.x", "20.x"] steps: - uses: actions/checkout@v2 diff --git a/lib/loaders/load.mjs b/lib/loaders/load.mjs index a1c4fb4..d31ff29 100644 --- a/lib/loaders/load.mjs +++ b/lib/loaders/load.mjs @@ -32,6 +32,6 @@ export const globalPreload = (context) => { return ` port.on('message', (m) => { if (process.connected) process.send(m); -}); +}).unref(); `; }; From 00f0137ac427a3b37562d4b6d3cbec257def3b9e Mon Sep 17 00:00:00 2001 From: HitkoDev Date: Sat, 16 Sep 2023 21:26:24 +0200 Subject: [PATCH 3/5] Resovle dependency issues --- .github/workflows/nodejs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index e7593c3..2e4dde2 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -37,5 +37,7 @@ jobs: uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} + - if: matrix.node-version == '14.x' + run: npm install -g npm@9 # node 14 comes with npm 6, which has some known bugs with dependency resolution - run: npm i - run: npm test From cfff80f0379ff5d3aef418b2a93f6bc55b164d3f Mon Sep 17 00:00:00 2001 From: Hitko Development Date: Wed, 11 Feb 2026 21:31:53 +0100 Subject: [PATCH 4/5] Update for node 20.6+ --- lib/loaders/load.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/loaders/load.mjs b/lib/loaders/load.mjs index d31ff29..c27584e 100644 --- a/lib/loaders/load.mjs +++ b/lib/loaders/load.mjs @@ -24,7 +24,7 @@ export async function load(url, context, defaultLoad) { } } -export const globalPreload = (context) => { +export const initialize = async (context) => { // Store port port = context.port; From 0e79d72625c84e045fb56c65a1e70b8f3eacbc09 Mon Sep 17 00:00:00 2001 From: HitkoDev Date: Wed, 11 Feb 2026 22:25:39 +0100 Subject: [PATCH 5/5] Support node 21+ --- lib/index.js | 4 +++- lib/loaders/load.mjs | 7 ++++++- lib/wrap.js | 22 ++++++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/lib/index.js b/lib/index.js index b4ecea6..7a3a761 100644 --- a/lib/index.js +++ b/lib/index.js @@ -100,7 +100,9 @@ module.exports = function ( const loaderURL = pathToFileURL(resolveMain(localPath(join('loaders', `${loaderName}.mjs`)))); - args.push(`--experimental-loader=${loaderURL.href}`); + if (!semver.satisfies(process.version, '>=21.0.0')) { + args.push(`--experimental-loader=${loaderURL.href}`); + } child = fork(script, scriptArgs, { cwd: process.cwd(), diff --git a/lib/loaders/load.mjs b/lib/loaders/load.mjs index c27584e..b592a88 100644 --- a/lib/loaders/load.mjs +++ b/lib/loaders/load.mjs @@ -24,7 +24,7 @@ export async function load(url, context, defaultLoad) { } } -export const initialize = async (context) => { +export const globalPreload = (context) => { // Store port port = context.port; @@ -35,3 +35,8 @@ port.on('message', (m) => { }).unref(); `; }; + +export const initialize = (context) => { + // Store port + port = context.port; +}; diff --git a/lib/wrap.js b/lib/wrap.js index 7889ea5..aa0e868 100755 --- a/lib/wrap.js +++ b/lib/wrap.js @@ -2,6 +2,10 @@ const { dirname, extname } = require('path'); const childProcess = require('child_process'); const { sync: resolve } = require('resolve'); const { isMainThread } = require('worker_threads'); +const { register } = require('node:module'); +const { pathToFileURL } = require('node:url'); +const semver = require('semver'); +const { MessageChannel } = require('node:worker_threads'); const { getConfig } = require('./cfg'); const hook = require('./hook'); @@ -73,3 +77,21 @@ if (typeof mod === 'object' && mod.name) { } else if (typeof mod === 'string') { require(resolve(mod, { basedir })); } + +if (semver.satisfies(process.version, '>=21.0.0')) { + // This example showcases how a message channel can be used to + // communicate with the hooks, by sending `port2` to the hooks. + const { port1, port2 } = new MessageChannel(); + + port1 + .on('message', m => { + if (process.connected) process.send(m); + }) + .unref(); + + register('./loaders/load.mjs', { + parentURL: pathToFileURL(__filename), + data: { port: port2 }, + transferList: [port2] + }); +}