From 92c1cbca82acfd8b781a7dbf8bc9ccb5f32cf46f Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Tue, 13 Jan 2026 16:34:00 +0100 Subject: [PATCH 1/2] test: split test-esm-loader-hooks Previously whenever one of the test case fails in the CI, it barely logged anything useful in the CI and it was difficult to nail down the specific failing case with a local reproduction, especially when the test fixutre is inline JavaScript. This patch: - Puts all the inline JavaScript in on-disk fixtures so that they can be re-run easily. - Split the tests into individual files so that it's easier to nail down the failure - Use spawnSyncAndAssert which logs useful information when the child process does not behave as expected. - Rename the tests as module-hooks/test-async-loader-hooks-* because they belong to the module hooks test suite and are not esm-specific. --- test/es-module/test-esm-loader-hooks.mjs | 860 ------------------ .../loader-delayed-async-load.mjs | 3 + .../es-module-loaders/loader-exit-on-load.mjs | 4 + .../loader-exit-on-resolve.mjs | 4 + .../loader-exit-top-level.mjs | 1 + .../loader-globalpreload-and-initialize.mjs | 2 + .../loader-globalpreload-only.mjs | 1 + .../loader-globalpreload-with-return.mjs | 3 + .../loader-initialize-exit.mjs | 3 + .../loader-initialize-never-settling.mjs | 3 + .../loader-initialize-rejecting.mjs | 3 + .../loader-initialize-throw-null.mjs | 3 + .../loader-load-commonjs-with-source.mjs | 13 + .../loader-load-source-maps.mjs | 9 + .../es-module-loaders/loader-mixed-opt-in.mjs | 28 + .../es-module-loaders/loader-throw-bigint.mjs | 1 + .../loader-throw-boolean.mjs | 1 + .../loader-throw-empty-object.mjs | 1 + .../es-module-loaders/loader-throw-error.mjs | 1 + .../loader-throw-function.mjs | 1 + .../es-module-loaders/loader-throw-null.mjs | 1 + .../es-module-loaders/loader-throw-number.mjs | 1 + .../es-module-loaders/loader-throw-object.mjs | 1 + .../es-module-loaders/loader-throw-string.mjs | 1 + .../es-module-loaders/loader-throw-symbol.mjs | 1 + .../loader-throw-undefined.mjs | 1 + .../es-module-loaders/loader-worker-spawn.mjs | 7 + .../register-hooks-with-register.mjs | 6 + .../register-loader-with-url-parenturl.mjs | 11 + .../register-loader-in-sequence.mjs | 11 + ...ister-loader-initialize-never-settling.mjs | 8 + .../module-hooks/register-loader-with-cjs.cjs | 14 + .../register-loader-with-ports.mjs | 22 + ...loader-hooks-called-with-expected-args.mjs | 28 + ...sync-loader-hooks-called-with-register.mjs | 31 + ...obalpreload-no-warning-with-initialize.mjs | 21 + ...ync-loader-hooks-globalpreload-warning.mjs | 23 + ...nc-loader-hooks-initialize-in-sequence.mjs | 21 + ...t-async-loader-hooks-initialize-invoke.mjs | 22 + ...loader-hooks-initialize-never-settling.mjs | 18 + ...c-loader-hooks-initialize-process-exit.mjs | 23 + ...sync-loader-hooks-initialize-rejecting.mjs | 24 + ...ync-loader-hooks-initialize-throw-null.mjs | 24 + .../test-async-loader-hooks-mixed-opt-in.mjs | 20 + ...oks-never-settling-import-meta-resolve.mjs | 20 + ...c-loader-hooks-never-settling-load-cjs.mjs | 20 + ...oks-never-settling-load-esm-no-warning.mjs | 22 + ...s-never-settling-load-esm-with-warning.mjs | 21 + ...c-loader-hooks-never-settling-race-cjs.mjs | 20 + ...c-loader-hooks-never-settling-race-esm.mjs | 20 + ...oader-hooks-never-settling-resolve-cjs.mjs | 20 + ...-never-settling-resolve-esm-no-warning.mjs | 22 + ...ever-settling-resolve-esm-with-warning.mjs | 21 + ...t-async-loader-hooks-no-leak-internals.mjs | 20 + ...-async-loader-hooks-process-exit-async.mjs | 24 + ...t-async-loader-hooks-process-exit-sync.mjs | 24 + ...nc-loader-hooks-process-exit-top-level.mjs | 22 + ...t-async-loader-hooks-register-with-cjs.mjs | 20 + ...sync-loader-hooks-register-with-import.mjs | 22 + ...async-loader-hooks-register-with-ports.mjs | 18 + ...ync-loader-hooks-register-with-require.mjs | 22 + ...ader-hooks-register-with-url-parenturl.mjs | 24 + ...oader-hooks-remove-beforeexit-listener.mjs | 22 + ...c-loader-hooks-require-resolve-default.mjs | 20 + ...nc-loader-hooks-require-resolve-opt-in.mjs | 22 + ...est-async-loader-hooks-source-maps-cjs.mjs | 23 + .../test-async-loader-hooks-throw-bigint.mjs | 22 + .../test-async-loader-hooks-throw-boolean.mjs | 22 + ...-async-loader-hooks-throw-empty-object.mjs | 22 + .../test-async-loader-hooks-throw-error.mjs | 22 + ...test-async-loader-hooks-throw-function.mjs | 22 + .../test-async-loader-hooks-throw-null.mjs | 22 + .../test-async-loader-hooks-throw-number.mjs | 22 + .../test-async-loader-hooks-throw-object.mjs | 22 + .../test-async-loader-hooks-throw-string.mjs | 22 + .../test-async-loader-hooks-throw-symbol.mjs | 20 + ...est-async-loader-hooks-throw-undefined.mjs | 22 + ...ync-loader-hooks-use-hooks-require-esm.mjs | 20 + ...r-hooks-with-worker-permission-allowed.mjs | 24 + ...ooks-with-worker-permission-restricted.mjs | 25 + ...loader-hooks-without-worker-permission.mjs | 25 + 81 files changed, 1228 insertions(+), 860 deletions(-) delete mode 100644 test/es-module/test-esm-loader-hooks.mjs create mode 100644 test/fixtures/es-module-loaders/loader-delayed-async-load.mjs create mode 100644 test/fixtures/es-module-loaders/loader-exit-on-load.mjs create mode 100644 test/fixtures/es-module-loaders/loader-exit-on-resolve.mjs create mode 100644 test/fixtures/es-module-loaders/loader-exit-top-level.mjs create mode 100644 test/fixtures/es-module-loaders/loader-globalpreload-and-initialize.mjs create mode 100644 test/fixtures/es-module-loaders/loader-globalpreload-only.mjs create mode 100644 test/fixtures/es-module-loaders/loader-globalpreload-with-return.mjs create mode 100644 test/fixtures/es-module-loaders/loader-initialize-exit.mjs create mode 100644 test/fixtures/es-module-loaders/loader-initialize-never-settling.mjs create mode 100644 test/fixtures/es-module-loaders/loader-initialize-rejecting.mjs create mode 100644 test/fixtures/es-module-loaders/loader-initialize-throw-null.mjs create mode 100644 test/fixtures/es-module-loaders/loader-load-commonjs-with-source.mjs create mode 100644 test/fixtures/es-module-loaders/loader-load-source-maps.mjs create mode 100644 test/fixtures/es-module-loaders/loader-mixed-opt-in.mjs create mode 100644 test/fixtures/es-module-loaders/loader-throw-bigint.mjs create mode 100644 test/fixtures/es-module-loaders/loader-throw-boolean.mjs create mode 100644 test/fixtures/es-module-loaders/loader-throw-empty-object.mjs create mode 100644 test/fixtures/es-module-loaders/loader-throw-error.mjs create mode 100644 test/fixtures/es-module-loaders/loader-throw-function.mjs create mode 100644 test/fixtures/es-module-loaders/loader-throw-null.mjs create mode 100644 test/fixtures/es-module-loaders/loader-throw-number.mjs create mode 100644 test/fixtures/es-module-loaders/loader-throw-object.mjs create mode 100644 test/fixtures/es-module-loaders/loader-throw-string.mjs create mode 100644 test/fixtures/es-module-loaders/loader-throw-symbol.mjs create mode 100644 test/fixtures/es-module-loaders/loader-throw-undefined.mjs create mode 100644 test/fixtures/es-module-loaders/loader-worker-spawn.mjs create mode 100644 test/fixtures/es-module-loaders/register-hooks-with-register.mjs create mode 100644 test/fixtures/es-module-loaders/register-loader-with-url-parenturl.mjs create mode 100644 test/fixtures/module-hooks/register-loader-in-sequence.mjs create mode 100644 test/fixtures/module-hooks/register-loader-initialize-never-settling.mjs create mode 100644 test/fixtures/module-hooks/register-loader-with-cjs.cjs create mode 100644 test/fixtures/module-hooks/register-loader-with-ports.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-called-with-expected-args.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-called-with-register.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-globalpreload-no-warning-with-initialize.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-globalpreload-warning.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-initialize-in-sequence.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-initialize-invoke.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-initialize-never-settling.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-initialize-process-exit.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-initialize-rejecting.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-initialize-throw-null.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-mixed-opt-in.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-never-settling-import-meta-resolve.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-never-settling-load-cjs.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-never-settling-load-esm-no-warning.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-never-settling-load-esm-with-warning.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-never-settling-race-cjs.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-never-settling-race-esm.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-never-settling-resolve-cjs.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-never-settling-resolve-esm-no-warning.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-never-settling-resolve-esm-with-warning.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-no-leak-internals.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-process-exit-async.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-process-exit-sync.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-process-exit-top-level.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-register-with-cjs.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-register-with-import.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-register-with-ports.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-register-with-require.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-register-with-url-parenturl.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-remove-beforeexit-listener.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-require-resolve-default.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-require-resolve-opt-in.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-source-maps-cjs.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-throw-bigint.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-throw-boolean.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-throw-empty-object.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-throw-error.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-throw-function.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-throw-null.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-throw-number.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-throw-object.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-throw-string.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-throw-symbol.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-throw-undefined.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-use-hooks-require-esm.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-with-worker-permission-allowed.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-with-worker-permission-restricted.mjs create mode 100644 test/module-hooks/test-async-loader-hooks-without-worker-permission.mjs diff --git a/test/es-module/test-esm-loader-hooks.mjs b/test/es-module/test-esm-loader-hooks.mjs deleted file mode 100644 index 3ff5954c06ef54..00000000000000 --- a/test/es-module/test-esm-loader-hooks.mjs +++ /dev/null @@ -1,860 +0,0 @@ -import { spawnPromisified } from '../common/index.mjs'; -import * as fixtures from '../common/fixtures.mjs'; -import assert from 'node:assert'; -import { execPath } from 'node:process'; -import { describe, it } from 'node:test'; - -describe('Loader hooks', { concurrency: !process.env.TEST_PARALLEL }, () => { - it('are called with all expected arguments', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - fixtures.fileURL('es-module-loaders/hooks-input.mjs'), - fixtures.path('es-modules/json-modules.mjs'), - ]); - - assert.strictEqual(stderr, ''); - assert.strictEqual(code, 0); - assert.strictEqual(signal, null); - - const lines = stdout.split('\n'); - assert.match(lines[0], /{"url":"file:\/\/\/.*\/json-modules\.mjs","format":"test","shortCircuit":true}/); - assert.match(lines[1], /{"source":{"type":"Buffer","data":\[.*\]},"format":"module","shortCircuit":true}/); - assert.match(lines[2], /{"url":"file:\/\/\/.*\/experimental\.json","format":"test","shortCircuit":true}/); - assert.match(lines[3], /{"source":{"type":"Buffer","data":\[.*\]},"format":"json","shortCircuit":true}/); - assert.strictEqual(lines[4], ''); - assert.strictEqual(lines.length, 5); - }); - - it('are called with all expected arguments using register function', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader=data:text/javascript,', - '--input-type=module', - '--eval', - "import { register } from 'node:module';" + - `register(${JSON.stringify(fixtures.fileURL('es-module-loaders/hooks-input.mjs'))});` + - `await import(${JSON.stringify(fixtures.fileURL('es-modules/json-modules.mjs'))});`, - ]); - - assert.strictEqual(stderr, ''); - assert.strictEqual(code, 0); - assert.strictEqual(signal, null); - - const lines = stdout.split('\n'); - assert.match(lines[0], /{"url":"file:\/\/\/.*\/json-modules\.mjs","format":"test","shortCircuit":true}/); - assert.match(lines[1], /{"source":{"type":"Buffer","data":\[.*\]},"format":"module","shortCircuit":true}/); - assert.match(lines[2], /{"url":"file:\/\/\/.*\/experimental\.json","format":"test","shortCircuit":true}/); - assert.match(lines[3], /{"source":{"type":"Buffer","data":\[.*\]},"format":"json","shortCircuit":true}/); - assert.strictEqual(lines[4], ''); - assert.strictEqual(lines.length, 5); - }); - - describe('should handle never-settling hooks in ESM files', { concurrency: !process.env.TEST_PARALLEL }, () => { - it('top-level await of a never-settling resolve without warning', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - fixtures.fileURL('es-module-loaders/never-settling-resolve-step/loader.mjs'), - fixtures.path('es-module-loaders/never-settling-resolve-step/never-resolve.mjs'), - ]); - - assert.strictEqual(stderr, ''); - assert.match(stdout, /^should be output\r?\n$/); - assert.strictEqual(code, 13); - assert.strictEqual(signal, null); - }); - - it('top-level await of a never-settling resolve with warning', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--experimental-loader', - fixtures.fileURL('es-module-loaders/never-settling-resolve-step/loader.mjs'), - fixtures.path('es-module-loaders/never-settling-resolve-step/never-resolve.mjs'), - ]); - - assert.match(stderr, /Warning: Detected unsettled top-level await at.+never-resolve\.mjs:5/); - assert.match(stdout, /^should be output\r?\n$/); - assert.strictEqual(code, 13); - assert.strictEqual(signal, null); - }); - - it('top-level await of a never-settling load without warning', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - fixtures.fileURL('es-module-loaders/never-settling-resolve-step/loader.mjs'), - fixtures.path('es-module-loaders/never-settling-resolve-step/never-load.mjs'), - ]); - - assert.strictEqual(stderr, ''); - assert.match(stdout, /^should be output\r?\n$/); - assert.strictEqual(code, 13); - assert.strictEqual(signal, null); - }); - - it('top-level await of a never-settling load with warning', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--experimental-loader', - fixtures.fileURL('es-module-loaders/never-settling-resolve-step/loader.mjs'), - fixtures.path('es-module-loaders/never-settling-resolve-step/never-load.mjs'), - ]); - - assert.match(stderr, /Warning: Detected unsettled top-level await at.+never-load\.mjs:5/); - assert.match(stdout, /^should be output\r?\n$/); - assert.strictEqual(code, 13); - assert.strictEqual(signal, null); - }); - - it('top-level await of a race of never-settling hooks', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - fixtures.fileURL('es-module-loaders/never-settling-resolve-step/loader.mjs'), - fixtures.path('es-module-loaders/never-settling-resolve-step/race.mjs'), - ]); - - assert.strictEqual(stderr, ''); - assert.match(stdout, /^true\r?\n$/); - assert.strictEqual(code, 0); - assert.strictEqual(signal, null); - }); - - it('import.meta.resolve of a never-settling resolve should throw', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - fixtures.fileURL('es-module-loaders/never-settling-resolve-step/loader.mjs'), - fixtures.path('es-module-loaders/never-settling-resolve-step/import.meta.never-resolve.mjs'), - ]); - - assert.strictEqual(stderr, ''); - assert.match(stdout, /^should be output\r?\n$/); - assert.strictEqual(code, 0); - assert.strictEqual(signal, null); - }); - }); - - describe('should handle never-settling hooks in CJS files', { concurrency: !process.env.TEST_PARALLEL }, () => { - it('never-settling resolve', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - fixtures.fileURL('es-module-loaders/never-settling-resolve-step/loader.mjs'), - fixtures.path('es-module-loaders/never-settling-resolve-step/never-resolve.cjs'), - ]); - - assert.strictEqual(stderr, ''); - assert.match(stdout, /^should be output\r?\n$/); - assert.strictEqual(code, 0); - assert.strictEqual(signal, null); - }); - - - it('never-settling load', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - fixtures.fileURL('es-module-loaders/never-settling-resolve-step/loader.mjs'), - fixtures.path('es-module-loaders/never-settling-resolve-step/never-load.cjs'), - ]); - - assert.strictEqual(stderr, ''); - assert.match(stdout, /^should be output\r?\n$/); - assert.strictEqual(code, 0); - assert.strictEqual(signal, null); - }); - - it('race of never-settling hooks', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - fixtures.fileURL('es-module-loaders/never-settling-resolve-step/loader.mjs'), - fixtures.path('es-module-loaders/never-settling-resolve-step/race.cjs'), - ]); - - assert.strictEqual(stderr, ''); - assert.match(stdout, /^true\r?\n$/); - assert.strictEqual(code, 0); - assert.strictEqual(signal, null); - }); - }); - - it('should not work without worker permission', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--permission', - '--allow-fs-read', - '*', - '--experimental-loader', - fixtures.fileURL('empty.js'), - fixtures.path('es-modules/esm-top-level-await.mjs'), - ]); - - assert.match(stderr, /Error: Access to this API has been restricted/); - assert.strictEqual(stdout, ''); - assert.strictEqual(code, 1); - assert.strictEqual(signal, null); - }); - - it('should allow loader hooks to spawn workers when allowed by the CLI flags', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--permission', - '--allow-worker', - '--allow-fs-read', - '*', - '--experimental-loader', - `data:text/javascript,import{Worker}from"worker_threads";new Worker(${encodeURIComponent(JSON.stringify(fixtures.path('empty.js')))}).unref()`, - fixtures.path('es-modules/esm-top-level-await.mjs'), - ]); - - assert.strictEqual(stderr, ''); - assert.match(stdout, /^1\r?\n2\r?\n$/); - assert.strictEqual(code, 0); - assert.strictEqual(signal, null); - }); - - it('should not allow loader hooks to spawn workers if restricted by the CLI flags', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--permission', - '--allow-fs-read', - '*', - '--experimental-loader', - `data:text/javascript,import{Worker}from"worker_threads";new Worker(${encodeURIComponent(JSON.stringify(fixtures.path('empty.js')))}).unref()`, - fixtures.path('es-modules/esm-top-level-await.mjs'), - ]); - - assert.match(stderr, /code: 'ERR_ACCESS_DENIED'/); - assert.strictEqual(stdout, ''); - assert.strictEqual(code, 1); - assert.strictEqual(signal, null); - }); - - it('should not leak internals or expose import.meta.resolve', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - fixtures.fileURL('es-module-loaders/loader-edge-cases.mjs'), - fixtures.path('empty.js'), - ]); - - assert.strictEqual(stderr, ''); - assert.strictEqual(stdout, ''); - assert.strictEqual(code, 0); - assert.strictEqual(signal, null); - }); - - it('should be fine to call `process.exit` from a custom async hook', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - 'data:text/javascript,export function load(a,b,next){if(a==="data:exit")process.exit(42);return next(a,b)}', - '--input-type=module', - '--eval', - 'import "data:exit"', - ]); - - assert.strictEqual(stderr, ''); - assert.strictEqual(stdout, ''); - assert.strictEqual(code, 42); - assert.strictEqual(signal, null); - }); - - it('should be fine to call `process.exit` from a custom sync hook', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - 'data:text/javascript,export function resolve(a,b,next){if(a==="exit:")process.exit(42);return next(a,b)}', - '--input-type=module', - '--eval', - 'import "data:text/javascript,import.meta.resolve(%22exit:%22)"', - ]); - - assert.strictEqual(stderr, ''); - assert.strictEqual(stdout, ''); - assert.strictEqual(code, 42); - assert.strictEqual(signal, null); - }); - - it('should be fine to call `process.exit` from the loader thread top-level', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - 'data:text/javascript,process.exit(42)', - fixtures.path('empty.js'), - ]); - - assert.strictEqual(stderr, ''); - assert.strictEqual(stdout, ''); - assert.strictEqual(code, 42); - assert.strictEqual(signal, null); - }); - - describe('should handle a throwing top-level body', () => { - it('should handle regular Error object', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - 'data:text/javascript,throw new Error("error message")', - fixtures.path('empty.js'), - ]); - - assert.match(stderr, /Error: error message\r?\n/); - assert.strictEqual(stdout, ''); - assert.strictEqual(code, 1); - assert.strictEqual(signal, null); - }); - - it('should handle null', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - 'data:text/javascript,throw null', - fixtures.path('empty.js'), - ]); - - assert.match(stderr, /\nnull\r?\n/); - assert.strictEqual(stdout, ''); - assert.strictEqual(code, 1); - assert.strictEqual(signal, null); - }); - - it('should handle undefined', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - 'data:text/javascript,throw undefined', - fixtures.path('empty.js'), - ]); - - assert.match(stderr, /\nundefined\r?\n/); - assert.strictEqual(stdout, ''); - assert.strictEqual(code, 1); - assert.strictEqual(signal, null); - }); - - it('should handle boolean', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - 'data:text/javascript,throw true', - fixtures.path('empty.js'), - ]); - - assert.match(stderr, /\ntrue\r?\n/); - assert.strictEqual(stdout, ''); - assert.strictEqual(code, 1); - assert.strictEqual(signal, null); - }); - - it('should handle empty plain object', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - 'data:text/javascript,throw {}', - fixtures.path('empty.js'), - ]); - - assert.match(stderr, /\n\{\}\r?\n/); - assert.strictEqual(stdout, ''); - assert.strictEqual(code, 1); - assert.strictEqual(signal, null); - }); - - it('should handle plain object', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - 'data:text/javascript,throw {fn(){},symbol:Symbol("symbol"),u:undefined}', - fixtures.path('empty.js'), - ]); - - assert.match(stderr, /\n\{ fn: \[Function: fn\], symbol: Symbol\(symbol\), u: undefined \}\r?\n/); - assert.strictEqual(stdout, ''); - assert.strictEqual(code, 1); - assert.strictEqual(signal, null); - }); - - it('should handle number', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - 'data:text/javascript,throw 1', - fixtures.path('empty.js'), - ]); - - assert.match(stderr, /\n1\r?\n/); - assert.strictEqual(stdout, ''); - assert.strictEqual(code, 1); - assert.strictEqual(signal, null); - }); - - it('should handle bigint', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - 'data:text/javascript,throw 1n', - fixtures.path('empty.js'), - ]); - - assert.match(stderr, /\n1\r?\n/); - assert.strictEqual(stdout, ''); - assert.strictEqual(code, 1); - assert.strictEqual(signal, null); - }); - - it('should handle string', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - 'data:text/javascript,throw "literal string"', - fixtures.path('empty.js'), - ]); - - assert.match(stderr, /\nliteral string\r?\n/); - assert.strictEqual(stdout, ''); - assert.strictEqual(code, 1); - assert.strictEqual(signal, null); - }); - - it('should handle symbol', async () => { - const { code, signal, stdout } = await spawnPromisified(execPath, [ - '--experimental-loader', - 'data:text/javascript,throw Symbol("symbol descriptor")', - fixtures.path('empty.js'), - ]); - - // Throwing a symbol doesn't produce any output - assert.strictEqual(stdout, ''); - assert.strictEqual(code, 1); - assert.strictEqual(signal, null); - }); - - it('should handle function', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - 'data:text/javascript,throw function fnName(){}', - fixtures.path('empty.js'), - ]); - - assert.match(stderr, /\n\[Function: fnName\]\r?\n/); - assert.strictEqual(stdout, ''); - assert.strictEqual(code, 1); - assert.strictEqual(signal, null); - }); - }); - - describe('globalPreload', () => { - it('should emit warning', async () => { - const { stderr } = await spawnPromisified(execPath, [ - '--experimental-loader', - 'data:text/javascript,export function globalPreload(){}', - '--experimental-loader', - 'data:text/javascript,export function globalPreload(){return""}', - fixtures.path('empty.js'), - ]); - - assert.strictEqual(stderr.match(/`globalPreload` has been removed; use `initialize` instead/g).length, 1); - }); - - it('should not emit warning when initialize is supplied', async () => { - const { stderr } = await spawnPromisified(execPath, [ - '--experimental-loader', - 'data:text/javascript,export function globalPreload(){}export function initialize(){}', - fixtures.path('empty.js'), - ]); - - assert.doesNotMatch(stderr, /`globalPreload` has been removed; use `initialize` instead/); - }); - }); - - it('should be fine to call `process.removeAllListeners("beforeExit")` from the main thread', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - 'data:text/javascript,export function load(a,b,c){return new Promise(d=>setTimeout(()=>d(c(a,b)),99))}', - '--input-type=module', - '--eval', - 'setInterval(() => process.removeAllListeners("beforeExit"),1).unref();await import("data:text/javascript,")', - ]); - - assert.strictEqual(stderr, ''); - assert.strictEqual(stdout, ''); - assert.strictEqual(code, 0); - assert.strictEqual(signal, null); - }); - - describe('`initialize`/`register`', () => { - it('should invoke `initialize` correctly', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - fixtures.fileURL('es-module-loaders/hooks-initialize.mjs'), - '--input-type=module', - '--eval', - 'import os from "node:os";', - ]); - - assert.strictEqual(stderr, ''); - assert.deepStrictEqual(stdout.split('\n'), ['hooks initialize 1', '']); - assert.strictEqual(code, 0); - assert.strictEqual(signal, null); - }); - - it('should allow communicating with loader via `register` ports', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--input-type=module', - '--eval', - ` - import {MessageChannel} from 'node:worker_threads'; - import {register} from 'node:module'; - import {once} from 'node:events'; - const {port1, port2} = new MessageChannel(); - port1.on('message', (msg) => { - console.log('message', msg); - }); - const result = register( - ${JSON.stringify(fixtures.fileURL('es-module-loaders/hooks-initialize-port.mjs'))}, - {data: port2, transferList: [port2]}, - ); - console.log('register', result); - - const timeout = setTimeout(() => {}, 2**31 - 1); // to keep the process alive. - await Promise.all([ - once(port1, 'message').then(() => once(port1, 'message')), - import('node:os'), - ]); - clearTimeout(timeout); - port1.close(); - `, - ]); - - assert.strictEqual(stderr, ''); - assert.deepStrictEqual(stdout.split('\n'), [ 'register undefined', - 'message initialize', - 'message resolve node:os', - '' ]); - - assert.strictEqual(code, 0); - assert.strictEqual(signal, null); - }); - - it('should have `register` accept URL objects as `parentURL`', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--import', - `data:text/javascript,${encodeURIComponent( - 'import{ register } from "node:module";' + - 'import { pathToFileURL } from "node:url";' + - 'register("./hooks-initialize.mjs", pathToFileURL("./"));' - )}`, - '--input-type=module', - '--eval', - ` - import {register} from 'node:module'; - register( - ${JSON.stringify(fixtures.fileURL('es-module-loaders/loader-load-foo-or-42.mjs'))}, - new URL('data:'), - ); - - import('node:os').then((result) => { - console.log(JSON.stringify(result)); - }); - `, - ], { cwd: fixtures.fileURL('es-module-loaders/') }); - - assert.strictEqual(stderr, ''); - assert.deepStrictEqual(stdout.split('\n').sort(), ['hooks initialize 1', '{"default":"foo"}', ''].sort()); - - assert.strictEqual(code, 0); - assert.strictEqual(signal, null); - }); - - it('should have `register` work with cjs', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--input-type=commonjs', - '--eval', - ` - 'use strict'; - const {register} = require('node:module'); - register( - ${JSON.stringify(fixtures.fileURL('es-module-loaders/hooks-initialize.mjs'))}, - ); - register( - ${JSON.stringify(fixtures.fileURL('es-module-loaders/loader-load-foo-or-42.mjs'))}, - ); - - import('node:os').then((result) => { - console.log(JSON.stringify(result)); - }); - `, - ]); - - assert.strictEqual(stderr, ''); - assert.deepStrictEqual(stdout.split('\n').sort(), ['hooks initialize 1', '{"default":"foo"}', ''].sort()); - - assert.strictEqual(code, 0); - assert.strictEqual(signal, null); - }); - - it('`register` should work with `require`', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--require', - fixtures.path('es-module-loaders/register-loader.cjs'), - '--input-type=module', - '--eval', - 'import "node:os";', - ]); - - assert.strictEqual(stderr, ''); - assert.deepStrictEqual(stdout.split('\n'), ['resolve passthru', 'resolve passthru', '']); - assert.strictEqual(code, 0); - assert.strictEqual(signal, null); - }); - - it('`register` should work with `import`', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--import', - fixtures.fileURL('es-module-loaders/register-loader.mjs'), - '--input-type=module', - '--eval', - 'import "node:os"', - ]); - - assert.strictEqual(stderr, ''); - assert.deepStrictEqual(stdout.split('\n'), ['resolve passthru', '']); - assert.strictEqual(code, 0); - assert.strictEqual(signal, null); - }); - - it('should execute `initialize` in sequence', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--input-type=module', - '--eval', - ` - import {register} from 'node:module'; - console.log('result 1', register( - ${JSON.stringify(fixtures.fileURL('es-module-loaders/hooks-initialize.mjs'))} - )); - console.log('result 2', register( - ${JSON.stringify(fixtures.fileURL('es-module-loaders/hooks-initialize.mjs'))} - )); - - await import('node:os'); - `, - ]); - - assert.strictEqual(stderr, ''); - assert.deepStrictEqual(stdout.split('\n'), [ 'hooks initialize 1', - 'result 1 undefined', - 'hooks initialize 2', - 'result 2 undefined', - '' ]); - assert.strictEqual(code, 0); - assert.strictEqual(signal, null); - }); - - it('should handle `initialize` returning never-settling promise', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--input-type=module', - '--eval', - ` - import {register} from 'node:module'; - try { - register('data:text/javascript,export function initialize(){return new Promise(()=>{})}'); - } catch (e) { - console.log('caught', e.code); - } - `, - ]); - - assert.strictEqual(stderr, ''); - assert.strictEqual(stdout.trim(), 'caught ERR_ASYNC_LOADER_REQUEST_NEVER_SETTLED'); - assert.strictEqual(code, 0); - assert.strictEqual(signal, null); - }); - - it('should handle `initialize` returning rejecting promise', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--input-type=module', - '--eval', - ` - import {register} from 'node:module'; - register('data:text/javascript,export function initialize(){return Promise.reject()}'); - `, - ]); - - assert.match(stderr, /undefined\r?\n/); - assert.strictEqual(stdout, ''); - assert.strictEqual(code, 1); - assert.strictEqual(signal, null); - }); - - it('should handle `initialize` throwing null', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--input-type=module', - '--eval', - ` - import {register} from 'node:module'; - register('data:text/javascript,export function initialize(){throw null}'); - `, - ]); - - assert.match(stderr, /null\r?\n/); - assert.strictEqual(stdout, ''); - assert.strictEqual(code, 1); - assert.strictEqual(signal, null); - }); - - it('should be fine to call `process.exit` from a initialize hook', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--input-type=module', - '--eval', - ` - import {register} from 'node:module'; - register('data:text/javascript,export function initialize(){process.exit(42);}'); - `, - ]); - - assert.strictEqual(stderr, ''); - assert.strictEqual(stdout, ''); - assert.strictEqual(code, 42); - assert.strictEqual(signal, null); - }); - }); - - it('should use CJS loader to respond to require.resolve calls by default', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - fixtures.fileURL('es-module-loaders/loader-resolve-passthru.mjs'), - fixtures.path('require-resolve.js'), - ]); - - assert.strictEqual(stderr, ''); - assert.strictEqual(stdout, 'resolve passthru\n'); - assert.strictEqual(code, 0); - assert.strictEqual(signal, null); - }); - - it('should use ESM loader to respond to require.resolve calls when opting in', async () => { - const readFile = async () => {}; - const fileURLToPath = () => {}; - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - `data:text/javascript,import{readFile}from"node:fs/promises";import{fileURLToPath}from"node:url";export ${ - async function load(url, context, nextLoad) { - const result = await nextLoad(url, context); - if (url.endsWith('/common/index.js')) { - result.source = '"use strict";module.exports=require("node:module").createRequire(' + - `${JSON.stringify(url)})(${JSON.stringify(fileURLToPath(url))});\n`; - } else if (url.startsWith('file:') && (context.format == null || context.format === 'commonjs')) { - result.source = await readFile(new URL(url)); - } - return result; - }}`, - '--experimental-loader', - fixtures.fileURL('es-module-loaders/loader-resolve-passthru.mjs'), - fixtures.path('require-resolve.js'), - ]); - - assert.strictEqual(stderr, ''); - assert.strictEqual(stdout, 'resolve passthru\n'.repeat(10)); - assert.strictEqual(code, 0); - assert.strictEqual(signal, null); - }); - - it('should use hooks', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(process.execPath, [ - '--no-experimental-require-module', - '--import', - fixtures.fileURL('es-module-loaders/builtin-named-exports.mjs'), - fixtures.path('es-modules/require-esm-throws-with-loaders.js'), - ]); - - assert.strictEqual(stderr, ''); - assert.strictEqual(stdout, ''); - assert.strictEqual(code, 0); - assert.strictEqual(signal, null); - }); - - it('should support source maps in commonjs translator', async () => { - const readFile = async () => {}; - const hook = ` - import { readFile } from 'node:fs/promises'; - export ${ - async function load(url, context, nextLoad) { - const resolved = await nextLoad(url, context); - if (context.format === 'commonjs') { - resolved.source = await readFile(new URL(url)); - } - return resolved; - } - }`; - - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--enable-source-maps', - '--import', - `data:text/javascript,${encodeURIComponent(` - import{ register } from "node:module"; - register(${ - JSON.stringify('data:text/javascript,' + encodeURIComponent(hook)) - }); - `)}`, - fixtures.path('source-map/throw-on-require.js'), - ]); - - assert.strictEqual(stdout, ''); - assert.match(stderr, /throw-on-require\.ts:9:9/); - assert.strictEqual(code, 1); - assert.strictEqual(signal, null); - }); - - it('should handle mixed of opt-in modules and non-opt-in ones', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - `data:text/javascript,const fixtures=${encodeURI(JSON.stringify(fixtures.path('empty.js')))};export ${ - encodeURIComponent(function resolve(s, c, n) { - if (s.endsWith('entry-point')) { - return { - shortCircuit: true, - url: 'file:///c:/virtual-entry-point', - format: 'commonjs', - }; - } - return n(s, c); - }) - }export ${ - encodeURIComponent(async function load(u, c, n) { - if (u === 'file:///c:/virtual-entry-point') { - return { - shortCircuit: true, - source: `"use strict";require(${JSON.stringify(fixtures)});console.log("Hello");`, - format: 'commonjs', - }; - } - return n(u, c); - })}`, - 'entry-point', - ]); - - assert.strictEqual(stderr, ''); - assert.strictEqual(stdout, 'Hello\n'); - assert.strictEqual(code, 0); - assert.strictEqual(signal, null); - }); -}); diff --git a/test/fixtures/es-module-loaders/loader-delayed-async-load.mjs b/test/fixtures/es-module-loaders/loader-delayed-async-load.mjs new file mode 100644 index 00000000000000..33bae9bdafd9c9 --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-delayed-async-load.mjs @@ -0,0 +1,3 @@ +export function load(a, b, c) { + return new Promise(d => setTimeout(() => d(c(a, b)), 99)); +} diff --git a/test/fixtures/es-module-loaders/loader-exit-on-load.mjs b/test/fixtures/es-module-loaders/loader-exit-on-load.mjs new file mode 100644 index 00000000000000..4ee413fa46ce41 --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-exit-on-load.mjs @@ -0,0 +1,4 @@ +export function load(a, b, next) { + if (a === 'data:exit') process.exit(42); + return next(a, b); +} diff --git a/test/fixtures/es-module-loaders/loader-exit-on-resolve.mjs b/test/fixtures/es-module-loaders/loader-exit-on-resolve.mjs new file mode 100644 index 00000000000000..cffa4bcc486a95 --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-exit-on-resolve.mjs @@ -0,0 +1,4 @@ +export function resolve(a, b, next) { + if (a === 'exit:') process.exit(42); + return next(a, b); +} diff --git a/test/fixtures/es-module-loaders/loader-exit-top-level.mjs b/test/fixtures/es-module-loaders/loader-exit-top-level.mjs new file mode 100644 index 00000000000000..6427ca06037fcd --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-exit-top-level.mjs @@ -0,0 +1 @@ +process.exit(42); diff --git a/test/fixtures/es-module-loaders/loader-globalpreload-and-initialize.mjs b/test/fixtures/es-module-loaders/loader-globalpreload-and-initialize.mjs new file mode 100644 index 00000000000000..7055b90f4e7568 --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-globalpreload-and-initialize.mjs @@ -0,0 +1,2 @@ +export function globalPreload() {} +export function initialize() {} diff --git a/test/fixtures/es-module-loaders/loader-globalpreload-only.mjs b/test/fixtures/es-module-loaders/loader-globalpreload-only.mjs new file mode 100644 index 00000000000000..6b7f59d6319804 --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-globalpreload-only.mjs @@ -0,0 +1 @@ +export function globalPreload() {} diff --git a/test/fixtures/es-module-loaders/loader-globalpreload-with-return.mjs b/test/fixtures/es-module-loaders/loader-globalpreload-with-return.mjs new file mode 100644 index 00000000000000..f54cf72ce5b9f9 --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-globalpreload-with-return.mjs @@ -0,0 +1,3 @@ +export function globalPreload() { + return ''; +} diff --git a/test/fixtures/es-module-loaders/loader-initialize-exit.mjs b/test/fixtures/es-module-loaders/loader-initialize-exit.mjs new file mode 100644 index 00000000000000..53cb4fa26f0164 --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-initialize-exit.mjs @@ -0,0 +1,3 @@ +export function initialize() { + process.exit(42); +} diff --git a/test/fixtures/es-module-loaders/loader-initialize-never-settling.mjs b/test/fixtures/es-module-loaders/loader-initialize-never-settling.mjs new file mode 100644 index 00000000000000..80033b663a5a0d --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-initialize-never-settling.mjs @@ -0,0 +1,3 @@ +export function initialize() { + return new Promise(() => {}); +} diff --git a/test/fixtures/es-module-loaders/loader-initialize-rejecting.mjs b/test/fixtures/es-module-loaders/loader-initialize-rejecting.mjs new file mode 100644 index 00000000000000..899accc15114e9 --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-initialize-rejecting.mjs @@ -0,0 +1,3 @@ +export function initialize() { + return Promise.reject(); +} diff --git a/test/fixtures/es-module-loaders/loader-initialize-throw-null.mjs b/test/fixtures/es-module-loaders/loader-initialize-throw-null.mjs new file mode 100644 index 00000000000000..3d834b5c23cfb3 --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-initialize-throw-null.mjs @@ -0,0 +1,3 @@ +export function initialize() { + throw null; +} diff --git a/test/fixtures/es-module-loaders/loader-load-commonjs-with-source.mjs b/test/fixtures/es-module-loaders/loader-load-commonjs-with-source.mjs new file mode 100644 index 00000000000000..3f431e630cddce --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-load-commonjs-with-source.mjs @@ -0,0 +1,13 @@ +import { readFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; + +export async function load(url, context, nextLoad) { + const result = await nextLoad(url, context); + if (url.endsWith('/common/index.js')) { + result.source = '"use strict";module.exports=require("node:module").createRequire(' + + JSON.stringify(url) + ')(' + JSON.stringify(fileURLToPath(url)) + ');\n'; + } else if (url.startsWith('file:') && (context.format == null || context.format === 'commonjs')) { + result.source = await readFile(new URL(url)); + } + return result; +} diff --git a/test/fixtures/es-module-loaders/loader-load-source-maps.mjs b/test/fixtures/es-module-loaders/loader-load-source-maps.mjs new file mode 100644 index 00000000000000..bf5ef6b91028a4 --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-load-source-maps.mjs @@ -0,0 +1,9 @@ +import { readFile } from 'node:fs/promises'; + +export async function load(url, context, nextLoad) { + const resolved = await nextLoad(url, context); + if (context.format === 'commonjs') { + resolved.source = await readFile(new URL(url)); + } + return resolved; +} diff --git a/test/fixtures/es-module-loaders/loader-mixed-opt-in.mjs b/test/fixtures/es-module-loaders/loader-mixed-opt-in.mjs new file mode 100644 index 00000000000000..69b17a88249bef --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-mixed-opt-in.mjs @@ -0,0 +1,28 @@ +import { createRequire } from 'module'; + +const require = createRequire(import.meta.url); +const fixtures = require('../../common/fixtures.js'); + +const fixturesPath = fixtures.path('empty.js'); + +export function resolve(s, c, n) { + if (s.endsWith('entry-point')) { + return { + shortCircuit: true, + url: 'file:///c:/virtual-entry-point', + format: 'commonjs', + }; + } + return n(s, c); +} + +export async function load(u, c, n) { + if (u === 'file:///c:/virtual-entry-point') { + return { + shortCircuit: true, + source: `"use strict";require(${JSON.stringify(fixturesPath)});console.log("Hello");`, + format: 'commonjs', + }; + } + return n(u, c); +} diff --git a/test/fixtures/es-module-loaders/loader-throw-bigint.mjs b/test/fixtures/es-module-loaders/loader-throw-bigint.mjs new file mode 100644 index 00000000000000..700b0387fa7ffc --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-throw-bigint.mjs @@ -0,0 +1 @@ +throw 1n; diff --git a/test/fixtures/es-module-loaders/loader-throw-boolean.mjs b/test/fixtures/es-module-loaders/loader-throw-boolean.mjs new file mode 100644 index 00000000000000..874fbd92e5fa22 --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-throw-boolean.mjs @@ -0,0 +1 @@ +throw true; diff --git a/test/fixtures/es-module-loaders/loader-throw-empty-object.mjs b/test/fixtures/es-module-loaders/loader-throw-empty-object.mjs new file mode 100644 index 00000000000000..778886fcb90a4d --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-throw-empty-object.mjs @@ -0,0 +1 @@ +throw {}; diff --git a/test/fixtures/es-module-loaders/loader-throw-error.mjs b/test/fixtures/es-module-loaders/loader-throw-error.mjs new file mode 100644 index 00000000000000..816051b6c7831f --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-throw-error.mjs @@ -0,0 +1 @@ +throw new Error('error message'); diff --git a/test/fixtures/es-module-loaders/loader-throw-function.mjs b/test/fixtures/es-module-loaders/loader-throw-function.mjs new file mode 100644 index 00000000000000..35503d670976cd --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-throw-function.mjs @@ -0,0 +1 @@ +throw function fnName() {}; diff --git a/test/fixtures/es-module-loaders/loader-throw-null.mjs b/test/fixtures/es-module-loaders/loader-throw-null.mjs new file mode 100644 index 00000000000000..37d3d14b8bfe1b --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-throw-null.mjs @@ -0,0 +1 @@ +throw null; diff --git a/test/fixtures/es-module-loaders/loader-throw-number.mjs b/test/fixtures/es-module-loaders/loader-throw-number.mjs new file mode 100644 index 00000000000000..6dbc3994c7ac5d --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-throw-number.mjs @@ -0,0 +1 @@ +throw 1; diff --git a/test/fixtures/es-module-loaders/loader-throw-object.mjs b/test/fixtures/es-module-loaders/loader-throw-object.mjs new file mode 100644 index 00000000000000..fa7c9b7d43af4d --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-throw-object.mjs @@ -0,0 +1 @@ +throw { fn() {}, symbol: Symbol('symbol'), u: undefined }; diff --git a/test/fixtures/es-module-loaders/loader-throw-string.mjs b/test/fixtures/es-module-loaders/loader-throw-string.mjs new file mode 100644 index 00000000000000..613a4321f50de6 --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-throw-string.mjs @@ -0,0 +1 @@ +throw 'literal string'; diff --git a/test/fixtures/es-module-loaders/loader-throw-symbol.mjs b/test/fixtures/es-module-loaders/loader-throw-symbol.mjs new file mode 100644 index 00000000000000..05876e0b4ada86 --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-throw-symbol.mjs @@ -0,0 +1 @@ +throw Symbol('symbol descriptor'); diff --git a/test/fixtures/es-module-loaders/loader-throw-undefined.mjs b/test/fixtures/es-module-loaders/loader-throw-undefined.mjs new file mode 100644 index 00000000000000..38ecbdff9801f6 --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-throw-undefined.mjs @@ -0,0 +1 @@ +throw undefined; diff --git a/test/fixtures/es-module-loaders/loader-worker-spawn.mjs b/test/fixtures/es-module-loaders/loader-worker-spawn.mjs new file mode 100644 index 00000000000000..2860d116e6478c --- /dev/null +++ b/test/fixtures/es-module-loaders/loader-worker-spawn.mjs @@ -0,0 +1,7 @@ +import { Worker } from 'worker_threads'; +import { createRequire } from 'module'; + +const require = createRequire(import.meta.url); +const fixtures = require('../../common/fixtures.js'); + +new Worker(fixtures.path('empty.js')).unref(); diff --git a/test/fixtures/es-module-loaders/register-hooks-with-register.mjs b/test/fixtures/es-module-loaders/register-hooks-with-register.mjs new file mode 100644 index 00000000000000..f6ca913e5958f9 --- /dev/null +++ b/test/fixtures/es-module-loaders/register-hooks-with-register.mjs @@ -0,0 +1,6 @@ +import { register } from 'node:module'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +register(join(__dirname, 'hooks-initialize.mjs'), import.meta.url); diff --git a/test/fixtures/es-module-loaders/register-loader-with-url-parenturl.mjs b/test/fixtures/es-module-loaders/register-loader-with-url-parenturl.mjs new file mode 100644 index 00000000000000..1d82d2abc5576d --- /dev/null +++ b/test/fixtures/es-module-loaders/register-loader-with-url-parenturl.mjs @@ -0,0 +1,11 @@ +import {register} from 'node:module'; +import fixtures from '../../common/fixtures.js'; + +register( + fixtures.fileURL('es-module-loaders/loader-load-foo-or-42.mjs'), + new URL('data:'), +); + +import('node:os').then((result) => { + console.log(JSON.stringify(result)); +}); diff --git a/test/fixtures/module-hooks/register-loader-in-sequence.mjs b/test/fixtures/module-hooks/register-loader-in-sequence.mjs new file mode 100644 index 00000000000000..c129fcfc18092d --- /dev/null +++ b/test/fixtures/module-hooks/register-loader-in-sequence.mjs @@ -0,0 +1,11 @@ +import {register} from 'node:module'; +import fixtures from '../../common/fixtures.js'; + +console.log('result 1', register( + fixtures.fileURL('es-module-loaders/hooks-initialize.mjs') +)); +console.log('result 2', register( + fixtures.fileURL('es-module-loaders/hooks-initialize.mjs') +)); + +await import('node:os'); diff --git a/test/fixtures/module-hooks/register-loader-initialize-never-settling.mjs b/test/fixtures/module-hooks/register-loader-initialize-never-settling.mjs new file mode 100644 index 00000000000000..13091effa970bf --- /dev/null +++ b/test/fixtures/module-hooks/register-loader-initialize-never-settling.mjs @@ -0,0 +1,8 @@ +import {register} from 'node:module'; +import fixtures from '../../common/fixtures.js'; + +try { + register(fixtures.fileURL('es-module-loaders/loader-initialize-never-settling.mjs')); +} catch (e) { + console.log('caught', e.code); +} diff --git a/test/fixtures/module-hooks/register-loader-with-cjs.cjs b/test/fixtures/module-hooks/register-loader-with-cjs.cjs new file mode 100644 index 00000000000000..2cd6c666bd147d --- /dev/null +++ b/test/fixtures/module-hooks/register-loader-with-cjs.cjs @@ -0,0 +1,14 @@ +'use strict'; +const {register} = require('node:module'); +const fixtures = require('../../common/fixtures.js'); + +register( + fixtures.fileURL('es-module-loaders/hooks-initialize.mjs'), +); +register( + fixtures.fileURL('es-module-loaders/loader-load-foo-or-42.mjs'), +); + +import('node:os').then((result) => { + console.log(JSON.stringify(result)); +}); diff --git a/test/fixtures/module-hooks/register-loader-with-ports.mjs b/test/fixtures/module-hooks/register-loader-with-ports.mjs new file mode 100644 index 00000000000000..45e125f96a208f --- /dev/null +++ b/test/fixtures/module-hooks/register-loader-with-ports.mjs @@ -0,0 +1,22 @@ +import {MessageChannel} from 'node:worker_threads'; +import {register} from 'node:module'; +import {once} from 'node:events'; +import fixtures from '../../common/fixtures.js'; + +const {port1, port2} = new MessageChannel(); +port1.on('message', (msg) => { + console.log('message', msg); +}); +const result = register( + fixtures.fileURL('es-module-loaders/hooks-initialize-port.mjs'), + {data: port2, transferList: [port2]}, +); +console.log('register', result); + +const timeout = setTimeout(() => {}, 2**31 - 1); // to keep the process alive. +await Promise.all([ + once(port1, 'message').then(() => once(port1, 'message')), + import('node:os'), +]); +clearTimeout(timeout); +port1.close(); diff --git a/test/module-hooks/test-async-loader-hooks-called-with-expected-args.mjs b/test/module-hooks/test-async-loader-hooks-called-with-expected-args.mjs new file mode 100644 index 00000000000000..195bf02bb6fdb3 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-called-with-expected-args.mjs @@ -0,0 +1,28 @@ +// Test that loader hooks are called with all expected arguments +import '../common/index.mjs'; +import assert from 'node:assert'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndAssert } from '../common/child_process.js'; + +spawnSyncAndAssert( + execPath, + [ + '--no-warnings', + '--experimental-loader', + fixtures.fileURL('es-module-loaders/hooks-input.mjs'), + fixtures.path('es-modules/json-modules.mjs'), + ], + { + stdout(output) { + const lines = output.split('\n'); + assert.match(lines[0], /{"url":"file:\/\/\/.*\/json-modules\.mjs","format":"test","shortCircuit":true}/); + assert.match(lines[1], /{"source":{"type":"Buffer","data":\[.*\]},"format":"module","shortCircuit":true}/); + assert.match(lines[2], /{"url":"file:\/\/\/.*\/experimental\.json","format":"test","shortCircuit":true}/); + assert.match(lines[3], /{"source":{"type":"Buffer","data":\[.*\]},"format":"json","shortCircuit":true}/); + assert.strictEqual(lines.length, 4); + }, + stderr: '', + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-called-with-register.mjs b/test/module-hooks/test-async-loader-hooks-called-with-register.mjs new file mode 100644 index 00000000000000..d1f9181240d758 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-called-with-register.mjs @@ -0,0 +1,31 @@ +// Test that loader hooks are called with all expected arguments using register function +import '../common/index.mjs'; +import assert from 'node:assert'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndAssert } from '../common/child_process.js'; + +spawnSyncAndAssert( + execPath, + [ + '--no-warnings', + '--experimental-loader=data:text/javascript,', + '--input-type=module', + '--eval', + "import { register } from 'node:module';" + + `register(${JSON.stringify(fixtures.fileURL('es-module-loaders/hooks-input.mjs'))});` + + `await import(${JSON.stringify(fixtures.fileURL('es-modules/json-modules.mjs'))});`, + ], + { + stdout(output) { + const lines = output.split('\n'); + assert.match(lines[0], /{"url":"file:\/\/\/.*\/json-modules\.mjs","format":"test","shortCircuit":true}/); + assert.match(lines[1], /{"source":{"type":"Buffer","data":\[.*\]},"format":"module","shortCircuit":true}/); + assert.match(lines[2], /{"url":"file:\/\/\/.*\/experimental\.json","format":"test","shortCircuit":true}/); + assert.match(lines[3], /{"source":{"type":"Buffer","data":\[.*\]},"format":"json","shortCircuit":true}/); + assert.strictEqual(lines.length, 4); + }, + stderr: '', + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-globalpreload-no-warning-with-initialize.mjs b/test/module-hooks/test-async-loader-hooks-globalpreload-no-warning-with-initialize.mjs new file mode 100644 index 00000000000000..54501748bceac1 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-globalpreload-no-warning-with-initialize.mjs @@ -0,0 +1,21 @@ +// Test that `globalPreload` should not emit warning when `initialize` is supplied + +import '../common/index.mjs'; +import assert from 'node:assert'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndAssert } from '../common/child_process.js'; + +spawnSyncAndAssert( + execPath, + [ + '--experimental-loader', + fixtures.fileURL('es-module-loaders/loader-globalpreload-and-initialize.mjs'), + fixtures.path('empty.js'), + ], + { + stderr(output) { + assert.doesNotMatch(output, /`globalPreload` has been removed; use `initialize` instead/); + }, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-globalpreload-warning.mjs b/test/module-hooks/test-async-loader-hooks-globalpreload-warning.mjs new file mode 100644 index 00000000000000..36a2c0089a91d0 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-globalpreload-warning.mjs @@ -0,0 +1,23 @@ +// Test that `globalPreload` should emit warning +import '../common/index.mjs'; +import assert from 'node:assert'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndAssert } from '../common/child_process.js'; + +spawnSyncAndAssert( + execPath, + [ + '--experimental-loader', + fixtures.fileURL('es-module-loaders/loader-globalpreload-only.mjs'), + '--experimental-loader', + fixtures.fileURL('es-module-loaders/loader-globalpreload-with-return.mjs'), + fixtures.path('empty.js'), + ], + { + stderr(output) { + const matches = output.match(/`globalPreload` has been removed; use `initialize` instead/g); + assert.strictEqual(matches.length, 1); + }, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-initialize-in-sequence.mjs b/test/module-hooks/test-async-loader-hooks-initialize-in-sequence.mjs new file mode 100644 index 00000000000000..bc973f58784527 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-initialize-in-sequence.mjs @@ -0,0 +1,21 @@ +// Test that `initialize` should execute in sequence +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndAssert } from '../common/child_process.js'; + +spawnSyncAndAssert( + execPath, + [ + '--no-warnings', + fixtures.path('module-hooks/register-loader-in-sequence.mjs'), + ], + { + stdout: 'hooks initialize 1\n' + + 'result 1 undefined\n' + + 'hooks initialize 2\n' + + 'result 2 undefined', + trim: true, + stderr: '', + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-initialize-invoke.mjs b/test/module-hooks/test-async-loader-hooks-initialize-invoke.mjs new file mode 100644 index 00000000000000..72dc87c12e359a --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-initialize-invoke.mjs @@ -0,0 +1,22 @@ +// Test that `initialize` should be invoked correctly +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndAssert } from '../common/child_process.js'; + +spawnSyncAndAssert( + execPath, + [ + '--no-warnings', + '--experimental-loader', + fixtures.fileURL('es-module-loaders/hooks-initialize.mjs'), + '--input-type=module', + '--eval', + 'import os from "node:os";', + ], + { + stdout: 'hooks initialize 1', + stderr: '', + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-initialize-never-settling.mjs b/test/module-hooks/test-async-loader-hooks-initialize-never-settling.mjs new file mode 100644 index 00000000000000..d1c12238ef0ca6 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-initialize-never-settling.mjs @@ -0,0 +1,18 @@ +// Test that `initialize` returning never-settling promise should be handled +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndAssert } from '../common/child_process.js'; + +spawnSyncAndAssert( + execPath, + [ + '--no-warnings', + fixtures.path('module-hooks/register-loader-initialize-never-settling.mjs'), + ], + { + stdout: 'caught ERR_ASYNC_LOADER_REQUEST_NEVER_SETTLED', + stderr: '', + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-initialize-process-exit.mjs b/test/module-hooks/test-async-loader-hooks-initialize-process-exit.mjs new file mode 100644 index 00000000000000..d9476af27e8f7d --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-initialize-process-exit.mjs @@ -0,0 +1,23 @@ +// Test that it should be fine to call `process.exit` from a `initialize` hook +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndExit } from '../common/child_process.js'; + +spawnSyncAndExit( + execPath, + [ + '--no-warnings', + '--experimental-loader', + fixtures.fileURL('es-module-loaders/loader-initialize-exit.mjs'), + '--input-type=module', + '--eval', + 'import "node:os"', + ], + { + status: 42, + signal: null, + stdout: '', + stderr: '', + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-initialize-rejecting.mjs b/test/module-hooks/test-async-loader-hooks-initialize-rejecting.mjs new file mode 100644 index 00000000000000..e5d2a62fbc86da --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-initialize-rejecting.mjs @@ -0,0 +1,24 @@ +// Test that `initialize` returning rejecting promise should be handled +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndExit } from '../common/child_process.js'; + +spawnSyncAndExit( + execPath, + [ + '--no-warnings', + '--experimental-loader', + fixtures.fileURL('es-module-loaders/loader-initialize-rejecting.mjs'), + '--input-type=module', + '--eval', + 'import "node:os"', + ], + { + status: 1, + signal: null, + stdout: '', + stderr: /undefined$/m, + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-initialize-throw-null.mjs b/test/module-hooks/test-async-loader-hooks-initialize-throw-null.mjs new file mode 100644 index 00000000000000..69844e31ae91fc --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-initialize-throw-null.mjs @@ -0,0 +1,24 @@ +// Test that `initialize` throwing null should be handled +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndExit } from '../common/child_process.js'; + +spawnSyncAndExit( + execPath, + [ + '--no-warnings', + '--experimental-loader', + fixtures.fileURL('es-module-loaders/loader-initialize-throw-null.mjs'), + '--input-type=module', + '--eval', + 'import "node:os"', + ], + { + status: 1, + signal: null, + stdout: '', + stderr: /null$/m, + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-mixed-opt-in.mjs b/test/module-hooks/test-async-loader-hooks-mixed-opt-in.mjs new file mode 100644 index 00000000000000..398df25d7bbe66 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-mixed-opt-in.mjs @@ -0,0 +1,20 @@ +// Test that loader should handle mixed of opt-in modules and non-opt-in ones +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndAssert } from '../common/child_process.js'; + +spawnSyncAndAssert( + execPath, + [ + '--no-warnings', + '--experimental-loader', + fixtures.fileURL('es-module-loaders/loader-mixed-opt-in.mjs'), + 'entry-point', + ], + { + stdout: 'Hello', + stderr: '', + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-never-settling-import-meta-resolve.mjs b/test/module-hooks/test-async-loader-hooks-never-settling-import-meta-resolve.mjs new file mode 100644 index 00000000000000..35a4f7a566e519 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-never-settling-import-meta-resolve.mjs @@ -0,0 +1,20 @@ +// Test import.meta.resolve of a never-settling resolve should throw in ESM files +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndAssert } from '../common/child_process.js'; + +spawnSyncAndAssert( + execPath, + [ + '--no-warnings', + '--experimental-loader', + fixtures.fileURL('es-module-loaders/never-settling-resolve-step/loader.mjs'), + fixtures.path('es-module-loaders/never-settling-resolve-step/import.meta.never-resolve.mjs'), + ], + { + stdout: /^should be output$/, + stderr: '', + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-never-settling-load-cjs.mjs b/test/module-hooks/test-async-loader-hooks-never-settling-load-cjs.mjs new file mode 100644 index 00000000000000..05401b9080b339 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-never-settling-load-cjs.mjs @@ -0,0 +1,20 @@ +// Test never-settling load in CJS files +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndAssert } from '../common/child_process.js'; + +spawnSyncAndAssert( + execPath, + [ + '--no-warnings', + '--experimental-loader', + fixtures.fileURL('es-module-loaders/never-settling-resolve-step/loader.mjs'), + fixtures.path('es-module-loaders/never-settling-resolve-step/never-load.cjs'), + ], + { + stdout: /^should be output$/, + stderr: '', + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-never-settling-load-esm-no-warning.mjs b/test/module-hooks/test-async-loader-hooks-never-settling-load-esm-no-warning.mjs new file mode 100644 index 00000000000000..dbadecc3287672 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-never-settling-load-esm-no-warning.mjs @@ -0,0 +1,22 @@ +// Test top-level await of a never-settling load without warning in ESM files +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndExit } from '../common/child_process.js'; + +spawnSyncAndExit( + execPath, + [ + '--no-warnings', + '--experimental-loader', + fixtures.fileURL('es-module-loaders/never-settling-resolve-step/loader.mjs'), + fixtures.path('es-module-loaders/never-settling-resolve-step/never-load.mjs'), + ], + { + status: 13, + signal: null, + stdout: /^should be output$/, + stderr: '', + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-never-settling-load-esm-with-warning.mjs b/test/module-hooks/test-async-loader-hooks-never-settling-load-esm-with-warning.mjs new file mode 100644 index 00000000000000..d4b30ae23f86f5 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-never-settling-load-esm-with-warning.mjs @@ -0,0 +1,21 @@ +// Test top-level await of a never-settling load with warning in ESM files +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndExit } from '../common/child_process.js'; + +spawnSyncAndExit( + execPath, + [ + '--experimental-loader', + fixtures.fileURL('es-module-loaders/never-settling-resolve-step/loader.mjs'), + fixtures.path('es-module-loaders/never-settling-resolve-step/never-load.mjs'), + ], + { + status: 13, + signal: null, + stdout: /^should be output$/, + stderr: /Warning: Detected unsettled top-level await at.+never-load\.mjs:5/, + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-never-settling-race-cjs.mjs b/test/module-hooks/test-async-loader-hooks-never-settling-race-cjs.mjs new file mode 100644 index 00000000000000..1de677f888383b --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-never-settling-race-cjs.mjs @@ -0,0 +1,20 @@ +// Test race of never-settling hooks in CJS files +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndAssert } from '../common/child_process.js'; + +spawnSyncAndAssert( + execPath, + [ + '--no-warnings', + '--experimental-loader', + fixtures.fileURL('es-module-loaders/never-settling-resolve-step/loader.mjs'), + fixtures.path('es-module-loaders/never-settling-resolve-step/race.cjs'), + ], + { + stdout: /^true$/, + stderr: '', + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-never-settling-race-esm.mjs b/test/module-hooks/test-async-loader-hooks-never-settling-race-esm.mjs new file mode 100644 index 00000000000000..e662dc43210d72 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-never-settling-race-esm.mjs @@ -0,0 +1,20 @@ +// Test top-level await of a race of never-settling hooks in ESM files +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndAssert } from '../common/child_process.js'; + +spawnSyncAndAssert( + execPath, + [ + '--no-warnings', + '--experimental-loader', + fixtures.fileURL('es-module-loaders/never-settling-resolve-step/loader.mjs'), + fixtures.path('es-module-loaders/never-settling-resolve-step/race.mjs'), + ], + { + stdout: /^true$/, + stderr: '', + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-never-settling-resolve-cjs.mjs b/test/module-hooks/test-async-loader-hooks-never-settling-resolve-cjs.mjs new file mode 100644 index 00000000000000..1469a7b313cce2 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-never-settling-resolve-cjs.mjs @@ -0,0 +1,20 @@ +// Test never-settling resolve in CJS files +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndAssert } from '../common/child_process.js'; + +spawnSyncAndAssert( + execPath, + [ + '--no-warnings', + '--experimental-loader', + fixtures.fileURL('es-module-loaders/never-settling-resolve-step/loader.mjs'), + fixtures.path('es-module-loaders/never-settling-resolve-step/never-resolve.cjs'), + ], + { + stdout: /^should be output$/, + stderr: '', + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-never-settling-resolve-esm-no-warning.mjs b/test/module-hooks/test-async-loader-hooks-never-settling-resolve-esm-no-warning.mjs new file mode 100644 index 00000000000000..43a7a98127c851 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-never-settling-resolve-esm-no-warning.mjs @@ -0,0 +1,22 @@ +// Test top-level await of a never-settling resolve without warning in ESM files +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndExit } from '../common/child_process.js'; + +spawnSyncAndExit( + execPath, + [ + '--no-warnings', + '--experimental-loader', + fixtures.fileURL('es-module-loaders/never-settling-resolve-step/loader.mjs'), + fixtures.path('es-module-loaders/never-settling-resolve-step/never-resolve.mjs'), + ], + { + status: 13, + signal: null, + stdout: /^should be output$/, + stderr: '', + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-never-settling-resolve-esm-with-warning.mjs b/test/module-hooks/test-async-loader-hooks-never-settling-resolve-esm-with-warning.mjs new file mode 100644 index 00000000000000..1a4c40c8bb96c0 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-never-settling-resolve-esm-with-warning.mjs @@ -0,0 +1,21 @@ +// Test top-level await of a never-settling resolve with warning in ESM files +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndExit } from '../common/child_process.js'; + +spawnSyncAndExit( + execPath, + [ + '--experimental-loader', + fixtures.fileURL('es-module-loaders/never-settling-resolve-step/loader.mjs'), + fixtures.path('es-module-loaders/never-settling-resolve-step/never-resolve.mjs'), + ], + { + status: 13, + signal: null, + stdout: /^should be output$/, + stderr: /Warning: Detected unsettled top-level await at.+never-resolve\.mjs:5/, + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-no-leak-internals.mjs b/test/module-hooks/test-async-loader-hooks-no-leak-internals.mjs new file mode 100644 index 00000000000000..781aed8601f54a --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-no-leak-internals.mjs @@ -0,0 +1,20 @@ +// Test that loader hooks should not leak internals or expose import.meta.resolve +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndAssert } from '../common/child_process.js'; + +spawnSyncAndAssert( + execPath, + [ + '--no-warnings', + '--experimental-loader', + fixtures.fileURL('es-module-loaders/loader-edge-cases.mjs'), + fixtures.path('empty.js'), + ], + { + stdout: '', + stderr: '', + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-process-exit-async.mjs b/test/module-hooks/test-async-loader-hooks-process-exit-async.mjs new file mode 100644 index 00000000000000..af3b939bcf8c0e --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-process-exit-async.mjs @@ -0,0 +1,24 @@ +// Test that it should be fine to call `process.exit` from a custom async hook +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndExit } from '../common/child_process.js'; + +spawnSyncAndExit( + execPath, + [ + '--no-warnings', + '--experimental-loader', + fixtures.fileURL('es-module-loaders/loader-exit-on-load.mjs'), + '--input-type=module', + '--eval', + 'import "data:exit"', + ], + { + status: 42, + signal: null, + stdout: '', + stderr: '', + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-process-exit-sync.mjs b/test/module-hooks/test-async-loader-hooks-process-exit-sync.mjs new file mode 100644 index 00000000000000..64c079b90b7507 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-process-exit-sync.mjs @@ -0,0 +1,24 @@ +// Test that it should be fine to call `process.exit` from a custom sync hook +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndExit } from '../common/child_process.js'; + +spawnSyncAndExit( + execPath, + [ + '--no-warnings', + '--experimental-loader', + fixtures.fileURL('es-module-loaders/loader-exit-on-resolve.mjs'), + '--input-type=module', + '--eval', + 'import "data:text/javascript,import.meta.resolve(%22exit:%22)"', + ], + { + status: 42, + signal: null, + stdout: '', + stderr: '', + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-process-exit-top-level.mjs b/test/module-hooks/test-async-loader-hooks-process-exit-top-level.mjs new file mode 100644 index 00000000000000..a25c0013c5076d --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-process-exit-top-level.mjs @@ -0,0 +1,22 @@ +// Test that it should be fine to call process.exit from the loader thread top-level +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndExit } from '../common/child_process.js'; + +spawnSyncAndExit( + execPath, + [ + '--no-warnings', + '--experimental-loader', + fixtures.fileURL('es-module-loaders/loader-exit-top-level.mjs'), + fixtures.path('empty.js'), + ], + { + status: 42, + signal: null, + stdout: '', + stderr: '', + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-register-with-cjs.mjs b/test/module-hooks/test-async-loader-hooks-register-with-cjs.mjs new file mode 100644 index 00000000000000..8d83ee6be87816 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-register-with-cjs.mjs @@ -0,0 +1,20 @@ +// Test that `register` should work with cjs +import '../common/index.mjs'; +import assert from 'node:assert'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndAssert } from '../common/child_process.js'; + +spawnSyncAndAssert( + execPath, + [ + '--no-warnings', + fixtures.path('module-hooks/register-loader-with-cjs.cjs'), + ], + { + stdout(output) { + assert.deepStrictEqual(output.split('\n').sort(), ['hooks initialize 1', '{"default":"foo"}', ''].sort()); + }, + stderr: '', + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-register-with-import.mjs b/test/module-hooks/test-async-loader-hooks-register-with-import.mjs new file mode 100644 index 00000000000000..454eccb5c62478 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-register-with-import.mjs @@ -0,0 +1,22 @@ +// Test that `register` should work with `import` +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndAssert } from '../common/child_process.js'; + +spawnSyncAndAssert( + execPath, + [ + '--no-warnings', + '--import', + fixtures.fileURL('es-module-loaders/register-loader.mjs'), + '--input-type=module', + '--eval', + 'import "node:os"', + ], + { + stdout: 'resolve passthru', + stderr: '', + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-register-with-ports.mjs b/test/module-hooks/test-async-loader-hooks-register-with-ports.mjs new file mode 100644 index 00000000000000..022760596fd929 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-register-with-ports.mjs @@ -0,0 +1,18 @@ +// Test that loader should allow communicating with loader via `register` ports +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndAssert } from '../common/child_process.js'; + +spawnSyncAndAssert( + execPath, + [ + '--no-warnings', + fixtures.path('module-hooks/register-loader-with-ports.mjs'), + ], + { + stdout: 'register undefined\nmessage initialize\nmessage resolve node:os', + stderr: '', + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-register-with-require.mjs b/test/module-hooks/test-async-loader-hooks-register-with-require.mjs new file mode 100644 index 00000000000000..87ea43f8a66c9d --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-register-with-require.mjs @@ -0,0 +1,22 @@ +// Test that `register` should work with `require` +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndAssert } from '../common/child_process.js'; + +spawnSyncAndAssert( + execPath, + [ + '--no-warnings', + '--require', + fixtures.path('es-module-loaders/register-loader.cjs'), + '--input-type=module', + '--eval', + 'import "node:os";', + ], + { + stdout: 'resolve passthru\nresolve passthru', + stderr: '', + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-register-with-url-parenturl.mjs b/test/module-hooks/test-async-loader-hooks-register-with-url-parenturl.mjs new file mode 100644 index 00000000000000..ed5289e61740e9 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-register-with-url-parenturl.mjs @@ -0,0 +1,24 @@ +// Test that `register` should accept URL objects as `parentURL` +import '../common/index.mjs'; +import assert from 'node:assert'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndAssert } from '../common/child_process.js'; + +spawnSyncAndAssert( + execPath, + [ + '--no-warnings', + '--import', + fixtures.fileURL('es-module-loaders/register-hooks-with-register.mjs'), + fixtures.path('es-module-loaders/register-loader-with-url-parenturl.mjs'), + ], + { + cwd: fixtures.path('es-module-loaders/'), + stdout(output) { + assert.deepStrictEqual(output.split('\n').sort(), ['hooks initialize 1', '{"default":"foo"}'].sort()); + }, + stderr: '', + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-remove-beforeexit-listener.mjs b/test/module-hooks/test-async-loader-hooks-remove-beforeexit-listener.mjs new file mode 100644 index 00000000000000..9a28bb59dd3164 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-remove-beforeexit-listener.mjs @@ -0,0 +1,22 @@ +// Test that it should be fine to call `process.removeAllListeners("beforeExit")`("beforeExit") from the main thread +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndAssert } from '../common/child_process.js'; + +spawnSyncAndAssert( + execPath, + [ + '--no-warnings', + '--experimental-loader', + fixtures.fileURL('es-module-loaders/loader-delayed-async-load.mjs'), + '--input-type=module', + '--eval', + 'setInterval(() => process.removeAllListeners("beforeExit"),1).unref();await import("data:text/javascript,")', + ], + { + stdout: '', + stderr: '', + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-require-resolve-default.mjs b/test/module-hooks/test-async-loader-hooks-require-resolve-default.mjs new file mode 100644 index 00000000000000..71e539fe118b8b --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-require-resolve-default.mjs @@ -0,0 +1,20 @@ +// Test that loader should use CJS loader to respond to `require.resolve` calls by default +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndAssert } from '../common/child_process.js'; + +spawnSyncAndAssert( + execPath, + [ + '--no-warnings', + '--experimental-loader', + fixtures.fileURL('es-module-loaders/loader-resolve-passthru.mjs'), + fixtures.path('require-resolve.js'), + ], + { + stdout: 'resolve passthru', + stderr: '', + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-require-resolve-opt-in.mjs b/test/module-hooks/test-async-loader-hooks-require-resolve-opt-in.mjs new file mode 100644 index 00000000000000..0cbf2fd2d942a4 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-require-resolve-opt-in.mjs @@ -0,0 +1,22 @@ +// Test that loader should use ESM loader to respond to `require.resolve` calls when opting in +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndAssert } from '../common/child_process.js'; + +spawnSyncAndAssert( + execPath, + [ + '--no-warnings', + '--experimental-loader', + fixtures.fileURL('es-module-loaders/loader-load-commonjs-with-source.mjs'), + '--experimental-loader', + fixtures.fileURL('es-module-loaders/loader-resolve-passthru.mjs'), + fixtures.path('require-resolve.js'), + ], + { + stdout: 'resolve passthru\n'.repeat(10).trim(), + stderr: '', + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-source-maps-cjs.mjs b/test/module-hooks/test-async-loader-hooks-source-maps-cjs.mjs new file mode 100644 index 00000000000000..83bd53405d1bd9 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-source-maps-cjs.mjs @@ -0,0 +1,23 @@ +// Test that loader should support source maps in commonjs translator +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndExit } from '../common/child_process.js'; + +spawnSyncAndExit( + execPath, + [ + '--no-warnings', + '--enable-source-maps', + '--import', + fixtures.fileURL('es-module-loaders/loader-load-source-maps.mjs'), + fixtures.path('source-map/throw-on-require.js'), + ], + { + status: 1, + signal: null, + stdout: '', + stderr: /throw-on-require\.ts:9:9/, + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-throw-bigint.mjs b/test/module-hooks/test-async-loader-hooks-throw-bigint.mjs new file mode 100644 index 00000000000000..34136f81bf2193 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-throw-bigint.mjs @@ -0,0 +1,22 @@ +// Test that loader hooks should handle bigint thrown from top-level +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndExit } from '../common/child_process.js'; + +spawnSyncAndExit( + execPath, + [ + '--no-warnings', + '--experimental-loader', + fixtures.fileURL('es-module-loaders/loader-throw-bigint.mjs'), + fixtures.path('empty.js'), + ], + { + status: 1, + signal: null, + stdout: '', + stderr: /^1$/m, + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-throw-boolean.mjs b/test/module-hooks/test-async-loader-hooks-throw-boolean.mjs new file mode 100644 index 00000000000000..476fad3c141535 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-throw-boolean.mjs @@ -0,0 +1,22 @@ +// Test that loader hooks should handle boolean thrown from top-level +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndExit } from '../common/child_process.js'; + +spawnSyncAndExit( + execPath, + [ + '--no-warnings', + '--experimental-loader', + fixtures.fileURL('es-module-loaders/loader-throw-boolean.mjs'), + fixtures.path('empty.js'), + ], + { + status: 1, + signal: null, + stdout: '', + stderr: /^true$/m, + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-throw-empty-object.mjs b/test/module-hooks/test-async-loader-hooks-throw-empty-object.mjs new file mode 100644 index 00000000000000..ac09704c28ca6d --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-throw-empty-object.mjs @@ -0,0 +1,22 @@ +// Test that loader hooks should handle empty plain object thrown from top-level +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndExit } from '../common/child_process.js'; + +spawnSyncAndExit( + execPath, + [ + '--no-warnings', + '--experimental-loader', + fixtures.fileURL('es-module-loaders/loader-throw-empty-object.mjs'), + fixtures.path('empty.js'), + ], + { + status: 1, + signal: null, + stdout: '', + stderr: /^\{\}$/m, + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-throw-error.mjs b/test/module-hooks/test-async-loader-hooks-throw-error.mjs new file mode 100644 index 00000000000000..b37ce50db1e3f4 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-throw-error.mjs @@ -0,0 +1,22 @@ +// Test that loader hooks should handle regular Error object thrown from top-level +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndExit } from '../common/child_process.js'; + +spawnSyncAndExit( + execPath, + [ + '--no-warnings', + '--experimental-loader', + fixtures.fileURL('es-module-loaders/loader-throw-error.mjs'), + fixtures.path('empty.js'), + ], + { + status: 1, + signal: null, + stdout: '', + stderr: /^Error: error message$/m, + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-throw-function.mjs b/test/module-hooks/test-async-loader-hooks-throw-function.mjs new file mode 100644 index 00000000000000..bb371f727c84c7 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-throw-function.mjs @@ -0,0 +1,22 @@ +// Test that loader hooks should handle function thrown from top-level +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndExit } from '../common/child_process.js'; + +spawnSyncAndExit( + execPath, + [ + '--no-warnings', + '--experimental-loader', + fixtures.fileURL('es-module-loaders/loader-throw-function.mjs'), + fixtures.path('empty.js'), + ], + { + status: 1, + signal: null, + stdout: '', + stderr: /^\[Function: fnName\]$/m, + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-throw-null.mjs b/test/module-hooks/test-async-loader-hooks-throw-null.mjs new file mode 100644 index 00000000000000..33528ff2b97560 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-throw-null.mjs @@ -0,0 +1,22 @@ +// Test that loader hooks should handle null thrown from top-level +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndExit } from '../common/child_process.js'; + +spawnSyncAndExit( + execPath, + [ + '--no-warnings', + '--experimental-loader', + fixtures.fileURL('es-module-loaders/loader-throw-null.mjs'), + fixtures.path('empty.js'), + ], + { + status: 1, + signal: null, + stdout: '', + stderr: /^null$/m, + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-throw-number.mjs b/test/module-hooks/test-async-loader-hooks-throw-number.mjs new file mode 100644 index 00000000000000..da1f301089ca6c --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-throw-number.mjs @@ -0,0 +1,22 @@ +// Test that loader hooks should handle number thrown from top-level +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndExit } from '../common/child_process.js'; + +spawnSyncAndExit( + execPath, + [ + '--no-warnings', + '--experimental-loader', + fixtures.fileURL('es-module-loaders/loader-throw-number.mjs'), + fixtures.path('empty.js'), + ], + { + status: 1, + signal: null, + stdout: '', + stderr: /^1$/m, + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-throw-object.mjs b/test/module-hooks/test-async-loader-hooks-throw-object.mjs new file mode 100644 index 00000000000000..8280bd0d105bb9 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-throw-object.mjs @@ -0,0 +1,22 @@ +// Test that loader hooks should handle plain object thrown from top-level +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndExit } from '../common/child_process.js'; + +spawnSyncAndExit( + execPath, + [ + '--no-warnings', + '--experimental-loader', + fixtures.fileURL('es-module-loaders/loader-throw-object.mjs'), + fixtures.path('empty.js'), + ], + { + status: 1, + signal: null, + stdout: '', + stderr: /^\{ fn: \[Function: fn\], symbol: Symbol\(symbol\), u: undefined \}$/m, + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-throw-string.mjs b/test/module-hooks/test-async-loader-hooks-throw-string.mjs new file mode 100644 index 00000000000000..4ea6372e865c61 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-throw-string.mjs @@ -0,0 +1,22 @@ +// Test that loader hooks should handle string thrown from top-level +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndExit } from '../common/child_process.js'; + +spawnSyncAndExit( + execPath, + [ + '--no-warnings', + '--experimental-loader', + fixtures.fileURL('es-module-loaders/loader-throw-string.mjs'), + fixtures.path('empty.js'), + ], + { + status: 1, + signal: null, + stdout: '', + stderr: /^literal string$/m, + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-throw-symbol.mjs b/test/module-hooks/test-async-loader-hooks-throw-symbol.mjs new file mode 100644 index 00000000000000..44339537ba9ce8 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-throw-symbol.mjs @@ -0,0 +1,20 @@ +// Test that loader hooks should handle symbol thrown from top-level +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndExit } from '../common/child_process.js'; + +spawnSyncAndExit( + execPath, + [ + '--experimental-loader', + fixtures.fileURL('es-module-loaders/loader-throw-symbol.mjs'), + fixtures.path('empty.js'), + ], + { + status: 1, + signal: null, + stdout: '', // Throwing a symbol doesn't produce any output + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-throw-undefined.mjs b/test/module-hooks/test-async-loader-hooks-throw-undefined.mjs new file mode 100644 index 00000000000000..aff5292a857f59 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-throw-undefined.mjs @@ -0,0 +1,22 @@ +// Test that loader hooks should handle undefined thrown from top-level +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndExit } from '../common/child_process.js'; + +spawnSyncAndExit( + execPath, + [ + '--no-warnings', + '--experimental-loader', + fixtures.fileURL('es-module-loaders/loader-throw-undefined.mjs'), + fixtures.path('empty.js'), + ], + { + status: 1, + signal: null, + stdout: '', + stderr: /^undefined$/m, + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-use-hooks-require-esm.mjs b/test/module-hooks/test-async-loader-hooks-use-hooks-require-esm.mjs new file mode 100644 index 00000000000000..a86b0607246fa9 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-use-hooks-require-esm.mjs @@ -0,0 +1,20 @@ +// Test that loader should use hooks for require of ESM +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndAssert } from '../common/child_process.js'; + +spawnSyncAndAssert( + execPath, + [ + '--no-experimental-require-module', + '--import', + fixtures.fileURL('es-module-loaders/builtin-named-exports.mjs'), + fixtures.path('es-modules/require-esm-throws-with-loaders.js'), + ], + { + stdout: '', + stderr: '', + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-with-worker-permission-allowed.mjs b/test/module-hooks/test-async-loader-hooks-with-worker-permission-allowed.mjs new file mode 100644 index 00000000000000..9be7e8c39600a5 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-with-worker-permission-allowed.mjs @@ -0,0 +1,24 @@ +// Test that loader hooks should allow spawning workers when allowed by CLI flags +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndAssert } from '../common/child_process.js'; + +spawnSyncAndAssert( + execPath, + [ + '--no-warnings', + '--permission', + '--allow-worker', + '--allow-fs-read', + '*', + '--experimental-loader', + fixtures.fileURL('es-module-loaders/loader-worker-spawn.mjs'), + fixtures.path('es-modules/esm-top-level-await.mjs'), + ], + { + stdout: /^1\n2$/, + stderr: '', + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-with-worker-permission-restricted.mjs b/test/module-hooks/test-async-loader-hooks-with-worker-permission-restricted.mjs new file mode 100644 index 00000000000000..fe76f2f5842c00 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-with-worker-permission-restricted.mjs @@ -0,0 +1,25 @@ +// Test that loader hooks should not allow spawning workers if restricted by CLI flags +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndExit } from '../common/child_process.js'; + +spawnSyncAndExit( + execPath, + [ + '--no-warnings', + '--permission', + '--allow-fs-read', + '*', + '--experimental-loader', + fixtures.fileURL('es-module-loaders/loader-worker-spawn.mjs'), + fixtures.path('es-modules/esm-top-level-await.mjs'), + ], + { + status: 1, + signal: null, + stdout: '', + stderr: /code: 'ERR_ACCESS_DENIED'/, + trim: true, + }, +); diff --git a/test/module-hooks/test-async-loader-hooks-without-worker-permission.mjs b/test/module-hooks/test-async-loader-hooks-without-worker-permission.mjs new file mode 100644 index 00000000000000..21589234144db1 --- /dev/null +++ b/test/module-hooks/test-async-loader-hooks-without-worker-permission.mjs @@ -0,0 +1,25 @@ +// Test that loader hooks should not work without worker permission +import '../common/index.mjs'; +import { execPath } from 'node:process'; +import fixtures from '../common/fixtures.js'; +import { spawnSyncAndExit } from '../common/child_process.js'; + +spawnSyncAndExit( + execPath, + [ + '--no-warnings', + '--permission', + '--allow-fs-read', + '*', + '--experimental-loader', + fixtures.fileURL('empty.js'), + fixtures.path('es-modules/esm-top-level-await.mjs'), + ], + { + status: 1, + signal: null, + stdout: '', + stderr: /Error: Access to this API has been restricted/, + trim: true, + }, +); From 110ecb7df4732599e755ed2104c0b6572b2a4a89 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Wed, 14 Jan 2026 16:45:03 +0100 Subject: [PATCH 2/2] fixup! test: split test-esm-loader-hooks --- .../register-hooks-with-current-cwd-parent-url.mjs | 3 +++ .../es-module-loaders/register-hooks-with-register.mjs | 6 ------ .../test-async-loader-hooks-register-with-url-parenturl.mjs | 4 +++- 3 files changed, 6 insertions(+), 7 deletions(-) create mode 100644 test/fixtures/es-module-loaders/register-hooks-with-current-cwd-parent-url.mjs delete mode 100644 test/fixtures/es-module-loaders/register-hooks-with-register.mjs diff --git a/test/fixtures/es-module-loaders/register-hooks-with-current-cwd-parent-url.mjs b/test/fixtures/es-module-loaders/register-hooks-with-current-cwd-parent-url.mjs new file mode 100644 index 00000000000000..580d092ab5655a --- /dev/null +++ b/test/fixtures/es-module-loaders/register-hooks-with-current-cwd-parent-url.mjs @@ -0,0 +1,3 @@ +import { register } from 'node:module'; +import { pathToFileURL } from 'node:url'; +register('./hooks-initialize.mjs', pathToFileURL('./')); diff --git a/test/fixtures/es-module-loaders/register-hooks-with-register.mjs b/test/fixtures/es-module-loaders/register-hooks-with-register.mjs deleted file mode 100644 index f6ca913e5958f9..00000000000000 --- a/test/fixtures/es-module-loaders/register-hooks-with-register.mjs +++ /dev/null @@ -1,6 +0,0 @@ -import { register } from 'node:module'; -import { fileURLToPath } from 'node:url'; -import { dirname, join } from 'node:path'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -register(join(__dirname, 'hooks-initialize.mjs'), import.meta.url); diff --git a/test/module-hooks/test-async-loader-hooks-register-with-url-parenturl.mjs b/test/module-hooks/test-async-loader-hooks-register-with-url-parenturl.mjs index ed5289e61740e9..a033ab3c4d2fdc 100644 --- a/test/module-hooks/test-async-loader-hooks-register-with-url-parenturl.mjs +++ b/test/module-hooks/test-async-loader-hooks-register-with-url-parenturl.mjs @@ -10,11 +10,13 @@ spawnSyncAndAssert( [ '--no-warnings', '--import', - fixtures.fileURL('es-module-loaders/register-hooks-with-register.mjs'), + fixtures.fileURL('es-module-loaders/register-hooks-with-current-cwd-parent-url.mjs'), fixtures.path('es-module-loaders/register-loader-with-url-parenturl.mjs'), ], { cwd: fixtures.path('es-module-loaders/'), + }, + { stdout(output) { assert.deepStrictEqual(output.split('\n').sort(), ['hooks initialize 1', '{"default":"foo"}'].sort()); },