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..2e4dde2 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 @@ -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 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/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/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..b592a88 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,20 @@ 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); +}).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] + }); +} 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/)) {