Upload Kmake

This commit is contained in:
Gorochu
2026-05-26 23:36:42 -07:00
parent ba051b2f74
commit 555ec72358
41615 changed files with 13344630 additions and 1 deletions

View File

@ -0,0 +1,19 @@
prefix es-module
# To mark a test as flaky, list the test name in the appropriate section
# below, without ".js", followed by ": PASS,FLAKY". Example:
# sample-test : PASS,FLAKY
[true] # This section applies to all platforms
[$system==linux || $system==freebsd]
# https://github.com/nodejs/node/issues/47836
test-esm-loader-http-imports: PASS,FLAKY
[$arch==arm || $arch==arm64]
# https://github.com/nodejs/node/issues/47297
test-wasm-web-api: SKIP
[$system==ibmi]
# https://github.com/nodejs/node/issues/58582
test-wasm-web-api: PASS,FLAKY

View File

@ -0,0 +1,75 @@
// Previously, this tested that require(esm) throws ERR_REQUIRE_ESM, which is no longer applicable
// since require(esm) is now supported. The test has been repurposed to ensure that the old behavior
// is preserved when the --no-experimental-require-module flag is used.
'use strict';
const { spawnPromisified } = require('../common');
const fixtures = require('../common/fixtures.js');
const assert = require('node:assert');
const path = require('node:path');
const { execPath } = require('node:process');
const { describe, it } = require('node:test');
const requiringCjsAsEsm = path.resolve(fixtures.path('/es-modules/cjs-esm.js'));
const requiringEsm = path.resolve(fixtures.path('/es-modules/cjs-esm-esm.js'));
const pjson = path.toNamespacedPath(
fixtures.path('/es-modules/package-type-module/package.json')
);
describe('CJS ↔︎ ESM interop warnings', { concurrency: !process.env.TEST_PARALLEL }, () => {
it(async () => {
const required = path.resolve(
fixtures.path('/es-modules/package-type-module/cjs.js')
);
const basename = 'cjs.js';
const { code, signal, stderr } = await spawnPromisified(execPath, [
'--no-experimental-require-module', requiringCjsAsEsm,
]);
assert.ok(
stderr.replaceAll('\r', '').includes(
`Error [ERR_REQUIRE_ESM]: require() of ES Module ${required} from ${requiringCjsAsEsm} not supported.\n`
)
);
assert.ok(
stderr.replaceAll('\r', '').includes(
`Instead either rename ${basename} to end in .cjs, change the requiring ` +
'code to use dynamic import() which is available in all CommonJS ' +
`modules, or change "type": "module" to "type": "commonjs" in ${pjson} to ` +
'treat all .js files as CommonJS (using .mjs for all ES modules ' +
'instead).\n'
)
);
assert.strictEqual(code, 1);
assert.strictEqual(signal, null);
});
it(async () => {
const required = path.resolve(
fixtures.path('/es-modules/package-type-module/esm.js')
);
const basename = 'esm.js';
const { code, signal, stderr } = await spawnPromisified(execPath, [
'--no-experimental-require-module', requiringEsm,
]);
assert.ok(
stderr.replace(/\r/g, '').includes(
`Error [ERR_REQUIRE_ESM]: require() of ES Module ${required} from ${requiringEsm} not supported.\n`
)
);
assert.ok(
stderr.replace(/\r/g, '').includes(
`Instead change the require of ${basename} in ${requiringEsm} to` +
' a dynamic import() which is available in all CommonJS modules.\n'
)
);
assert.strictEqual(code, 1);
assert.strictEqual(signal, null);
});
});

View File

@ -0,0 +1,140 @@
'use strict';
// Flags: --expose-internals --permission --allow-fs-read=* --allow-child-process
require('../common');
const { describe, it } = require('node:test');
const assert = require('node:assert');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const fixtures = require('../common/fixtures.js');
function escapeWhenSepIsBackSlash(filePath) {
return path.sep === '\\' ? filePath.replace(/\\/g, '\\\\') : filePath;
}
describe('legacyMainResolve', () => {
it('should ensure permission model when resolving by packageConfig.main', () => {
const fixtextureFolder = fixtures.path('/es-modules/legacy-main-resolver');
const paths = [
['./index-js/index.js', []],
['./index-js/index', ['./index-js/index.js']],
['./index-json/index', ['./index-json/index.js']],
['./index-node/index', ['./index-node/index.js', './index-node/index.json']],
['./index-js', []],
['./index-json', ['./index-json/index.js']],
['./index-node', ['./index-node/index.js', './index-node/index.json']],
];
for (const [mainOrFolder, allowReads] of paths) {
const allowReadFilePaths = allowReads.map((filepath) => path.resolve(fixtextureFolder, filepath));
const allowReadFiles = allowReads?.length > 0 ?
allowReadFilePaths.flatMap((path) => ['--allow-fs-read', path]) :
[];
const fixtextureFolderEscaped = escapeWhenSepIsBackSlash(fixtextureFolder);
const { status, stderr } = spawnSync(
process.execPath,
[
'--expose-internals',
'--permission',
...allowReadFiles,
'-e',
`
const { legacyMainResolve } = require('node:internal/modules/esm/resolve');
const { pathToFileURL } = require('node:url');
const path = require('node:path');
const assert = require('node:assert');
const packageJsonUrl = pathToFileURL(
path.resolve(
${JSON.stringify(fixtextureFolderEscaped)},
'package.json'
)
);
const packageConfig = { main: '${mainOrFolder}' };
const base = path.resolve(
${JSON.stringify(fixtextureFolderEscaped)},
);
assert.throws(() => legacyMainResolve(packageJsonUrl, packageConfig, base), {
code: 'ERR_ACCESS_DENIED',
resource: path.toNamespacedPath(
path.resolve(
${JSON.stringify(fixtextureFolderEscaped)},
${JSON.stringify(mainOrFolder)},
)
)
});
`,
]
);
assert.strictEqual(status, 0, stderr.toString());
}
});
it('should ensure permission model when resolving by packageJsonUrl', () => {
const fixtextureFolder = fixtures.path('/es-modules/legacy-main-resolver');
const paths = [
['./index-js', './index.js', []],
['./index-json', './index.json', ['./index.js']],
['./index-node', './index.node', ['./index.js', './index.json']],
];
for (const [folder, expectedFile, allowReads] of paths) {
const allowReadFilePaths = allowReads.map((filepath) => path.resolve(fixtextureFolder, folder, filepath));
const allowReadFiles = allowReads?.length > 0 ?
allowReadFilePaths.flatMap((path) => ['--allow-fs-read', path]) :
[];
const fixtextureFolderEscaped = escapeWhenSepIsBackSlash(fixtextureFolder);
const { status, stderr } = spawnSync(
process.execPath,
[
'--expose-internals',
'--permission',
...allowReadFiles,
'-e',
`
const { legacyMainResolve } = require('node:internal/modules/esm/resolve');
const { pathToFileURL } = require('node:url');
const path = require('node:path');
const assert = require('node:assert');
const packageJsonUrl = pathToFileURL(
path.resolve(
${JSON.stringify(fixtextureFolderEscaped)},
${JSON.stringify(folder)},
'package.json'
)
);
const packageConfig = { main: undefined };
const base = path.resolve(
${JSON.stringify(fixtextureFolderEscaped)},
);
assert.throws(() => legacyMainResolve(packageJsonUrl, packageConfig, base), {
code: 'ERR_ACCESS_DENIED',
resource: path.toNamespacedPath(
path.resolve(
${JSON.stringify(fixtextureFolderEscaped)},
${JSON.stringify(folder)},
${JSON.stringify(expectedFile)},
)
)
});
`,
]
);
assert.strictEqual(status, 0, stderr.toString());
}
});
});

View File

@ -0,0 +1,177 @@
'use strict';
// Flags: --expose-internals
require('../common');
const { describe, it } = require('node:test');
const path = require('node:path');
const assert = require('node:assert');
const { pathToFileURL } = require('node:url');
const { legacyMainResolve } = require('node:internal/modules/esm/resolve');
const fixtures = require('../common/fixtures.js');
describe('legacyMainResolve', () => {
it('should resolve using packageConfig.main', () => {
const packageJsonUrl = pathToFileURL(
path.resolve(
fixtures.path('/es-modules/legacy-main-resolver'),
'package.json'
)
);
const paths = [
['./index-js/index.js', './index-js/index.js'],
['./index-js/index', './index-js/index.js'],
['./index-json/index', './index-json/index.json'],
['./index-node/index', './index-node/index.node'],
['./index-js', './index-js/index.js'],
['./index-json', './index-json/index.json'],
['./index-node', './index-node/index.node'],
];
for (const [main, expected] of paths) {
const packageConfig = { main };
const base = path.resolve(
fixtures.path('/es-modules/legacy-main-resolver')
);
assert.strictEqual(
legacyMainResolve(packageJsonUrl, packageConfig, base).href,
pathToFileURL(path.join(base, expected)).href
);
}
});
it('should resolve using packageJsonUrl', () => {
const paths = [
['index-js', './index-js/index.js'],
['index-json', './index-json/index.json'],
['index-node', './index-node/index.node'],
];
for (const [folder, expected] of paths) {
const packageJsonUrl = pathToFileURL(
path.resolve(
fixtures.path('/es-modules/legacy-main-resolver'),
folder,
'package.json'
)
);
const packageConfig = { main: undefined };
const base = path.resolve(
fixtures.path('/es-modules/legacy-main-resolver')
);
assert.strictEqual(
legacyMainResolve(packageJsonUrl, packageConfig, base).href,
pathToFileURL(path.join(base, expected)).href
);
}
});
it('should throw when packageJsonUrl is not URL', () => {
assert.throws(
() =>
legacyMainResolve(
path.resolve(
fixtures.path('/es-modules/legacy-main-resolver/index-node'),
'package.json'
),
{},
''
),
{ code: 'ERR_INTERNAL_ASSERTION' },
);
});
it('should throw when packageConfigMain is invalid URL', () => {
assert.throws(
() =>
legacyMainResolve(
pathToFileURL(
path.resolve(
// Is invalid because this path does not point to a file
fixtures.path('/es-modules/legacy-main-resolver/index-node')
)
),
{ main: './invalid/index.js' },
''
),
{ code: 'ERR_INVALID_URL' },
);
});
it('should throw when packageJsonUrl is invalid URL', () => {
assert.throws(
() =>
legacyMainResolve(
pathToFileURL(
path.resolve(
// Is invalid because this path does not point to a file
fixtures.path('/es-modules/legacy-main-resolver/index-node')
)
),
{ main: undefined },
''
),
{ code: 'ERR_INVALID_URL' },
);
});
it('should throw when cannot resolve to a file', () => {
const packageJsonUrl = pathToFileURL(
path.resolve(
fixtures.path('/es-modules/legacy-main-resolver'),
'package.json'
)
);
assert.throws(
() => legacyMainResolve(packageJsonUrl, { main: null }, packageJsonUrl),
{ message: /index\.js/, code: 'ERR_MODULE_NOT_FOUND' },
);
});
it('should not crash when cannot resolve to a file that contains special chars', () => {
const packageJsonUrl = pathToFileURL('/c/file%20with%20percents/package.json');
assert.throws(
() => legacyMainResolve(packageJsonUrl, { main: null }, packageJsonUrl),
{ message: /index\.js/, code: 'ERR_MODULE_NOT_FOUND' },
);
});
it('should report main file on error message when not found', () => {
const packageJsonUrl = pathToFileURL(
path.resolve(
fixtures.path('/es-modules/legacy-main-resolver'),
'package.json'
)
);
assert.throws(
() => legacyMainResolve(packageJsonUrl, { main: './index.node' }, packageJsonUrl),
{ message: /index\.node/, code: 'ERR_MODULE_NOT_FOUND' },
);
});
it('should throw when cannot resolve to a file (base not defined)', () => {
const packageJsonUrl = pathToFileURL(
path.resolve(
fixtures.path('/es-modules/legacy-main-resolver'),
'package.json'
)
);
assert.throws(
() => legacyMainResolve(packageJsonUrl, { main: null }, undefined),
{ message: /"base" argument must be/, code: 'ERR_INVALID_ARG_TYPE' },
);
});
it('should interpret main as a path, not a URL', () => {
const packageJsonUrl = fixtures.fileURL('/es-modules/legacy-main-resolver/package.json');
assert.deepStrictEqual(
legacyMainResolve(packageJsonUrl, { main: '../folder%25with percentage#/' }, packageJsonUrl),
fixtures.fileURL('/es-modules/folder%25with percentage#/index.js'),
);
});
});

View File

@ -0,0 +1,12 @@
'use strict';
const { mustNotCall, mustCall } = require('../common');
Object.defineProperties(Object.prototype, {
then: {
set: mustNotCall('set %Object.prototype%.then'),
get: mustNotCall('get %Object.prototype%.then'),
},
});
import('data:text/javascript,').then(mustCall());

View File

@ -0,0 +1,19 @@
// Flags: --no-experimental-require-module
'use strict';
// Tests that --experimental-require-module is not implied by --experimental-detect-module
// and is checked independently.
require('../common');
const assert = require('assert');
// Check that require() still throws SyntaxError for an ambiguous module that's detected to be ESM.
// TODO(joyeecheung): now that --experimental-detect-module is unflagged, it makes more sense
// to either throw ERR_REQUIRE_ESM for require() of detected ESM instead, or add a hint about the
// use of require(esm) to the SyntaxError.
assert.throws(
() => require('../fixtures/es-modules/loose.js'),
{
name: 'SyntaxError',
message: /Unexpected token 'export'/
});

View File

@ -0,0 +1,32 @@
// Flags: --expose-gc --experimental-vm-modules
'use strict';
// This tests that vm.Script would not get GC'ed while the script can still
// initiate dynamic import.
// See https://github.com/nodejs/node/issues/43205.
require('../common');
const vm = require('vm');
const code = `
new Promise(resolve => {
setTimeout(() => {
gc(); // vm.Script should not be GC'ed while the script is alive.
resolve();
}, 1);
}).then(() => import('foo'));`;
// vm.runInThisContext creates a vm.Script underneath, which should not be GC'ed
// while import() can still be initiated.
vm.runInThisContext(code, {
async importModuleDynamically() {
const m = new vm.SyntheticModule(['bar'], () => {
m.setExport('bar', 1);
});
await m.link(() => {});
await m.evaluate();
return m;
}
});

View File

@ -0,0 +1,5 @@
import '../common/index.mjs';
import assert, { strict } from 'assert';
import assertStrict from 'assert/strict';
assert.strictEqual(strict, assertStrict);

View File

@ -0,0 +1,81 @@
// Flags: --experimental-loader ./test/fixtures/es-module-loaders/assertionless-json-import.mjs
'use strict';
const common = require('../common');
const { strictEqual } = require('assert');
async function test() {
{
const [secret0, secret1] = await Promise.all([
import('../fixtures/experimental.json'),
import(
'../fixtures/experimental.json',
{ with: { type: 'json' } }
),
]);
strictEqual(secret0.default.ofLife, 42);
strictEqual(secret1.default.ofLife, 42);
strictEqual(secret0.default, secret1.default);
strictEqual(secret0, secret1);
}
{
const [secret0, secret1] = await Promise.all([
import('../fixtures/experimental.json?test'),
import(
'../fixtures/experimental.json?test',
{ with: { type: 'json' } }
),
]);
strictEqual(secret0.default.ofLife, 42);
strictEqual(secret1.default.ofLife, 42);
strictEqual(secret0.default, secret1.default);
strictEqual(secret0, secret1);
}
{
const [secret0, secret1] = await Promise.all([
import('../fixtures/experimental.json#test'),
import(
'../fixtures/experimental.json#test',
{ with: { type: 'json' } }
),
]);
strictEqual(secret0.default.ofLife, 42);
strictEqual(secret1.default.ofLife, 42);
strictEqual(secret0.default, secret1.default);
strictEqual(secret0, secret1);
}
{
const [secret0, secret1] = await Promise.all([
import('../fixtures/experimental.json?test2#test'),
import(
'../fixtures/experimental.json?test2#test',
{ with: { type: 'json' } }
),
]);
strictEqual(secret0.default.ofLife, 42);
strictEqual(secret1.default.ofLife, 42);
strictEqual(secret0.default, secret1.default);
strictEqual(secret0, secret1);
}
{
const [secret0, secret1] = await Promise.all([
import('data:application/json,{"ofLife":42}'),
import(
'data:application/json,{"ofLife":42}',
{ with: { type: 'json' } }
),
]);
strictEqual(secret0.default.ofLife, 42);
strictEqual(secret1.default.ofLife, 42);
}
}
test().then(common.mustCall());

View File

@ -0,0 +1,10 @@
import '../common/index.mjs';
import assert from 'assert';
import ok from '../fixtures/es-modules/test-esm-ok.mjs';
import * as okShebangNs from './test-esm-shebang.mjs';
// encode the '.'
import * as okShebangPercentNs from './test-esm-shebang%2emjs';
assert(ok);
assert(okShebangNs.default);
assert.strict.equal(okShebangNs, okShebangPercentNs);

View File

@ -0,0 +1,20 @@
import '../common/index.mjs';
import assert from 'assert';
import { fork } from 'child_process';
import { once } from 'events';
import { fileURLToPath } from 'url';
if (process.argv[2] !== 'child') {
const filename = fileURLToPath(import.meta.url);
const cp = fork(filename, ['child']);
const message = 'Hello World';
cp.send(message);
const [received] = await once(cp, 'message');
assert.deepStrictEqual(received, message);
cp.disconnect();
await once(cp, 'exit');
} else {
process.on('message', (msg) => process.send(msg));
}

View File

@ -0,0 +1,20 @@
'use strict';
const { spawnPromisified } = require('../common');
const fixtures = require('../common/fixtures.js');
const assert = require('node:assert');
const { execPath } = require('node:process');
const { describe, it } = require('node:test');
const entry = fixtures.path('/es-modules/builtin-imports-case.mjs');
describe('ESM: importing builtins & CJS', () => {
it('should work', async () => {
const { code, signal, stdout } = await spawnPromisified(execPath, [entry]);
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
assert.strictEqual(stdout, 'ok\n');
});
});

View File

@ -0,0 +1,28 @@
'use strict';
const { spawnPromisified } = require('../common');
const fixtures = require('../common/fixtures.js');
const assert = require('node:assert');
const { execPath } = require('node:process');
const { describe, it } = require('node:test');
describe('ESM: importing CJS', { concurrency: !process.env.TEST_PARALLEL }, () => {
it('should support valid CJS exports', async () => {
const validEntry = fixtures.path('/es-modules/cjs-exports.mjs');
const { code, signal, stdout } = await spawnPromisified(execPath, [validEntry]);
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
assert.strictEqual(stdout, 'ok\n');
});
it('should error on invalid CJS exports', async () => {
const invalidEntry = fixtures.path('/es-modules/cjs-exports-invalid.mjs');
const { code, signal, stderr } = await spawnPromisified(execPath, [invalidEntry]);
assert.match(stderr, /SyntaxError: The requested module '\.\/invalid-cjs\.js' does not provide an export named 'default'/);
assert.strictEqual(code, 1);
assert.strictEqual(signal, null);
});
});

View File

@ -0,0 +1,95 @@
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';
// Expect note to be included in the error output
// Don't match the following sentence because it can change as features are
// added.
const expectedNote = 'Failed to load the ES module';
const mustIncludeMessage = {
getMessage: (stderr) => `${expectedNote} not found in ${stderr}`,
includeNote: true,
};
const mustNotIncludeMessage = {
getMessage: (stderr) => `${expectedNote} must not be included in ${stderr}`,
includeNote: false,
};
describe('ESM: Errors for unexpected exports', { concurrency: !process.env.TEST_PARALLEL }, () => {
for (
const { errorNeedle, filePath, getMessage, includeNote }
of [
{
// name: '',
filePath: fixtures.path('/es-modules/es-note-unexpected-export-1.cjs'),
...mustIncludeMessage,
},
{
// name: '',
filePath: fixtures.path('/es-modules/es-note-unexpected-import-1.cjs'),
...mustIncludeMessage,
},
{
// name: '',
filePath: fixtures.path('/es-modules/es-note-promiserej-import-2.cjs'),
...mustNotIncludeMessage,
},
{
// name: '',
filePath: fixtures.path('/es-modules/es-note-unexpected-import-3.cjs'),
...mustIncludeMessage,
},
{
// name: '',
filePath: fixtures.path('/es-modules/es-note-unexpected-import-4.cjs'),
...mustIncludeMessage,
},
{
// name: '',
filePath: fixtures.path('/es-modules/es-note-unexpected-import-5.cjs'),
...mustNotIncludeMessage,
},
{
// name: '',
filePath: fixtures.path('/es-modules/es-note-error-1.mjs'),
...mustNotIncludeMessage,
errorNeedle: /Error: some error/,
},
{
// name: '',
filePath: fixtures.path('/es-modules/es-note-error-2.mjs'),
...mustNotIncludeMessage,
errorNeedle: /string/,
},
{
// name: '',
filePath: fixtures.path('/es-modules/es-note-error-3.mjs'),
...mustNotIncludeMessage,
errorNeedle: /null/,
},
{
// name: '',
filePath: fixtures.path('/es-modules/es-note-error-4.mjs'),
...mustNotIncludeMessage,
errorNeedle: /undefined/,
},
]
) {
it(`should ${includeNote ? '' : 'NOT'} include note`, async () => {
const { code, stderr } = await spawnPromisified(execPath, [filePath]);
assert.strictEqual(code, 1);
if (errorNeedle != null) assert.match(stderr, errorNeedle);
const shouldIncludeNote = stderr.includes(expectedNote);
assert.ok(
includeNote ? shouldIncludeNote : !shouldIncludeNote,
`${filePath} ${getMessage(stderr)}`,
);
});
}
});

View File

@ -0,0 +1,20 @@
'use strict';
const { spawnPromisified } = require('../common');
const fixtures = require('../common/fixtures.js');
const assert = require('node:assert');
const { execPath } = require('node:process');
const { describe, it } = require('node:test');
describe('ESM: importing CJS', () => {
it('should work', async () => {
const { code, signal, stdout } = await spawnPromisified(execPath, [
fixtures.path('/es-modules/cjs.js'),
]);
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
assert.strictEqual(stdout, 'executed\n');
});
});

View File

@ -0,0 +1,77 @@
import '../common/index.mjs';
import { rejects } from 'assert';
const fixtureBase = '../fixtures/es-modules/package-cjs-named-error';
const errTemplate = (specifier, name, namedImports) =>
`Named export '${name}' not found. The requested module` +
` '${specifier}' is a CommonJS module, which may not support ` +
'all module.exports as named exports.\nCommonJS modules can ' +
'always be imported via the default export, for example using:' +
`\n\nimport pkg from '${specifier}';\n` + (namedImports ?
`const ${namedImports} = pkg;\n` : '');
const expectedWithoutExample = errTemplate('./fail.cjs', 'comeOn');
const expectedRelative = errTemplate('./fail.cjs', 'comeOn', '{ comeOn }');
const expectedRenamed = errTemplate('./fail.cjs', 'comeOn',
'{ comeOn: comeOnRenamed }');
const expectedPackageHack =
errTemplate('./json-hack/fail.js', 'comeOn', '{ comeOn }');
const expectedBare = errTemplate('deep-fail', 'comeOn', '{ comeOn }');
await rejects(async () => {
await import(`${fixtureBase}/single-quote.mjs`);
}, {
name: 'SyntaxError',
message: expectedRelative
}, 'should support relative specifiers with single quotes');
await rejects(async () => {
await import(`${fixtureBase}/double-quote.mjs`);
}, {
name: 'SyntaxError',
message: expectedRelative
}, 'should support relative specifiers with double quotes');
await rejects(async () => {
await import(`${fixtureBase}/renamed-import.mjs`);
}, {
name: 'SyntaxError',
message: expectedRenamed
}, 'should correctly format named imports with renames');
await rejects(async () => {
await import(`${fixtureBase}/multi-line.mjs`);
}, {
name: 'SyntaxError',
message: expectedWithoutExample,
}, 'should correctly format named imports across multiple lines');
await rejects(async () => {
await import(`${fixtureBase}/json-hack.mjs`);
}, {
name: 'SyntaxError',
message: expectedPackageHack
}, 'should respect recursive package.json for module type');
await rejects(async () => {
await import(`${fixtureBase}/bare-import-single.mjs`);
}, {
name: 'SyntaxError',
message: expectedBare
}, 'should support bare specifiers with single quotes');
await rejects(async () => {
await import(`${fixtureBase}/bare-import-double.mjs`);
}, {
name: 'SyntaxError',
message: expectedBare
}, 'should support bare specifiers with double quotes');
await rejects(async () => {
await import(`${fixtureBase}/escaped-single-quote.mjs`);
}, /import pkg from '\.\/oh'no\.cjs'/, 'should support relative specifiers with escaped single quote');

View File

@ -0,0 +1,10 @@
// Flags: --conditions=custom-condition -C another
import { mustCall } from '../common/index.mjs';
import { strictEqual } from 'assert';
import { requireFixture, importFixture } from '../fixtures/pkgexports.mjs';
[requireFixture, importFixture].forEach((loadFixture) => {
loadFixture('pkgexports/condition')
.then(mustCall((actual) => {
strictEqual(actual.default, 'from custom condition');
}));
});

View File

@ -0,0 +1,2 @@
import '../common/index.mjs';
import('./test-esm-cyclic-dynamic-import.mjs');

View File

@ -0,0 +1,114 @@
'use strict';
const common = require('../common');
const fixtures = require('../common/fixtures');
const assert = require('assert');
function createURL(mime, body) {
return `data:${mime},${body}`;
}
function createBase64URL(mime, body) {
return `data:${mime};base64,${Buffer.from(body).toString('base64')}`;
}
(async () => {
{
const body = 'export default {a:"aaa"};';
const plainESMURL = createURL('text/javascript', body);
const ns = await import(plainESMURL);
assert.deepStrictEqual(Object.keys(ns), ['default']);
assert.strictEqual(ns.default.a, 'aaa');
const importerOfURL = createURL(
'text/javascript',
`export {default as default} from ${JSON.stringify(plainESMURL)}`
);
assert.strictEqual(
(await import(importerOfURL)).default,
ns.default
);
const base64ESMURL = createBase64URL('text/javascript', body);
assert.notStrictEqual(
await import(base64ESMURL),
ns
);
}
{
const body = 'export default import.meta.url;';
const plainESMURL = createURL('text/javascript', body);
const ns = await import(plainESMURL);
assert.deepStrictEqual(Object.keys(ns), ['default']);
assert.strictEqual(ns.default, plainESMURL);
}
{
const body = 'export default import.meta.url;';
const plainESMURL = createURL('text/javascript;charset=UTF-8', body);
const ns = await import(plainESMURL);
assert.deepStrictEqual(Object.keys(ns), ['default']);
assert.strictEqual(ns.default, plainESMURL);
}
{
const body = 'export default import.meta.url;';
const plainESMURL = createURL('text/javascript;charset="UTF-8"', body);
const ns = await import(plainESMURL);
assert.deepStrictEqual(Object.keys(ns), ['default']);
assert.strictEqual(ns.default, plainESMURL);
}
{
const body = 'export default import.meta.url;';
const plainESMURL = createURL('text/javascript;;a=a;b=b;;', body);
const ns = await import(plainESMURL);
assert.deepStrictEqual(Object.keys(ns), ['default']);
assert.strictEqual(ns.default, plainESMURL);
}
{
const ns = await import('data:application/json;foo="test,"this"',
{ with: { type: 'json' } });
assert.deepStrictEqual(Object.keys(ns), ['default']);
assert.strictEqual(ns.default, 'this');
}
{
const ns = await import(`data:application/json;foo=${
encodeURIComponent('test,')
},0`, { with: { type: 'json' } });
assert.deepStrictEqual(Object.keys(ns), ['default']);
assert.strictEqual(ns.default, 0);
}
{
await assert.rejects(async () =>
import('data:application/json;foo="test,",0',
{ with: { type: 'json' } }), {
name: 'SyntaxError',
message: /Unterminated string in JSON at position 3/
});
}
{
const body = '{"x": 1}';
const plainESMURL = createURL('application/json', body);
const ns = await import(plainESMURL, { with: { type: 'json' } });
assert.deepStrictEqual(Object.keys(ns), ['default']);
assert.strictEqual(ns.default.x, 1);
}
{
const body = '{"default": 2}';
const plainESMURL = createURL('application/json', body);
const ns = await import(plainESMURL, { with: { type: 'json' } });
assert.deepStrictEqual(Object.keys(ns), ['default']);
assert.strictEqual(ns.default.default, 2);
}
{
const body = 'null';
const plainESMURL = createURL('invalid', body);
await assert.rejects(import(plainESMURL), { code: 'ERR_UNKNOWN_MODULE_FORMAT' });
}
{
const plainESMURL = 'data:text/javascript,export%20default%202';
const module = await import(plainESMURL);
assert.strictEqual(module.default, 2);
}
{
const plainESMURL = `data:text/javascript,${encodeURIComponent(`import ${JSON.stringify(fixtures.fileURL('es-module-url', 'empty.js'))}`)}`;
await import(plainESMURL);
}
{
const plainESMURL = 'data:text/javascript,var x = "hello world?"';
await import(plainESMURL);
}
})().then(common.mustCall());

View File

@ -0,0 +1,7 @@
import '../common/index.mjs';
import { strictEqual } from 'assert';
import asdf from
'../fixtures/es-modules/package-type-module/nested-default-type/module.js';
strictEqual(asdf, 'asdf');

View File

@ -0,0 +1,425 @@
import { spawnPromisified } from '../common/index.mjs';
import * as fixtures from '../common/fixtures.mjs';
import { spawn } from 'node:child_process';
import { describe, it } from 'node:test';
import { strictEqual, match } from 'node:assert';
describe('Module syntax detection', { concurrency: !process.env.TEST_PARALLEL }, () => {
describe('string input', { concurrency: !process.env.TEST_PARALLEL }, () => {
it('permits ESM syntax in --eval input without requiring --input-type=module', async () => {
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
'--eval',
'import { version } from "node:process"; console.log(version);',
]);
strictEqual(stderr, '');
strictEqual(stdout, `${process.version}\n`);
strictEqual(code, 0);
strictEqual(signal, null);
});
// ESM is unsupported for --print via --input-type=module
it('permits ESM syntax in STDIN input without requiring --input-type=module', async () => {
const child = spawn(process.execPath, []);
child.stdin.end('console.log(typeof import.meta.resolve)');
match((await child.stdout.toArray()).toString(), /^function\r?\n$/);
});
it('should be overridden by --input-type', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(process.execPath, [
'--input-type=commonjs',
'--eval',
'import.meta.url',
]);
match(stderr, /SyntaxError: Cannot use 'import\.meta' outside a module/);
strictEqual(stdout, '');
strictEqual(code, 1);
strictEqual(signal, null);
});
it('should not switch to module if code is parsable as script', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(process.execPath, [
'--eval',
'let __filename,__dirname,require,module,exports;this.a',
]);
strictEqual(stderr, '');
strictEqual(stdout, '');
strictEqual(code, 0);
strictEqual(signal, null);
});
it('does not trigger detection via source code `eval()`', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(process.execPath, [
'--eval',
'eval("import \'nonexistent\';");',
]);
match(stderr, /SyntaxError: Cannot use import statement outside a module/);
strictEqual(stdout, '');
strictEqual(code, 1);
strictEqual(signal, null);
});
});
describe('.js file input in a typeless package', { concurrency: !process.env.TEST_PARALLEL }, () => {
for (const { testName, entryPath } of [
{
testName: 'permits CommonJS syntax in a .js entry point',
entryPath: fixtures.path('es-modules/package-without-type/commonjs.js'),
},
{
testName: 'permits ESM syntax in a .js entry point',
entryPath: fixtures.path('es-modules/package-without-type/module.js'),
},
{
testName: 'permits CommonJS syntax in a .js file imported by a CommonJS entry point',
entryPath: fixtures.path('es-modules/package-without-type/imports-commonjs.cjs'),
},
{
testName: 'permits ESM syntax in a .js file imported by a CommonJS entry point',
entryPath: fixtures.path('es-modules/package-without-type/imports-esm.js'),
},
{
testName: 'permits CommonJS syntax in a .js file imported by an ESM entry point',
entryPath: fixtures.path('es-modules/package-without-type/imports-commonjs.mjs'),
},
{
testName: 'permits ESM syntax in a .js file imported by an ESM entry point',
entryPath: fixtures.path('es-modules/package-without-type/imports-esm.mjs'),
},
]) {
it(testName, async () => {
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
'--no-warnings',
entryPath,
]);
strictEqual(stderr, '');
strictEqual(stdout, 'executed\n');
strictEqual(code, 0);
strictEqual(signal, null);
});
}
});
describe('extensionless file input in a typeless package', { concurrency: !process.env.TEST_PARALLEL }, () => {
for (const { testName, entryPath } of [
{
testName: 'permits CommonJS syntax in an extensionless entry point',
entryPath: fixtures.path('es-modules/package-without-type/noext-cjs'),
},
{
testName: 'permits ESM syntax in an extensionless entry point',
entryPath: fixtures.path('es-modules/package-without-type/noext-esm'),
},
{
testName: 'permits CommonJS syntax in an extensionless file imported by a CommonJS entry point',
entryPath: fixtures.path('es-modules/package-without-type/imports-noext-cjs.js'),
},
{
testName: 'permits ESM syntax in an extensionless file imported by a CommonJS entry point',
entryPath: fixtures.path('es-modules/package-without-type/imports-noext-esm.js'),
},
{
testName: 'permits CommonJS syntax in an extensionless file imported by an ESM entry point',
entryPath: fixtures.path('es-modules/package-without-type/imports-noext-cjs.mjs'),
},
{
testName: 'permits ESM syntax in an extensionless file imported by an ESM entry point',
entryPath: fixtures.path('es-modules/package-without-type/imports-noext-esm.mjs'),
},
]) {
it(testName, async () => {
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
'--no-warnings',
entryPath,
]);
strictEqual(stderr, '');
strictEqual(stdout, 'executed\n');
strictEqual(code, 0);
strictEqual(signal, null);
});
}
it('should not hint wrong format in resolve hook', async () => {
let writeSync;
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
'--no-warnings',
'--loader',
`data:text/javascript,import { writeSync } from "node:fs"; export ${encodeURIComponent(
async function resolve(s, c, next) {
const result = await next(s, c);
writeSync(1, result.format + '\n');
return result;
}
)}`,
fixtures.path('es-modules/package-without-type/noext-esm'),
]);
strictEqual(stderr, '');
strictEqual(stdout, 'null\nexecuted\n');
strictEqual(code, 0);
strictEqual(signal, null);
});
});
describe('file input in a "type": "commonjs" package', { concurrency: !process.env.TEST_PARALLEL }, () => {
for (const { testName, entryPath } of [
{
testName: 'disallows ESM syntax in a .js entry point',
entryPath: fixtures.path('es-modules/package-type-commonjs/module.js'),
},
{
testName: 'disallows ESM syntax in a .js file imported by a CommonJS entry point',
entryPath: fixtures.path('es-modules/package-type-commonjs/imports-esm.js'),
},
{
testName: 'disallows ESM syntax in a .js file imported by an ESM entry point',
entryPath: fixtures.path('es-modules/package-type-commonjs/imports-esm.mjs'),
},
]) {
it(testName, async () => {
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
entryPath,
]);
match(stderr, /SyntaxError: Unexpected token 'export'/);
strictEqual(stdout, '');
strictEqual(code, 1);
strictEqual(signal, null);
});
}
});
describe('file input in a "type": "module" package', { concurrency: !process.env.TEST_PARALLEL }, () => {
for (const { testName, entryPath } of [
{
testName: 'disallows CommonJS syntax in a .js entry point',
entryPath: fixtures.path('es-modules/package-type-module/cjs.js'),
},
{
testName: 'disallows CommonJS syntax in a .js file imported by a CommonJS entry point',
entryPath: fixtures.path('es-modules/package-type-module/imports-commonjs.cjs'),
},
{
testName: 'disallows CommonJS syntax in a .js file imported by an ESM entry point',
entryPath: fixtures.path('es-modules/package-type-module/imports-commonjs.mjs'),
},
]) {
it(testName, async () => {
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
entryPath,
]);
match(stderr, /ReferenceError: module is not defined in ES module scope/);
strictEqual(stdout, '');
strictEqual(code, 1);
strictEqual(signal, null);
});
}
});
// https://github.com/nodejs/node/issues/50917
describe('syntax that errors in CommonJS but works in ESM', { concurrency: !process.env.TEST_PARALLEL }, () => {
it('permits top-level `await`', async () => {
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
'--eval',
'await Promise.resolve(); console.log("executed");',
]);
strictEqual(stderr, '');
strictEqual(stdout, 'executed\n');
strictEqual(code, 0);
strictEqual(signal, null);
});
it('reports unfinished top-level `await`', async () => {
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
'--no-warnings',
fixtures.path('es-modules/tla/unresolved.js'),
]);
strictEqual(stderr, '');
strictEqual(stdout, '');
strictEqual(code, 13);
strictEqual(signal, null);
});
it('permits top-level `await` above import/export syntax', async () => {
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
'--eval',
'await Promise.resolve(); import "node:os"; console.log("executed");',
]);
strictEqual(stderr, '');
strictEqual(stdout, 'executed\n');
strictEqual(code, 0);
strictEqual(signal, null);
});
it('still throws on `await` in an ordinary sync function', async () => {
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
'--eval',
'function fn() { await Promise.resolve(); } fn();',
]);
match(stderr, /SyntaxError: await is only valid in async function/);
strictEqual(stdout, '');
strictEqual(code, 1);
strictEqual(signal, null);
});
it('throws on undefined `require` when top-level `await` triggers ESM parsing', async () => {
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
'--eval',
'const fs = require("node:fs"); await Promise.resolve();',
]);
match(stderr, /ReferenceError: require is not defined in ES module scope/);
strictEqual(stdout, '');
strictEqual(code, 1);
strictEqual(signal, null);
});
it('permits declaration of CommonJS module variables', async () => {
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
'--no-warnings',
fixtures.path('es-modules/package-without-type/commonjs-wrapper-variables.js'),
]);
strictEqual(stderr, '');
strictEqual(stdout, 'exports require module __filename __dirname\n');
strictEqual(code, 0);
strictEqual(signal, null);
});
it('permits declaration of CommonJS module variables above import/export', async () => {
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
'--eval',
'const module = 3; import "node:os"; console.log("executed");',
]);
strictEqual(stderr, '');
strictEqual(stdout, 'executed\n');
strictEqual(code, 0);
strictEqual(signal, null);
});
it('still throws on double `const` declaration not at the top level', async () => {
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
'--eval',
'function fn() { const require = 1; const require = 2; } fn();',
]);
match(stderr, /SyntaxError: Identifier 'require' has already been declared/);
strictEqual(stdout, '');
strictEqual(code, 1);
strictEqual(signal, null);
});
});
describe('warn about typeless packages for .js files with ESM syntax', { concurrency: true }, () => {
for (const { testName, entryPath } of [
{
testName: 'warns for ESM syntax in a .js entry point in a typeless package',
entryPath: fixtures.path('es-modules/package-without-type/module.js'),
},
{
testName: 'warns for ESM syntax in a .js file imported by a CommonJS entry point in a typeless package',
entryPath: fixtures.path('es-modules/package-without-type/imports-esm.js'),
},
{
testName: 'warns for ESM syntax in a .js file imported by an ESM entry point in a typeless package',
entryPath: fixtures.path('es-modules/package-without-type/imports-esm.mjs'),
},
]) {
it(testName, async () => {
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
entryPath,
]);
match(stderr, /MODULE_TYPELESS_PACKAGE_JSON/);
strictEqual(stdout, 'executed\n');
strictEqual(code, 0);
strictEqual(signal, null);
});
}
it('does not warn when there are no package.json', async () => {
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
fixtures.path('es-modules/loose.js'),
]);
strictEqual(stderr, '');
strictEqual(stdout, 'executed\n');
strictEqual(code, 0);
strictEqual(signal, null);
});
it('warns only once for a package.json that affects multiple files', async () => {
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
fixtures.path('es-modules/package-without-type/detected-as-esm.js'),
]);
match(stderr, /MODULE_TYPELESS_PACKAGE_JSON/);
strictEqual(stderr.match(/MODULE_TYPELESS_PACKAGE_JSON/g).length, 1);
strictEqual(stdout, 'executed\nexecuted\n');
strictEqual(code, 0);
strictEqual(signal, null);
});
it('can be disabled via --no-experimental-detect-module', async () => {
const { stdout, stderr, code, signal } = await spawnPromisified(process.execPath, [
'--no-experimental-detect-module',
fixtures.path('es-modules/package-without-type/module.js'),
]);
match(stderr, /SyntaxError: Unexpected token 'export'/);
strictEqual(stdout, '');
strictEqual(code, 1);
strictEqual(signal, null);
});
});
});
// Checks the error caught during module detection does not trigger abort when
// `--abort-on-uncaught-exception` is passed in (as that's a caught internal error).
// Ref: https://github.com/nodejs/node/issues/50878
describe('Wrapping a `require` of an ES module while using `--abort-on-uncaught-exception`', () => {
it('should work', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(process.execPath, [
'--abort-on-uncaught-exception',
'--no-warnings',
'--eval',
'require("./package-type-module/esm.js")',
], {
cwd: fixtures.path('es-modules'),
});
strictEqual(stderr, '');
strictEqual(stdout, '');
strictEqual(code, 0);
strictEqual(signal, null);
});
});
describe('when working with Worker threads', () => {
it('should support sloppy scripts that declare CJS "global-like" variables', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(process.execPath, [
'--eval',
'new worker_threads.Worker("let __filename,__dirname,require,module,exports;this.a",{eval:true})',
]);
strictEqual(stderr, '');
strictEqual(stdout, '');
strictEqual(code, 0);
strictEqual(signal, null);
});
});

View File

@ -0,0 +1,14 @@
// Flags: --expose-internals
import '../common/index.mjs';
import assert from 'assert';
import { lookupService } from 'dns/promises';
const invalidAddress = 'fasdfdsaf';
assert.throws(() => {
lookupService(invalidAddress, 0);
}, {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
message: `The argument 'address' is invalid. Received '${invalidAddress}'`
});

View File

@ -0,0 +1,5 @@
import '../common/index.mjs';
// Assert we can import files with `%` in their pathname.
import '../fixtures/es-modules/test-esm-double-encoding-native%2520.mjs';

View File

@ -0,0 +1,47 @@
'use strict';
const common = require('../common');
const { strictEqual } = require('assert');
async function test() {
{
const results = await Promise.allSettled([
import('../fixtures/empty.js', { with: { type: 'json' } }),
import('../fixtures/empty.js'),
]);
strictEqual(results[0].status, 'rejected');
strictEqual(results[1].status, 'fulfilled');
}
{
const results = await Promise.allSettled([
import('../fixtures/empty.js'),
import('../fixtures/empty.js', { with: { type: 'json' } }),
]);
strictEqual(results[0].status, 'fulfilled');
strictEqual(results[1].status, 'rejected');
}
{
const results = await Promise.allSettled([
import('../fixtures/empty.json', { with: { type: 'json' } }),
import('../fixtures/empty.json'),
]);
strictEqual(results[0].status, 'fulfilled');
strictEqual(results[1].status, 'rejected');
}
{
const results = await Promise.allSettled([
import('../fixtures/empty.json'),
import('../fixtures/empty.json', { with: { type: 'json' } }),
]);
strictEqual(results[0].status, 'rejected');
strictEqual(results[1].status, 'fulfilled');
}
}
test().then(common.mustCall());

View File

@ -0,0 +1,42 @@
import '../common/index.mjs';
import { strictEqual } from 'assert';
{
const results = await Promise.allSettled([
import('../fixtures/empty.js', { with: { type: 'json' } }),
import('../fixtures/empty.js'),
]);
strictEqual(results[0].status, 'rejected');
strictEqual(results[1].status, 'fulfilled');
}
{
const results = await Promise.allSettled([
import('../fixtures/empty.js'),
import('../fixtures/empty.js', { with: { type: 'json' } }),
]);
strictEqual(results[0].status, 'fulfilled');
strictEqual(results[1].status, 'rejected');
}
{
const results = await Promise.allSettled([
import('../fixtures/empty.json', { with: { type: 'json' } }),
import('../fixtures/empty.json'),
]);
strictEqual(results[0].status, 'fulfilled');
strictEqual(results[1].status, 'rejected');
}
{
const results = await Promise.allSettled([
import('../fixtures/empty.json'),
import('../fixtures/empty.json', { with: { type: 'json' } }),
]);
strictEqual(results[0].status, 'rejected');
strictEqual(results[1].status, 'fulfilled');
}

View File

@ -0,0 +1,19 @@
'use strict';
const common = require('../common');
const fixtures = require('../common/fixtures');
const assert = require('node:assert');
(async () => {
// Make sure that the CommonJS module lexer has been initialized.
// See https://github.com/nodejs/node/blob/v21.1.0/lib/internal/modules/esm/translators.js#L61-L81.
await import(fixtures.fileURL('empty.js'));
let tickDuringCJSImport = false;
process.nextTick(() => { tickDuringCJSImport = true; });
await import(fixtures.fileURL('empty.cjs'));
assert(!tickDuringCJSImport);
})().then(common.mustCall());

View File

@ -0,0 +1,9 @@
import '../common/index.mjs';
import fixtures from '../common/fixtures.js';
import assert from 'node:assert';
let tickDuringCJSImport = false;
process.nextTick(() => { tickDuringCJSImport = true; });
await import(fixtures.fileURL('empty.cjs'));
assert(!tickDuringCJSImport);

View File

@ -0,0 +1,22 @@
'use strict';
const common = require('../common');
const tmpdir = require('../common/tmpdir');
const assert = require('node:assert');
const fs = require('node:fs/promises');
tmpdir.refresh();
const target = tmpdir.fileURL(`${Math.random()}.mjs`);
(async () => {
await assert.rejects(import(target), { code: 'ERR_MODULE_NOT_FOUND' });
await fs.writeFile(target, 'export default "actual target"\n');
const moduleRecord = await import(target);
await fs.rm(target);
assert.strictEqual(await import(target), moduleRecord);
})().then(common.mustCall());

View File

@ -0,0 +1,39 @@
import { spawnPromisified } from '../common/index.mjs';
import tmpdir from '../common/tmpdir.js';
import assert from 'node:assert';
import fs from 'node:fs/promises';
import { execPath } from 'node:process';
tmpdir.refresh();
const target = tmpdir.fileURL(`${Math.random()}.mjs`);
await assert.rejects(import(target), { code: 'ERR_MODULE_NOT_FOUND' });
await fs.writeFile(target, 'export default "actual target"\n');
const moduleRecord = await import(target);
await fs.rm(target);
assert.strictEqual(await import(target), moduleRecord);
// Add the file back, it should be deleted by the subprocess.
await fs.writeFile(target, 'export default "actual target"\n');
assert.deepStrictEqual(
await spawnPromisified(execPath, [
'--input-type=module',
'--eval',
[`import * as d from${JSON.stringify(target)};`,
'import{rm}from"node:fs/promises";',
`await rm(new URL(${JSON.stringify(target)}));`,
'import{strictEqual}from"node:assert";',
`strictEqual(JSON.stringify(await import(${JSON.stringify(target)})),JSON.stringify(d));`].join(''),
]),
{
code: 0,
signal: null,
stderr: '',
stdout: '',
});

View File

@ -0,0 +1,78 @@
'use strict';
const common = require('../common');
const { pathToFileURL } = require('url');
const assert = require('assert');
const relativePath = '../fixtures/es-modules/test-esm-ok.mjs';
const absolutePath = require.resolve(relativePath);
const targetURL = pathToFileURL(absolutePath);
function expectModuleError(result, code, message) {
Promise.resolve(result).catch(common.mustCall((error) => {
assert.strictEqual(error.code, code);
if (message) assert.strictEqual(error.message, message);
}));
}
function expectOkNamespace(result) {
Promise.resolve(result)
.then(common.mustCall((ns) => {
const expected = { default: true };
Object.defineProperty(expected, Symbol.toStringTag, {
value: 'Module'
});
Object.setPrototypeOf(expected, Object.getPrototypeOf(ns));
assert.deepStrictEqual(ns, expected);
}));
}
function expectFsNamespace(result) {
Promise.resolve(result)
.then(common.mustCall((ns) => {
assert.strictEqual(typeof ns.default.writeFile, 'function');
assert.strictEqual(typeof ns.writeFile, 'function');
}));
}
// For direct use of import expressions inside of CJS or ES modules, including
// via eval, all kinds of specifiers should work without issue.
(function testScriptOrModuleImport() {
// Importing another file, both direct & via eval
// expectOkNamespace(import(relativePath));
expectOkNamespace(eval(`import("${relativePath}")`));
expectOkNamespace(eval(`import("${relativePath}")`));
expectOkNamespace(eval(`import(${JSON.stringify(targetURL)})`));
// Importing a built-in, both direct & via eval
expectFsNamespace(import('fs'));
expectFsNamespace(eval('import("fs")'));
expectFsNamespace(eval('import("fs")'));
expectFsNamespace(import('node:fs'));
expectModuleError(import('node:unknown'),
'ERR_UNKNOWN_BUILTIN_MODULE');
expectModuleError(import('node:internal/test/binding'),
'ERR_UNKNOWN_BUILTIN_MODULE');
expectModuleError(import('./not-an-existing-module.mjs'),
'ERR_MODULE_NOT_FOUND');
expectModuleError(import('http://example.com/foo.js'),
'ERR_UNSUPPORTED_ESM_URL_SCHEME');
if (common.isWindows) {
const msg =
'Only URLs with a scheme in: file, data, and node are supported by the default ' +
'ESM loader. On Windows, absolute paths must be valid file:// URLs. ' +
"Received protocol 'c:'";
expectModuleError(import('C:\\example\\foo.mjs'),
'ERR_UNSUPPORTED_ESM_URL_SCHEME',
msg);
}
// If the specifier is an origin-relative URL, it should
// be treated as a file: URL.
expectOkNamespace(import(targetURL.pathname));
// If the referrer is a realm record, there is no way to resolve the
// specifier.
// TODO(legendecas): https://github.com/tc39/ecma262/pull/3195
expectModuleError(Promise.resolve('import("node:fs")').then(eval),
'ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING');
})();

View File

@ -0,0 +1,18 @@
'use strict';
const { spawnPromisified } = require('../common');
const fixtures = require('../common/fixtures.js');
const assert = require('node:assert');
const { execPath } = require('node:process');
const { describe, it } = require('node:test');
describe('ESM: importing an encoded path', () => {
it('should throw', async () => {
const { code } = await spawnPromisified(execPath, [
fixtures.path('es-module-url/native.mjs'),
]);
assert.strictEqual(code, 1);
});
});

View File

@ -0,0 +1,9 @@
import '../common/index.mjs';
import assert from 'assert';
// ./test-esm-ok.mjs
import ok from '../fixtures/es-modules/test-%65%73%6d-ok.mjs';
// ./test-esm-comma,.mjs
import comma from '../fixtures/es-modules/test-esm-comma%2c.mjs';
assert(ok);
assert(comma);

View File

@ -0,0 +1,26 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const file = '../fixtures/syntax/bad_syntax.mjs';
let error;
(async () => {
try {
await import(file);
} catch (e) {
assert.strictEqual(e.name, 'SyntaxError');
error = e;
}
assert(error);
await assert.rejects(
() => import(file),
(e) => {
assert.strictEqual(error, e);
return true;
}
);
})().then(common.mustCall());

View File

@ -0,0 +1,6 @@
// Flags: --experimental-loader ./test/fixtures/es-module-loaders/example-loader.mjs
/* eslint-disable node-core/require-common-first, node-core/required-modules */
import assert from 'assert';
import ok from '../fixtures/es-modules/test-esm-ok.mjs';
assert(ok);

View File

@ -0,0 +1,50 @@
import { spawnPromisified } from '../common/index.mjs';
import { fileURL } from '../common/fixtures.mjs';
import { doesNotMatch, match, strictEqual } from 'node:assert';
import { execPath } from 'node:process';
import { describe, it } from 'node:test';
describe('ESM: warn for obsolete hooks provided', { concurrency: !process.env.TEST_PARALLEL }, () => {
it('should not print warnings when no experimental features are enabled or used', async () => {
const { code, signal, stderr } = await spawnPromisified(execPath, [
'--input-type=module',
'--eval',
`import ${JSON.stringify(fileURL('es-module-loaders', 'module-named-exports.mjs'))}`,
]);
doesNotMatch(
stderr,
/ExperimentalWarning/,
new Error('No experimental warning(s) should be emitted when no experimental feature is enabled')
);
strictEqual(code, 0);
strictEqual(signal, null);
});
describe('experimental warnings for enabled experimental feature', () => {
for (
const [experiment, ...args] of [
[
/`--experimental-loader` may be removed in the future/,
'--experimental-loader',
fileURL('es-module-loaders', 'hooks-custom.mjs'),
],
]
) {
it(`should print for ${experiment.toString().replaceAll('/', '')}`, async () => {
const { code, signal, stderr } = await spawnPromisified(execPath, [
...args,
'--input-type=module',
'--eval',
`import ${JSON.stringify(fileURL('es-module-loaders', 'module-named-exports.mjs'))}`,
]);
match(stderr, /ExperimentalWarning/);
match(stderr, experiment);
strictEqual(code, 0);
strictEqual(signal, null);
});
}
});
});

View File

@ -0,0 +1,48 @@
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';
const importStatement = 'import { foo, notfound } from \'./module-named-exports.mjs\';';
const importStatementMultiline = `import {
foo,
notfound
} from './module-named-exports.mjs';
`;
describe('ESM: nonexistent exports', { concurrency: !process.env.TEST_PARALLEL }, () => {
for (
const { name, input }
of [
{
input: importStatement,
name: 'single-line import',
},
{
input: importStatementMultiline,
name: 'multi-line import',
},
]
) {
it(`should throw for nonexistent exports via ${name}`, async () => {
const { code, stderr } = await spawnPromisified(execPath, [
'--input-type=module',
'--eval',
input,
], {
cwd: fixtures.path('es-module-loaders'),
});
assert.notStrictEqual(code, 0);
// SyntaxError: The requested module './module-named-exports.mjs'
// does not provide an export named 'notfound'
assert.match(stderr, /SyntaxError:/);
// The quotes ensure that the path starts with ./ and not ../
assert.match(stderr, /'\.\/module-named-exports\.mjs'/);
assert.match(stderr, /notfound/);
});
}
});

View File

@ -0,0 +1,28 @@
// Flags: --pending-deprecation
import { mustCall } from '../common/index.mjs';
import assert from 'assert';
let curWarning = 0;
const expectedWarnings = [
'Use of deprecated leading or trailing slash',
'Use of deprecated double slash',
'.//asdf.js',
'".//internal/test.js"',
'".//internal//test.js"',
'"./////internal/////test.js"',
'"./trailing-pattern-slash/"',
'"./subpath/dir1/dir1.js"',
'"./subpath//dir1/dir1.js"',
'.//asdf.js',
'".//internal/test.js"',
'".//internal//test.js"',
'"./////internal/////test.js"',
'no_exports',
'default_index',
];
process.addListener('warning', mustCall((warning) => {
assert(warning.stack.includes(expectedWarnings[curWarning++]), warning.stack);
}, expectedWarnings.length));
await import('./test-esm-exports.mjs');

View File

@ -0,0 +1,248 @@
import { mustCall } from '../common/index.mjs';
import { ok, deepStrictEqual, strictEqual } from 'assert';
import { sep } from 'path';
import { requireFixture, importFixture } from '../fixtures/pkgexports.mjs';
import fromInside from '../fixtures/node_modules/pkgexports/lib/hole.js';
[requireFixture, importFixture].forEach((loadFixture) => {
const isRequire = loadFixture === requireFixture;
const maybeWrapped = isRequire ? (exports) => exports :
(exports) => ({ ...exports, 'module.exports': exports.default });
const validSpecifiers = new Map([
// A simple mapping of a path.
['pkgexports/valid-cjs', maybeWrapped({ default: 'asdf' })],
// A mapping pointing to a file that needs special encoding (%20) in URLs.
['pkgexports/space', maybeWrapped({ default: 'encoded path' })],
// Verifying that normal packages still work with exports turned on.
isRequire ? ['baz/index', { default: 'eye catcher' }] : [null],
// Fallbacks
['pkgexports/fallbackdir/asdf.js', maybeWrapped({ default: 'asdf' })],
['pkgexports/fallbackfile', maybeWrapped({ default: 'asdf' })],
// Conditional split for require
['pkgexports/condition', isRequire ? { default: 'encoded path' } :
maybeWrapped({ default: 'asdf' })],
// String exports sugar
['pkgexports-sugar', maybeWrapped({ default: 'main' })],
// Conditional object exports sugar
['pkgexports-sugar2', isRequire ? { default: 'not-exported' } :
maybeWrapped({ default: 'main' })],
// Resolve self
['pkgexports/resolve-self', isRequire ?
{ default: 'self-cjs' } : { default: 'self-mjs' }],
// Resolve self sugar
['pkgexports-sugar', maybeWrapped({ default: 'main' })],
// Path patterns
['pkgexports/subpath/sub-dir1', maybeWrapped({ default: 'main' })],
['pkgexports/subpath/sub-dir1.js', maybeWrapped({ default: 'main' })],
['pkgexports/features/dir1', maybeWrapped({ default: 'main' })],
['pkgexports/dir1/dir1/trailer', maybeWrapped({ default: 'main' })],
['pkgexports/dir2/dir2/trailer', maybeWrapped({ default: 'index' })],
['pkgexports/a/dir1/dir1', maybeWrapped({ default: 'main' })],
['pkgexports/a/b/dir1/dir1', maybeWrapped({ default: 'main' })],
// Deprecated:
// Double slashes:
['pkgexports/a//dir1/dir1', maybeWrapped({ default: 'main' })],
// double slash target
['pkgexports/doubleslash', maybeWrapped({ default: 'asdf' })],
// Null target with several slashes
['pkgexports/sub//internal/test.js', maybeWrapped({ default: 'internal only' })],
['pkgexports/sub//internal//test.js', maybeWrapped({ default: 'internal only' })],
['pkgexports/sub/////internal/////test.js', maybeWrapped({ default: 'internal only' })],
// trailing slash
['pkgexports/trailing-pattern-slash/',
maybeWrapped({ default: 'trailing-pattern-slash' })],
]);
if (!isRequire) {
// No exports or main field
validSpecifiers.set('no_exports', { default: 'index' });
// Main field without extension
validSpecifiers.set('default_index', { default: 'main' });
}
for (const [validSpecifier, expected] of validSpecifiers) {
if (validSpecifier === null) continue;
loadFixture(validSpecifier)
.then(mustCall((actual) => {
deepStrictEqual({ ...actual }, expected);
}));
}
const undefinedExports = new Map([
// There's no such export - so there's nothing to do.
['pkgexports/missing', './missing'],
// The file exists but isn't exported. The exports is a number which counts
// as a non-null value without any properties, just like `{}`.
['pkgexports-number/hidden.js', './hidden.js'],
// Sugar cases still encapsulate
['pkgexports-sugar/not-exported.js', './not-exported.js'],
['pkgexports-sugar2/not-exported.js', './not-exported.js'],
// Conditional exports with no match are "not exported" errors
['pkgexports/invalid1', './invalid1'],
['pkgexports/invalid4', './invalid4'],
// Null mapping
['pkgexports/sub/internal/test.js', './sub/internal/test.js'],
['pkgexports/sub/internal//test.js', './sub/internal//test.js'],
['pkgexports/null', './null'],
['pkgexports//null', './/null'],
['pkgexports/////null', './////null'],
['pkgexports/null/subpath', './null/subpath'],
// Empty fallback
['pkgexports/nofallback1', './nofallback1'],
// Non pattern matches
['pkgexports/trailer', './trailer'],
]);
const invalidExports = new Map([
// This path steps back inside the package but goes through an exports
// target that escapes the package, so we still catch that as invalid
['pkgexports/belowdir/pkgexports/asdf.js', './belowdir/'],
// This target file steps below the package
['pkgexports/belowfile', './belowfile'],
// Invalid targets
['pkgexports/invalid2', './invalid2'],
['pkgexports/invalid3', './invalid3'],
['pkgexports/invalid5', 'invalid5'],
// Missing / invalid fallbacks
['pkgexports/nofallback2', './nofallback2'],
// Reaching into nested node_modules
['pkgexports/nodemodules', './nodemodules'],
// Self resolve invalid
['pkgexports/resolve-self-invalid', './invalid2'],
]);
const invalidSpecifiers = new Map([
// Even though 'pkgexports/sub/asdf.js' works, alternate "path-like"
// variants do not to prevent confusion and accidental loopholes.
['pkgexports/sub/./../asdf.js', './sub/./../asdf.js'],
// Cannot reach into node_modules, even percent encoded
['pkgexports/sub/no%64e_modules', './sub/no%64e_modules'],
// Cannot backtrack below exposed path, even with percent encoded "."
['pkgexports/sub/%2e./asdf', './asdf'],
]);
for (const [specifier, subpath] of undefinedExports) {
loadFixture(specifier).catch(mustCall((err) => {
strictEqual(err.code, 'ERR_PACKAGE_PATH_NOT_EXPORTED');
assertStartsWith(err.message, 'Package subpath ');
assertIncludes(err.message, subpath);
}));
}
for (const [specifier, subpath] of invalidExports) {
loadFixture(specifier).catch(mustCall((err) => {
strictEqual(err.code, 'ERR_INVALID_PACKAGE_TARGET');
assertStartsWith(err.message, 'Invalid "exports"');
assertIncludes(err.message, subpath);
if (!subpath.startsWith('./')) {
assertIncludes(err.message, 'targets must start with');
}
}));
}
for (const [specifier, subpath] of invalidSpecifiers) {
loadFixture(specifier).catch(mustCall((err) => {
strictEqual(err.code, 'ERR_INVALID_MODULE_SPECIFIER');
assertStartsWith(err.message, 'Invalid module ');
assertIncludes(err.message, 'is not a valid match in pattern');
assertIncludes(err.message, subpath);
}));
}
// Conditional export, even with no match, should still be used instead
// of falling back to main
if (isRequire) {
loadFixture('pkgexports-main').catch(mustCall((err) => {
strictEqual(err.code, 'ERR_PACKAGE_PATH_NOT_EXPORTED');
assertStartsWith(err.message, 'No "exports" main ');
}));
}
const notFoundExports = new Map([
// Non-existing file
['pkgexports/sub/not-a-file.js', `pkgexports${sep}not-a-file.js`],
// No extension lookups
['pkgexports/no-ext', `pkgexports${sep}asdf`],
// Pattern specificity
['pkgexports/dir2/trailer', `subpath${sep}dir2.js`],
// Pattern double $$ escaping!
['pkgexports/a/$$', `subpath${sep}$$.js`],
]);
if (!isRequire) {
const onDirectoryImport = (err) => {
strictEqual(err.code, 'ERR_UNSUPPORTED_DIR_IMPORT');
assertStartsWith(err.message, 'Directory import');
};
loadFixture('pkgexports/subpath/dir1').catch(mustCall(onDirectoryImport));
loadFixture('pkgexports/subpath/dir2').catch(mustCall(onDirectoryImport));
}
for (const [specifier, request] of notFoundExports) {
loadFixture(specifier).catch(mustCall((err) => {
strictEqual(err.code, (isRequire ? '' : 'ERR_') + 'MODULE_NOT_FOUND');
assertIncludes(err.message, request);
assertStartsWith(err.message, 'Cannot find module');
}));
}
// The use of %2F and %5C escapes in paths fails loading
loadFixture('pkgexports/sub/..%2F..%2Fbar.js').catch(mustCall((err) => {
strictEqual(err.code, 'ERR_INVALID_MODULE_SPECIFIER');
}));
loadFixture('pkgexports/sub/..%5C..%5Cbar.js').catch(mustCall((err) => {
strictEqual(err.code, 'ERR_INVALID_MODULE_SPECIFIER');
}));
// Package export with numeric index properties must throw a validation error
loadFixture('pkgexports-numeric').catch(mustCall((err) => {
strictEqual(err.code, 'ERR_INVALID_PACKAGE_CONFIG');
}));
// Sugar conditional exports main mixed failure case
loadFixture('pkgexports-sugar-fail').catch(mustCall((err) => {
strictEqual(err.code, 'ERR_INVALID_PACKAGE_CONFIG');
assertStartsWith(err.message, 'Invalid package');
assertIncludes(err.message, '"exports" cannot contain some keys starting ' +
'with \'.\' and some not. The exports object must either be an object of ' +
'package subpath keys or an object of main entry condition name keys ' +
'only.');
}));
});
const { requireFromInside, importFromInside } = fromInside;
[importFromInside, requireFromInside].forEach((loadFromInside) => {
const isRequire = loadFromInside === requireFromInside;
const maybeWrapped = isRequire ? (exports) => exports :
(exports) => ({ ...exports, 'module.exports': exports.default });
const validSpecifiers = new Map([
// A file not visible from outside of the package
['../not-exported.js', maybeWrapped({ default: 'not-exported' })],
// Part of the public interface
['pkgexports/valid-cjs', maybeWrapped({ default: 'asdf' })],
]);
for (const [validSpecifier, expected] of validSpecifiers) {
if (validSpecifier === null) continue;
loadFromInside(validSpecifier)
.then(mustCall((actual) => {
deepStrictEqual({ ...actual }, expected);
}));
}
});
function assertStartsWith(actual, expected) {
const start = actual.toString().slice(0, expected.length);
strictEqual(start, expected);
}
function assertIncludes(actual, expected) {
ok(actual.toString().indexOf(expected) !== -1,
`${JSON.stringify(actual)} includes ${JSON.stringify(expected)}`);
}

View File

@ -0,0 +1,121 @@
import { spawnPromisified } from '../common/index.mjs';
import tmpdir from '../common/tmpdir.js';
import assert from 'node:assert';
import { mkdir, writeFile } from 'node:fs/promises';
import * as path from 'node:path';
import { execPath } from 'node:process';
import { describe, it, before } from 'node:test';
describe('ESM in main field', { concurrency: !process.env.TEST_PARALLEL }, () => {
before(() => tmpdir.refresh());
it('should handle fully-specified relative path without any warning', async () => {
const cwd = tmpdir.resolve(Math.random().toString());
const pkgPath = path.join(cwd, './node_modules/pkg/');
await mkdir(pkgPath, { recursive: true });
await writeFile(path.join(pkgPath, './index.js'), 'console.log("Hello World!")');
await writeFile(path.join(pkgPath, './package.json'), JSON.stringify({
main: './index.js',
type: 'module',
}));
const { code, stdout, stderr } = await spawnPromisified(execPath, [
'--input-type=module',
'--eval', 'import "pkg"',
], { cwd });
assert.strictEqual(stderr, '');
assert.match(stdout, /^Hello World!\r?\n$/);
assert.strictEqual(code, 0);
});
it('should handle fully-specified absolute path without any warning', async () => {
const cwd = tmpdir.resolve(Math.random().toString());
const pkgPath = path.join(cwd, './node_modules/pkg/');
await mkdir(pkgPath, { recursive: true });
await writeFile(path.join(pkgPath, './index.js'), 'console.log("Hello World!")');
await writeFile(path.join(pkgPath, './package.json'), JSON.stringify({
main: path.join(pkgPath, './index.js'),
type: 'module',
}));
const { code, stdout, stderr } = await spawnPromisified(execPath, [
'--input-type=module',
'--eval', 'import "pkg"',
], { cwd });
assert.strictEqual(stderr, '');
assert.match(stdout, /^Hello World!\r?\n$/);
assert.strictEqual(code, 0);
});
it('should emit warning when "main" and "exports" are missing', async () => {
const cwd = tmpdir.resolve(Math.random().toString());
const pkgPath = path.join(cwd, './node_modules/pkg/');
await mkdir(pkgPath, { recursive: true });
await writeFile(path.join(pkgPath, './index.js'), 'console.log("Hello World!")');
await writeFile(path.join(pkgPath, './package.json'), JSON.stringify({
type: 'module',
}));
const { code, stdout, stderr } = await spawnPromisified(execPath, [
'--input-type=module',
'--eval', 'import "pkg"',
], { cwd });
assert.match(stderr, /\[DEP0151\]/);
assert.match(stdout, /^Hello World!\r?\n$/);
assert.strictEqual(code, 0);
});
it('should emit warning when "main" is falsy', async () => {
const cwd = tmpdir.resolve(Math.random().toString());
const pkgPath = path.join(cwd, './node_modules/pkg/');
await mkdir(pkgPath, { recursive: true });
await writeFile(path.join(pkgPath, './index.js'), 'console.log("Hello World!")');
await writeFile(path.join(pkgPath, './package.json'), JSON.stringify({
type: 'module',
main: '',
}));
const { code, stdout, stderr } = await spawnPromisified(execPath, [
'--input-type=module',
'--eval', 'import "pkg"',
], { cwd });
assert.match(stderr, /\[DEP0151\]/);
assert.match(stdout, /^Hello World!\r?\n$/);
assert.strictEqual(code, 0);
});
it('should emit warning when "main" is a relative path without extension', async () => {
const cwd = tmpdir.resolve(Math.random().toString());
const pkgPath = path.join(cwd, './node_modules/pkg/');
await mkdir(pkgPath, { recursive: true });
await writeFile(path.join(pkgPath, './index.js'), 'console.log("Hello World!")');
await writeFile(path.join(pkgPath, './package.json'), JSON.stringify({
main: 'index',
type: 'module',
}));
const { code, stdout, stderr } = await spawnPromisified(execPath, [
'--input-type=module',
'--eval', 'import "pkg"',
], { cwd });
assert.match(stderr, /\[DEP0151\]/);
assert.match(stdout, /^Hello World!\r?\n$/);
assert.strictEqual(code, 0);
});
it('should emit warning when "main" is an absolute path without extension', async () => {
const cwd = tmpdir.resolve(Math.random().toString());
const pkgPath = path.join(cwd, './node_modules/pkg/');
await mkdir(pkgPath, { recursive: true });
await writeFile(path.join(pkgPath, './index.js'), 'console.log("Hello World!")');
await writeFile(path.join(pkgPath, './package.json'), JSON.stringify({
main: pkgPath + 'index',
type: 'module',
}));
const { code, stdout, stderr } = await spawnPromisified(execPath, [
'--input-type=module',
'--eval', 'import "pkg"',
], { cwd });
assert.match(stderr, /\[DEP0151\]/);
assert.match(stdout, /^Hello World!\r?\n$/);
assert.strictEqual(code, 0);
});
});

View File

@ -0,0 +1,96 @@
// Flags: --experimental-wasm-modules
import { spawnPromisified } from '../common/index.mjs';
import * as fixtures from '../common/fixtures.mjs';
import { describe, it } from 'node:test';
import { match, strictEqual } from 'node:assert';
describe('extensionless ES modules within a "type": "module" package scope', {
concurrency: !process.env.TEST_PARALLEL,
}, () => {
it('should run as the entry point', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(process.execPath, [
fixtures.path('es-modules/package-type-module/noext-esm'),
]);
strictEqual(stderr, '');
strictEqual(stdout, 'executed\n');
strictEqual(code, 0);
strictEqual(signal, null);
});
it('should be importable', async () => {
const { default: defaultExport } =
await import(fixtures.fileURL('es-modules/package-type-module/noext-esm'));
strictEqual(defaultExport, 'module');
});
it('should be importable from a module scope under node_modules', async () => {
const { default: defaultExport } =
await import(fixtures.fileURL(
'es-modules/package-type-module/node_modules/dep-with-package-json-type-module/noext-esm'));
strictEqual(defaultExport, 'module');
});
});
describe('extensionless Wasm modules within a "type": "module" package scope', {
concurrency: !process.env.TEST_PARALLEL,
}, () => {
it('should run as the entry point', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(process.execPath, [
'--experimental-wasm-modules',
'--no-warnings',
fixtures.path('es-modules/package-type-module/noext-wasm'),
]);
strictEqual(stderr, '');
strictEqual(stdout, 'executed\n');
strictEqual(code, 0);
strictEqual(signal, null);
});
it('should be importable', async () => {
const { add } = await import(fixtures.fileURL('es-modules/package-type-module/noext-wasm'));
strictEqual(add(1, 2), 3);
});
it('should be importable from a module scope under node_modules', async () => {
const { add } = await import(fixtures.fileURL(
'es-modules/package-type-module/node_modules/dep-with-package-json-type-module/noext-wasm'));
strictEqual(add(1, 2), 3);
});
});
describe('extensionless ES modules within no package scope', { concurrency: !process.env.TEST_PARALLEL }, () => {
it('should run as the entry point', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(process.execPath, [
fixtures.path('es-modules/noext-esm'),
]);
strictEqual(stdout, 'executed\n');
strictEqual(stderr, '');
strictEqual(code, 0);
strictEqual(signal, null);
});
it('should run on import', async () => {
await import(fixtures.fileURL('es-modules/noext-esm'));
});
});
describe('extensionless Wasm within no package scope', { concurrency: !process.env.TEST_PARALLEL }, () => {
it('should error as the entry point', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(process.execPath, [
'--experimental-wasm-modules',
'--no-warnings',
fixtures.path('es-modules/noext-wasm'),
]);
match(stderr, /SyntaxError/);
strictEqual(stdout, '');
strictEqual(code, 1);
strictEqual(signal, null);
});
it('should run on import', async () => {
await import(fixtures.fileURL('es-modules/noext-wasm'));
});
});

View File

@ -0,0 +1,24 @@
import '../common/index.mjs';
// eslint-disable-next-line no-undef
if (typeof arguments !== 'undefined') {
throw new Error('not an ESM');
}
if (typeof this !== 'undefined') {
throw new Error('not an ESM');
}
if (typeof exports !== 'undefined') {
throw new Error('not an ESM');
}
if (typeof require !== 'undefined') {
throw new Error('not an ESM');
}
if (typeof module !== 'undefined') {
throw new Error('not an ESM');
}
if (typeof __filename !== 'undefined') {
throw new Error('not an ESM');
}
if (typeof __dirname !== 'undefined') {
throw new Error('not an ESM');
}

View File

@ -0,0 +1,5 @@
import '../common/index.mjs';
import { stat } from 'fs/promises';
// Should not reject.
stat(new URL(import.meta.url));

View File

@ -0,0 +1,42 @@
import { spawnPromisified } from '../common/index.mjs';
import assert from 'node:assert';
import { execPath } from 'node:process';
await Promise.all([
// Using importAssertions in the resolve hook should warn but still work.
`data:text/javascript,export ${encodeURIComponent(function resolve() {
return { shortCircuit: true, url: 'data:application/json,1', importAssertions: { type: 'json' } };
})}`,
// Using importAssertions on the context object of the resolve hook should warn but still work.
`data:text/javascript,export ${encodeURIComponent(function resolve(s, c, n) {
const type = c.importAssertions.type;
return { shortCircuit: true, url: 'data:application/json,1', importAttributes: { type: type ?? 'json' } };
})}`,
// Setting importAssertions on the context object of the load hook should warn but still work.
`data:text/javascript,export ${encodeURIComponent(function load(u, c, n) {
c.importAssertions = { type: 'json' };
return n('data:application/json,1', c);
})}`,
// Creating a new context object with importAssertions in the load hook should warn but still work.
`data:text/javascript,export ${encodeURIComponent(function load(u, c, n) {
return n('data:application/json,1', { importAssertions: { type: 'json' } });
})}`,
].map(async (loaderURL) => {
const { stdout, stderr, code } = await spawnPromisified(execPath, [
'--input-type=module',
'--eval', `
import assert from 'node:assert';
import { register } from 'node:module';
register(${JSON.stringify(loaderURL)});
assert.deepStrictEqual(
{ ...await import('data:') },
{ default: 1 }
);`,
]);
assert.match(stderr, /Use `importAttributes` instead of `importAssertions`/);
assert.strictEqual(stdout, '');
assert.strictEqual(code, 0);
}));

View File

@ -0,0 +1,6 @@
import '../common/index.mjs';
import { strictEqual } from 'assert';
import secret from '../fixtures/experimental.json' with { type: 'json' };
strictEqual(secret.ofLife, 42);

View File

@ -0,0 +1,11 @@
import '../common/index.mjs';
import { strictEqual } from 'assert';
import secret0 from '../fixtures/experimental.json' with { type: 'json' };
const secret1 = await import('../fixtures/experimental.json', {
with: { type: 'json' },
});
strictEqual(secret0.ofLife, 42);
strictEqual(secret1.default.ofLife, 42);
strictEqual(secret1.default, secret0);

View File

@ -0,0 +1,10 @@
import '../common/index.mjs';
import { strictEqual } from 'assert';
import secret0 from '../fixtures/experimental.json' with { type: 'json' };
const secret1 = await import('../fixtures/experimental.json',
{ with: { type: 'json' } });
strictEqual(secret0.ofLife, 42);
strictEqual(secret1.default.ofLife, 42);
strictEqual(secret1.default, secret0);

View File

@ -0,0 +1,70 @@
'use strict';
const common = require('../common');
const { rejects } = require('assert');
const jsModuleDataUrl = 'data:text/javascript,export{}';
const jsonModuleDataUrl = 'data:application/json,""';
async function test() {
await rejects(
import('data:text/css,', { with: { type: 'css' } }),
{ code: 'ERR_UNKNOWN_MODULE_FORMAT' }
);
await rejects(
import('data:text/css,', { with: { unsupportedAttribute: 'value' } }),
{ code: 'ERR_IMPORT_ATTRIBUTE_UNSUPPORTED' }
);
await rejects(
import(`data:text/javascript,import${JSON.stringify(jsModuleDataUrl)}with{type:"json"}`),
{ code: 'ERR_IMPORT_ATTRIBUTE_TYPE_INCOMPATIBLE' }
);
await rejects(
import(jsModuleDataUrl, { with: { type: 'json' } }),
{ code: 'ERR_IMPORT_ATTRIBUTE_TYPE_INCOMPATIBLE' }
);
await rejects(
import(jsModuleDataUrl, { with: { type: 'json', other: 'unsupported' } }),
{ code: 'ERR_IMPORT_ATTRIBUTE_TYPE_INCOMPATIBLE' }
);
await rejects(
import(jsModuleDataUrl, { with: { type: 'unsupported' } }),
{ code: 'ERR_IMPORT_ATTRIBUTE_UNSUPPORTED' }
);
await rejects(
import(jsonModuleDataUrl),
{ code: 'ERR_IMPORT_ATTRIBUTE_MISSING' }
);
await rejects(
import(jsonModuleDataUrl, { with: {} }),
{ code: 'ERR_IMPORT_ATTRIBUTE_MISSING' }
);
await rejects(
import(jsonModuleDataUrl, { with: { foo: 'bar' } }),
{ code: 'ERR_IMPORT_ATTRIBUTE_MISSING' }
);
await rejects(
import(jsonModuleDataUrl, { with: { type: 'unsupported' } }),
{ code: 'ERR_IMPORT_ATTRIBUTE_UNSUPPORTED' }
);
await rejects(
import(jsonModuleDataUrl, { assert: { type: 'json' } }),
{ code: 'ERR_IMPORT_ATTRIBUTE_MISSING' }
);
await rejects(
import(`data:text/javascript,import${JSON.stringify(jsonModuleDataUrl)}assert{type:"json"}`),
SyntaxError
);
}
test().then(common.mustCall());

View File

@ -0,0 +1,62 @@
import '../common/index.mjs';
import { rejects } from 'assert';
const jsModuleDataUrl = 'data:text/javascript,export{}';
const jsonModuleDataUrl = 'data:application/json,""';
await rejects(
// This rejects because of the unsupported MIME type, not because of the
// unsupported assertion.
import('data:text/css,', { with: { type: 'css' } }),
{ code: 'ERR_UNKNOWN_MODULE_FORMAT' }
);
await rejects(
import(`data:text/javascript,import${JSON.stringify(jsModuleDataUrl)}with{type:"json"}`),
{ code: 'ERR_IMPORT_ATTRIBUTE_TYPE_INCOMPATIBLE' }
);
await rejects(
import(jsModuleDataUrl, { with: { type: 'json' } }),
{ code: 'ERR_IMPORT_ATTRIBUTE_TYPE_INCOMPATIBLE' }
);
await rejects(
import(jsModuleDataUrl, { with: { type: 'json', other: 'unsupported' } }),
{ code: 'ERR_IMPORT_ATTRIBUTE_TYPE_INCOMPATIBLE' }
);
await rejects(
import(import.meta.url, { with: { type: 'unsupported' } }),
{ code: 'ERR_IMPORT_ATTRIBUTE_UNSUPPORTED' }
);
await rejects(
import(jsonModuleDataUrl),
{ code: 'ERR_IMPORT_ATTRIBUTE_MISSING' }
);
await rejects(
import(jsonModuleDataUrl, { with: {} }),
{ code: 'ERR_IMPORT_ATTRIBUTE_MISSING' }
);
await rejects(
import(jsonModuleDataUrl, { with: { foo: 'bar' } }),
{ code: 'ERR_IMPORT_ATTRIBUTE_MISSING' }
);
await rejects(
import(jsonModuleDataUrl, { with: { type: 'unsupported' } }),
{ code: 'ERR_IMPORT_ATTRIBUTE_UNSUPPORTED' }
);
await rejects(
import(jsonModuleDataUrl, { assert: { type: 'json' } }),
{ code: 'ERR_IMPORT_ATTRIBUTE_MISSING' }
);
await rejects(
import(`data:text/javascript,import${JSON.stringify(jsonModuleDataUrl)}assert{type:"json"}`),
SyntaxError
);

View File

@ -0,0 +1,45 @@
// Flags: --expose-internals
'use strict';
require('../common');
const assert = require('assert');
const { validateAttributes } = require('internal/modules/esm/assert');
const url = 'test://';
assert.ok(validateAttributes(url, 'builtin', {}));
assert.ok(validateAttributes(url, 'commonjs', {}));
assert.ok(validateAttributes(url, 'json', { type: 'json' }));
assert.ok(validateAttributes(url, 'module', {}));
assert.ok(validateAttributes(url, 'wasm', {}));
assert.throws(() => validateAttributes(url, 'json', {}), {
code: 'ERR_IMPORT_ATTRIBUTE_MISSING',
});
assert.throws(() => validateAttributes(url, 'json', { type: 'json', unsupportedAttribute: 'value' }), {
code: 'ERR_IMPORT_ATTRIBUTE_UNSUPPORTED',
});
assert.throws(() => validateAttributes(url, 'module', { unsupportedAttribute: 'value' }), {
code: 'ERR_IMPORT_ATTRIBUTE_UNSUPPORTED',
});
assert.throws(() => validateAttributes(url, 'module', { type: 'json' }), {
code: 'ERR_IMPORT_ATTRIBUTE_TYPE_INCOMPATIBLE',
});
// The HTML spec specifically disallows this for now, while Wasm module import
// and whether it will require a type assertion is still an open question.
assert.throws(() => validateAttributes(url, 'module', { type: 'javascript' }), {
code: 'ERR_IMPORT_ATTRIBUTE_UNSUPPORTED',
});
assert.throws(() => validateAttributes(url, 'module', { type: 'css' }), {
code: 'ERR_IMPORT_ATTRIBUTE_UNSUPPORTED',
});
assert.throws(() => validateAttributes(url, 'module', { type: false }), {
code: 'ERR_INVALID_ARG_TYPE',
});

View File

@ -0,0 +1,218 @@
import { spawnPromisified } from '../common/index.mjs';
import fixtures from '../common/fixtures.js';
import assert from 'node:assert';
import { execPath } from 'node:process';
import { describe, it } from 'node:test';
const cjsEntry = fixtures.path('es-modules', 'cjs-file.cjs');
const cjsImport = fixtures.fileURL('es-modules', 'cjs-file.cjs');
const mjsEntry = fixtures.path('es-modules', 'mjs-file.mjs');
const mjsImport = fixtures.fileURL('es-modules', 'mjs-file.mjs');
describe('import modules using --import', { concurrency: !process.env.TEST_PARALLEL }, () => {
it('should import when using --eval', async () => {
const { code, signal, stderr, stdout } = await spawnPromisified(
execPath,
[
'--import', mjsImport,
'--eval', 'console.log("log")',
]
);
assert.strictEqual(stderr, '');
assert.match(stdout, /^\.mjs file\r?\nlog\r?\n$/);
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
});
it('should import when using --eval and --input-type=module', async () => {
const { code, signal, stderr, stdout } = await spawnPromisified(
execPath,
[
'--import', mjsImport,
'--input-type', 'module',
'--eval', 'console.log("log")',
]
);
assert.strictEqual(stderr, '');
assert.match(stdout, /^\.mjs file\r?\nlog\r?\n$/);
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
});
it('should import when main entrypoint is a cjs file', async () => {
const { code, signal, stderr, stdout } = await spawnPromisified(
execPath,
[
'--import', mjsImport,
cjsEntry,
]
);
assert.strictEqual(stderr, '');
assert.match(stdout, /^\.mjs file\r?\n\.cjs file\r?\n$/);
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
});
it('should import mjs when entrypoint is a cjs file', async () => {
const { code, signal, stderr, stdout } = await spawnPromisified(
execPath,
[
'--import', mjsImport,
cjsEntry,
]
);
assert.strictEqual(stderr, '');
assert.match(stdout, /^\.mjs file\r?\n\.cjs file\r?\n$/);
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
});
it('should import cjs when entrypoint is a mjs file', async () => {
const { code, signal, stderr, stdout } = await spawnPromisified(
execPath,
[
'--import', cjsImport,
mjsEntry,
]
);
assert.strictEqual(stderr, '');
assert.match(stdout, /^\.cjs file\r?\n\.mjs file\r?\n$/);
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
});
it('should import mjs when entrypoint is a cjs file', async () => {
const { code, signal, stderr, stdout } = await spawnPromisified(
execPath,
[
'--import', mjsImport,
cjsEntry,
]
);
assert.strictEqual(stderr, '');
assert.match(stdout, /^\.mjs file\r?\n\.cjs file\r?\n$/);
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
});
it('should de-duplicate redundant `--import`s', async () => {
const { code, signal, stderr, stdout } = await spawnPromisified(
execPath,
[
'--import', mjsImport,
'--import', mjsImport,
'--import', mjsImport,
cjsEntry,
]
);
assert.strictEqual(stderr, '');
assert.match(stdout, /^\.mjs file\r?\n\.cjs file\r?\n$/);
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
});
it('should import when running --check', async () => {
const { code, signal, stderr, stdout } = await spawnPromisified(
execPath,
[
'--import', mjsImport,
'--check',
cjsEntry,
]
);
assert.strictEqual(stderr, '');
assert.match(stdout, /^\.mjs file\r?\n$/);
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
});
it('should import when running --check fails', async () => {
const { code, signal, stderr, stdout } = await spawnPromisified(
execPath,
[
'--import', mjsImport,
'--no-warnings',
'--check',
fixtures.path('es-modules', 'invalid-cjs.js'),
]
);
assert.strictEqual(stderr, '');
assert.match(stdout, /^\.mjs file\r?\n$/);
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
});
it('should import --require before --import', async () => {
const { code, signal, stderr, stdout } = await spawnPromisified(
execPath,
[
'--import', mjsImport,
'--require', cjsEntry,
'--eval', 'console.log("log")',
]
);
assert.strictEqual(stderr, '');
assert.match(stdout, /^\.cjs file\r?\n\.mjs file\r?\nlog\r?\n$/);
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
});
it('should import a module with top level await', async () => {
const { code, signal, stderr, stdout } = await spawnPromisified(
execPath,
[
'--import', fixtures.fileURL('es-modules', 'esm-top-level-await.mjs'),
fixtures.path('es-modules', 'print-3.mjs'),
]
);
assert.strictEqual(stderr, '');
assert.match(stdout, /^1\r?\n2\r?\n3\r?\n$/);
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
});
it('should import files sequentially', async () => {
const { code, signal, stderr, stdout } = await spawnPromisified(
execPath,
[
'--import', fixtures.fileURL('es-modules', 'esm-top-level-await.mjs'),
'--import', fixtures.fileURL('es-modules', 'print-3.mjs'),
fixtures.path('empty.js'),
]
);
assert.strictEqual(stderr, '');
assert.match(stdout, /^1\r?\n2\r?\n3\r?\n$/);
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
});
it('should import files from the env before ones from the CLI', async () => {
const { code, signal, stderr, stdout } = await spawnPromisified(
execPath,
[
'--import', fixtures.fileURL('es-modules', 'print-3.mjs'),
fixtures.path('empty.js'),
],
{ env: { ...process.env, NODE_OPTIONS: `--import ${JSON.stringify(fixtures.fileURL('es-modules', 'esm-top-level-await.mjs'))}` } }
);
assert.strictEqual(stderr, '');
assert.match(stdout, /^1\r?\n2\r?\n3\r?\n$/);
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
});
});

View File

@ -0,0 +1,22 @@
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('ESM: named JSON exports', { concurrency: !process.env.TEST_PARALLEL }, () => {
it('should throw, citing named import', async () => {
const { code, stderr } = await spawnPromisified(execPath, [
fixtures.path('es-modules', 'import-json-named-export.mjs'),
]);
// SyntaxError: The requested module '../experimental.json'
// does not provide an export named 'ofLife'
assert.match(stderr, /SyntaxError:/);
assert.match(stderr, /'\.\.\/experimental\.json'/);
assert.match(stderr, /'ofLife'/);
assert.notStrictEqual(code, 0);
});
});

View File

@ -0,0 +1,74 @@
import { spawnPromisified } from '../common/index.mjs';
import * as fixtures from '../common/fixtures.js';
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
const importMetaMainScript = `
import assert from 'node:assert/strict';
assert.strictEqual(import.meta.main, true, 'import.meta.main should evaluate true in main module');
const { isMain: importedModuleIsMain } = await import(
${JSON.stringify(fixtures.fileURL('es-modules/import-meta-main.mjs'))}
);
assert.strictEqual(importedModuleIsMain, false, 'import.meta.main should evaluate false in imported module');
`;
function wrapScriptInEvalWorker(script) {
return `
import { Worker } from 'node:worker_threads';
new Worker(${JSON.stringify(script)}, { eval: true });
`;
}
function convertScriptSourceToDataUrl(script) {
return new URL(`data:text/javascript,${encodeURIComponent(script)}`);
}
function wrapScriptInUrlWorker(script) {
return `
import { Worker } from 'node:worker_threads';
new Worker(new URL(${JSON.stringify(convertScriptSourceToDataUrl(script))}));
`;
}
describe('import.meta.main in evaluated scripts', () => {
it('should evaluate true in evaluated script', async () => {
const result = await spawnPromisified(
process.execPath,
['--input-type=module', '--eval', importMetaMainScript],
);
assert.deepStrictEqual(result, {
stderr: '',
stdout: '',
code: 0,
signal: null,
});
});
it('should evaluate true in worker instantiated with module source by evaluated script', async () => {
const result = await spawnPromisified(
process.execPath,
['--input-type=module', '--eval', wrapScriptInEvalWorker(importMetaMainScript)],
);
assert.deepStrictEqual(result, {
stderr: '',
stdout: '',
code: 0,
signal: null,
});
});
it('should evaluate true in worker instantiated with `data:` URL by evaluated script', async () => {
const result = await spawnPromisified(
process.execPath,
['--input-type=module', '--eval', wrapScriptInUrlWorker(importMetaMainScript)],
);
assert.deepStrictEqual(result, {
stderr: '',
stdout: '',
code: 0,
signal: null,
});
});
});

View File

@ -0,0 +1,26 @@
import '../common/index.mjs';
import assert from 'node:assert/strict';
import { Worker } from 'node:worker_threads';
function get_environment() {
if (process.env.HAS_STARTED_WORKER) return 'in worker thread started by ES Module';
return 'in ES Module';
}
assert.strictEqual(
import.meta.main,
true,
`\`import.meta.main\` at top-level module ${get_environment()} should evaluate \`true\``
);
const { isMain: importedModuleIsMain } = await import('../fixtures/es-modules/import-meta-main.mjs');
assert.strictEqual(
importedModuleIsMain,
false,
`\`import.meta.main\` at dynamically imported module ${get_environment()} should evaluate \`false\``
);
if (!process.env.HAS_STARTED_WORKER) {
process.env.HAS_STARTED_WORKER = 1;
new Worker(import.meta.filename);
}

View File

@ -0,0 +1,120 @@
// Flags: --experimental-import-meta-resolve
import { spawnPromisified } from '../common/index.mjs';
import { fileURL as fixturesFileURL } from '../common/fixtures.mjs';
import assert from 'assert';
import { spawn } from 'child_process';
import { execPath } from 'process';
const fixtures = `${fixturesFileURL()}/`;
assert.strictEqual(import.meta.resolve('./test-esm-import-meta.mjs'),
new URL('./test-esm-import-meta.mjs', import.meta.url).href);
assert.strictEqual(import.meta.resolve('./notfound.mjs'), new URL('./notfound.mjs', import.meta.url).href);
assert.strictEqual(import.meta.resolve('./asset'), new URL('./asset', import.meta.url).href);
assert.throws(() => {
import.meta.resolve('does-not-exist');
}, {
code: 'ERR_MODULE_NOT_FOUND',
});
assert.strictEqual(
import.meta.resolve('../fixtures/empty-with-bom.txt'),
fixtures + 'empty-with-bom.txt');
assert.strictEqual(import.meta.resolve('../fixtures/'), fixtures);
assert.strictEqual(
import.meta.resolve('../fixtures/', import.meta.url),
fixtures);
assert.strictEqual(
import.meta.resolve('../fixtures/', new URL(import.meta.url)),
fixtures);
[[], {}, Symbol(), 0, 1, 1n, 1.1, () => {}, true, false].map((arg) =>
assert.throws(() => import.meta.resolve('../fixtures/', arg), {
code: 'ERR_INVALID_ARG_TYPE',
})
);
assert.strictEqual(import.meta.resolve('http://some-absolute/url'), 'http://some-absolute/url');
assert.strictEqual(import.meta.resolve('some://weird/protocol'), 'some://weird/protocol');
assert.strictEqual(import.meta.resolve('baz/', fixtures),
fixtures + 'node_modules/baz/');
assert.deepStrictEqual(
{ ...await import('data:text/javascript,export default import.meta.resolve("http://some-absolute/url")') },
{ default: 'http://some-absolute/url' },
);
assert.deepStrictEqual(
{ ...await import('data:text/javascript,export default import.meta.resolve("some://weird/protocol")') },
{ default: 'some://weird/protocol' },
);
assert.deepStrictEqual(
{ ...await import(`data:text/javascript,export default import.meta.resolve("baz/", ${encodeURIComponent(JSON.stringify(fixtures))})`) },
{ default: fixtures + 'node_modules/baz/' },
);
assert.deepStrictEqual(
{ ...await import('data:text/javascript,export default import.meta.resolve("fs")') },
{ default: 'node:fs' },
);
await assert.rejects(import('data:text/javascript,export default import.meta.resolve("does-not-exist")'), {
code: 'ERR_UNSUPPORTED_RESOLVE_REQUEST',
});
await assert.rejects(import('data:text/javascript,export default import.meta.resolve("./relative")'), {
code: 'ERR_UNSUPPORTED_RESOLVE_REQUEST',
});
{
const { stdout } = await spawnPromisified(execPath, [
'--input-type=module',
'--eval', 'console.log(typeof import.meta.resolve)',
]);
assert.match(stdout, /^function\r?\n$/);
}
{
const cp = spawn(execPath, [
'--input-type=module',
]);
cp.stdin.end('console.log(typeof import.meta.resolve)');
assert.match((await cp.stdout.toArray()).toString(), /^function\r?\n$/);
}
{
const { stdout } = await spawnPromisified(execPath, [
'--input-type=module',
'--eval', 'import "data:text/javascript,console.log(import.meta.resolve(%22node:os%22))"',
]);
assert.match(stdout, /^node:os\r?\n$/);
}
{
const cp = spawn(execPath, [
'--input-type=module',
]);
cp.stdin.end('import "data:text/javascript,console.log(import.meta.resolve(%22node:os%22))"');
assert.match((await cp.stdout.toArray()).toString(), /^node:os\r?\n$/);
}
{
const result = await spawnPromisified(execPath, [
'--no-warnings',
'--input-type=module',
'--import', 'data:text/javascript,import{register}from"node:module";register("data:text/javascript,")',
'--eval',
'console.log(import.meta.resolve(new URL("http://example.com")))',
]);
assert.deepStrictEqual(result, {
code: 0,
signal: null,
stderr: '',
stdout: 'http://example.com/\n',
});
}
{
const result = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-import-meta-resolve',
'--eval',
'import.meta.resolve("foo", "http://example.com/bar.js")',
]);
assert.match(result.stderr, /ERR_INVALID_URL/);
assert.strictEqual(result.stdout, '');
assert.strictEqual(result.code, 1);
}

View File

@ -0,0 +1,34 @@
import '../common/index.mjs';
import assert from 'assert';
assert.strictEqual(Object.getPrototypeOf(import.meta), null);
const keys = ['dirname', 'filename', 'main', 'resolve', 'url'];
assert.deepStrictEqual(Reflect.ownKeys(import.meta), keys);
const descriptors = Object.getOwnPropertyDescriptors(import.meta);
for (const descriptor of Object.values(descriptors)) {
delete descriptor.value; // Values are verified below.
assert.deepStrictEqual(descriptor, {
enumerable: true,
writable: true,
configurable: true
});
}
const urlReg = /^file:\/\/\/.*\/test\/es-module\/test-esm-import-meta\.mjs$/;
assert.match(import.meta.url, urlReg);
// Match *nix paths: `/some/path/test/es-module`
// Match Windows paths: `d:\\some\\path\\test\\es-module`
const dirReg = /^(\/|\w:\\).*(\/|\\)test(\/|\\)es-module$/;
assert.match(import.meta.dirname, dirReg);
// Match *nix paths: `/some/path/test/es-module/test-esm-import-meta.mjs`
// Match Windows paths: `d:\\some\\path\\test\\es-module\\test-esm-import-meta.js`
const fileReg = /^(\/|\w:\\).*(\/|\\)test(\/|\\)es-module(\/|\\)test-esm-import-meta\.mjs$/;
assert.match(import.meta.filename, fileReg);
// Verify that `data:` imports do not behave like `file:` imports.
import dataDirname from 'data:text/javascript,export default "dirname" in import.meta';
assert.strictEqual(dataDirname, false);

View File

@ -0,0 +1,27 @@
// Flags: --pending-deprecation
import { mustCall } from '../common/index.mjs';
import assert from 'assert';
let curWarning = 0;
const expectedWarnings = [
'Use of deprecated double slash',
'Use of deprecated double slash',
'./sub//null',
'./sub/////null',
'./sub//internal/test',
'./sub//internal//test',
'#subpath/////internal',
'#subpath//asdf.asdf',
'#subpath/as//df.asdf',
'./sub//null',
'./sub/////null',
'./sub//internal/test',
'./sub//internal//test',
'#subpath/////internal',
];
process.addListener('warning', mustCall((warning) => {
assert(warning.stack.includes(expectedWarnings[curWarning++]), warning.stack);
}, expectedWarnings.length));
await import('./test-esm-imports.mjs');

View File

@ -0,0 +1,142 @@
import { mustCall } from '../common/index.mjs';
import { ok, deepStrictEqual, strictEqual } from 'assert';
import importer from '../fixtures/es-modules/pkgimports/importer.js';
import { requireFixture } from '../fixtures/pkgexports.mjs';
const { requireImport, importImport } = importer;
[requireImport, importImport].forEach((loadFixture) => {
const isRequire = loadFixture === requireImport;
const maybeWrapped = isRequire ? (exports) => exports :
(exports) => ({ ...exports, 'module.exports': exports.default });
const internalImports = new Map([
// Base case
['#test', maybeWrapped({ default: 'test' })],
// import / require conditions
['#branch', maybeWrapped({ default: isRequire ? 'requirebranch' : 'importbranch' })],
// Subpath imports
['#subpath/x.js', maybeWrapped({ default: 'xsubpath' })],
// External imports
['#external', maybeWrapped({ default: 'asdf' })],
// External subpath imports
['#external/subpath/asdf.js', maybeWrapped({ default: 'asdf' })],
// Trailing pattern imports
['#subpath/asdf.asdf', maybeWrapped({ default: 'test' })],
// Leading slash
['#subpath//asdf.asdf', maybeWrapped({ default: 'test' })],
// Double slash
['#subpath/as//df.asdf', maybeWrapped({ default: 'test' })],
]);
for (const [validSpecifier, expected] of internalImports) {
if (validSpecifier === null) continue;
loadFixture(validSpecifier)
.then(mustCall((actual) => {
deepStrictEqual({ ...actual }, expected);
}));
}
const invalidImportTargets = new Set([
// Target steps below the package base
['#belowbase', '#belowbase'],
// Target is a URL
['#url', '#url'],
]);
for (const [specifier, subpath] of invalidImportTargets) {
loadFixture(specifier).catch(mustCall((err) => {
strictEqual(err.code, 'ERR_INVALID_PACKAGE_TARGET');
assertStartsWith(err.message, 'Invalid "imports"');
assertIncludes(err.message, subpath);
assertNotIncludes(err.message, 'targets must start with');
}));
}
const invalidImportSpecifiers = new Map([
// Backtracking below the package base
['#subpath/sub/../../../belowbase', 'request is not a valid match in pattern'],
// Percent-encoded slash errors
['#external/subpath/x%2Fy', 'must not include encoded "/" or "\\"'],
['#external/subpath/x%5Cy', 'must not include encoded "/" or "\\"'],
// Target must have a name
['#', '#'],
// Initial slash target must have a leading name
['#/initialslash', '#/initialslash'],
// Percent-encoded target paths
['#encodedslash', 'must not include encoded "/" or "\\"'],
['#encodedbackslash', 'must not include encoded "/" or "\\"'],
]);
for (const [specifier, expected] of invalidImportSpecifiers) {
loadFixture(specifier).catch(mustCall((err) => {
strictEqual(err.code, 'ERR_INVALID_MODULE_SPECIFIER');
assertStartsWith(err.message, 'Invalid module');
assertIncludes(err.message, expected);
}));
}
const undefinedImports = new Set([
// EOL subpaths
'#external/invalidsubpath/x',
// Missing import
'#missing',
// Explicit null import
'#null',
'#subpath/null',
// No condition match import
'#nullcondition',
// Null subpath shadowing
'#subpath/nullshadow/x',
// Null pattern
'#subpath/internal/test',
'#subpath/internal//test',
]);
for (const specifier of undefinedImports) {
loadFixture(specifier).catch(mustCall((err) => {
strictEqual(err.code, 'ERR_PACKAGE_IMPORT_NOT_DEFINED');
assertStartsWith(err.message, 'Package import ');
assertIncludes(err.message, specifier);
}));
}
// Handle not found for the defined imports target not existing
const nonDefinedImports = new Set([
'#notfound',
'#subpath//null',
'#subpath/////null',
'#subpath//internal/test',
'#subpath//internal//test',
'#subpath/////internal/////test',
]);
for (const specifier of nonDefinedImports) {
loadFixture(specifier).catch(mustCall((err) => {
strictEqual(err.code,
isRequire ? 'MODULE_NOT_FOUND' : 'ERR_MODULE_NOT_FOUND');
}));
}
});
// CJS resolver must still support #package packages in node_modules
requireFixture('#cjs').then(mustCall((actual) => {
strictEqual(actual.default, 'cjs backcompat');
}));
function assertStartsWith(actual, expected) {
const start = actual.toString().slice(0, expected.length);
strictEqual(start, expected);
}
function assertIncludes(actual, expected) {
ok(actual.toString().indexOf(expected) !== -1,
`${JSON.stringify(actual)} includes ${JSON.stringify(expected)}`);
}
function assertNotIncludes(actual, expected) {
ok(actual.toString().indexOf(expected) === -1,
`${JSON.stringify(actual)} doesn't include ${JSON.stringify(expected)}`);
}

View File

@ -0,0 +1,30 @@
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('ESM: ensure initialization happens only once', { concurrency: !process.env.TEST_PARALLEL }, () => {
it(async () => {
const { code, stderr, stdout } = await spawnPromisified(execPath, [
'--experimental-import-meta-resolve',
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-passthru.mjs'),
'--no-warnings',
fixtures.path('es-modules', 'runmain.mjs'),
]);
assert.strictEqual(stderr, '');
/**
* resolveHookRunCount = 2:
* 1. fixtures/…/runmain.mjs
* 2. node:module (imported by fixtures/…/runmain.mjs)
* 3. doesnt-matter.mjs (first import.meta.resolve call)
* 4. fixtures/…/runmain.mjs (entry point)
* 5. doesnt-matter.mjs (second import.meta.resolve call)
*/
assert.strictEqual(stdout.match(/resolve passthru/g)?.length, 5);
assert.strictEqual(code, 0);
});
});

View File

@ -0,0 +1,24 @@
'use strict';
const common = require('../common');
const assert = require('assert');
(async () => {
await assert.rejects(import('data:text/plain,export default0'), {
code: 'ERR_UNKNOWN_MODULE_FORMAT',
message:
'Unknown module format: text/plain for URL data:text/plain,' +
'export default0',
});
await assert.rejects(import('data:text/plain;base64,'), {
code: 'ERR_UNKNOWN_MODULE_FORMAT',
message:
'Unknown module format: text/plain for URL data:text/plain;base64,',
});
await assert.rejects(import('data:text/css,.error { color: red; }'), {
code: 'ERR_UNKNOWN_MODULE_FORMAT',
message: 'Unknown module format: text/css for URL data:text/css,.error { color: red; }',
});
await assert.rejects(import('data:WRONGtext/javascriptFORMAT,console.log("hello!");'), {
code: 'ERR_UNKNOWN_MODULE_FORMAT',
});
})().then(common.mustCall());

View File

@ -0,0 +1,30 @@
'use strict';
const { spawnPromisified } = require('../common');
const fixtures = require('../common/fixtures.js');
const assert = require('node:assert');
const path = require('node:path');
const { execPath } = require('node:process');
const { describe, it } = require('node:test');
describe('ESM: Package.json', { concurrency: !process.env.TEST_PARALLEL }, () => {
it('should throw on invalid pson', async () => {
const entry = fixtures.path('/es-modules/import-invalid-pjson.mjs');
const invalidJson = fixtures.path('/node_modules/invalid-pjson/package.json');
const { code, signal, stderr } = await spawnPromisified(execPath, [entry]);
assert.ok(stderr.includes('code: \'ERR_INVALID_PACKAGE_CONFIG\''), stderr);
assert.ok(
stderr.includes(
`Invalid package config ${path.toNamespacedPath(invalidJson)} while importing "invalid-pjson" from ${entry}.`
) || stderr.includes(
`Invalid package config ${path.toNamespacedPath(invalidJson)} while importing "invalid-pjson" from ${path.toNamespacedPath(entry)}.`
),
stderr
);
assert.strictEqual(code, 1);
assert.strictEqual(signal, null);
});
});

View File

@ -0,0 +1,24 @@
import '../common/index.mjs';
import { strictEqual, deepStrictEqual } from 'assert';
import { createRequire } from 'module';
import mod from '../fixtures/es-modules/json-cache/mod.cjs';
import another from '../fixtures/es-modules/json-cache/another.cjs';
import test from '../fixtures/es-modules/json-cache/test.json' with
{ type: 'json' };
const require = createRequire(import.meta.url);
const modCjs = require('../fixtures/es-modules/json-cache/mod.cjs');
const anotherCjs = require('../fixtures/es-modules/json-cache/another.cjs');
const testCjs = require('../fixtures/es-modules/json-cache/test.json');
strictEqual(mod.one, 1);
strictEqual(another.one, 'zalgo');
strictEqual(test.one, 'it comes');
deepStrictEqual(mod, modCjs);
deepStrictEqual(another, anotherCjs);
deepStrictEqual(test, testCjs);

View File

@ -0,0 +1,85 @@
import { spawnPromisified } from '../common/index.mjs';
import * as fixtures from '../common/fixtures.mjs';
import assert from 'node:assert';
import { execPath } from 'node:process';
import { before, describe, it, test } from 'node:test';
import { mkdir, rm, writeFile } from 'node:fs/promises';
import * as tmpdir from '../common/tmpdir.js';
import secret from '../fixtures/experimental.json' with { type: 'json' };
describe('ESM: importing JSON', () => {
before(() => tmpdir.refresh());
it('should load JSON', () => {
assert.strictEqual(secret.ofLife, 42);
});
it('should not print an experimental warning', async () => {
const { code, signal, stderr } = await spawnPromisified(execPath, [
fixtures.path('/es-modules/json-modules.mjs'),
]);
assert.strictEqual(stderr, '');
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
});
test('should load different modules when the URL is different', async (t) => {
const root = tmpdir.fileURL(`./test-esm-json-${Math.random()}/`);
try {
await mkdir(root, { recursive: true });
await t.test('json', async () => {
let i = 0;
const url = new URL('./foo.json', root);
await writeFile(url, JSON.stringify({ id: i++ }));
const absoluteURL = await import(`${url}`, {
with: { type: 'json' },
});
await writeFile(url, JSON.stringify({ id: i++ }));
const queryString = await import(`${url}?a=2`, {
with: { type: 'json' },
});
await writeFile(url, JSON.stringify({ id: i++ }));
const hash = await import(`${url}#a=2`, {
with: { type: 'json' },
});
await writeFile(url, JSON.stringify({ id: i++ }));
const queryStringAndHash = await import(`${url}?a=2#a=2`, {
with: { type: 'json' },
});
assert.notDeepStrictEqual(absoluteURL, queryString);
assert.notDeepStrictEqual(absoluteURL, hash);
assert.notDeepStrictEqual(queryString, hash);
assert.notDeepStrictEqual(absoluteURL, queryStringAndHash);
assert.notDeepStrictEqual(queryString, queryStringAndHash);
assert.notDeepStrictEqual(hash, queryStringAndHash);
});
await t.test('js', async () => {
let i = 0;
const url = new URL('./foo.mjs', root);
await writeFile(url, `export default ${JSON.stringify({ id: i++ })}\n`);
const absoluteURL = await import(`${url}`);
await writeFile(url, `export default ${JSON.stringify({ id: i++ })}\n`);
const queryString = await import(`${url}?a=1`);
await writeFile(url, `export default ${JSON.stringify({ id: i++ })}\n`);
const hash = await import(`${url}#a=1`);
await writeFile(url, `export default ${JSON.stringify({ id: i++ })}\n`);
const queryStringAndHash = await import(`${url}?a=1#a=1`);
assert.notDeepStrictEqual(absoluteURL, queryString);
assert.notDeepStrictEqual(absoluteURL, hash);
assert.notDeepStrictEqual(queryString, hash);
assert.notDeepStrictEqual(absoluteURL, queryStringAndHash);
assert.notDeepStrictEqual(queryString, queryStringAndHash);
assert.notDeepStrictEqual(hash, queryStringAndHash);
});
} finally {
await rm(root, { force: true, recursive: true });
}
});
});

View File

@ -0,0 +1,161 @@
import '../common/index.mjs';
import assert from 'assert';
import { syncBuiltinESMExports } from 'module';
import fs, { readFile, readFileSync } from 'fs';
import events, { defaultMaxListeners } from 'events';
import util from 'util';
const readFileDescriptor = Reflect.getOwnPropertyDescriptor(fs, 'readFile');
const readFileSyncDescriptor =
Reflect.getOwnPropertyDescriptor(fs, 'readFileSync');
const s = Symbol();
const fn = () => s;
Reflect.deleteProperty(fs, 'readFile');
syncBuiltinESMExports();
assert.deepStrictEqual([fs.readFile, readFile], [undefined, undefined]);
fs.readFile = fn;
syncBuiltinESMExports();
assert.deepStrictEqual([fs.readFile(), readFile()], [s, s]);
Reflect.defineProperty(fs, 'readFile', {
value: fn,
configurable: true,
writable: true,
});
syncBuiltinESMExports();
assert.deepStrictEqual([fs.readFile(), readFile()], [s, s]);
Reflect.deleteProperty(fs, 'readFile');
syncBuiltinESMExports();
assert.deepStrictEqual([fs.readFile, readFile], [undefined, undefined]);
let count = 0;
Reflect.defineProperty(fs, 'readFile', {
get() { return count; },
configurable: true,
});
syncBuiltinESMExports();
assert.deepStrictEqual([readFile, fs.readFile], [0, 0]);
count++;
syncBuiltinESMExports();
assert.deepStrictEqual([fs.readFile, readFile], [1, 1]);
let otherValue;
Reflect.defineProperty(fs, 'readFile', { // eslint-disable-line accessor-pairs
set(value) {
Reflect.deleteProperty(fs, 'readFile');
otherValue = value;
},
configurable: true,
});
Reflect.defineProperty(fs, 'readFileSync', {
get() {
return otherValue;
},
configurable: true,
});
fs.readFile = 2;
syncBuiltinESMExports();
assert.deepStrictEqual([readFile, readFileSync], [undefined, 2]);
Reflect.defineProperty(fs, 'readFile', readFileDescriptor);
Reflect.defineProperty(fs, 'readFileSync', readFileSyncDescriptor);
const originDefaultMaxListeners = events.defaultMaxListeners;
const utilProto = util.__proto__; // eslint-disable-line no-proto
count = 0;
Reflect.defineProperty(Function.prototype, 'defaultMaxListeners', {
configurable: true,
enumerable: true,
get: function() { return ++count; },
set: function(v) {
Reflect.defineProperty(this, 'defaultMaxListeners', {
configurable: true,
enumerable: true,
writable: true,
value: v,
});
},
});
syncBuiltinESMExports();
assert.strictEqual(defaultMaxListeners, originDefaultMaxListeners);
assert.strictEqual(events.defaultMaxListeners, originDefaultMaxListeners);
events.defaultMaxListeners += 1;
assert.strictEqual(events.defaultMaxListeners,
originDefaultMaxListeners + 1);
syncBuiltinESMExports();
assert.strictEqual(defaultMaxListeners, originDefaultMaxListeners + 1);
assert.strictEqual(Function.prototype.defaultMaxListeners, 1);
Function.prototype.defaultMaxListeners = 'foo';
syncBuiltinESMExports();
assert.strictEqual(Function.prototype.defaultMaxListeners, 'foo');
assert.strictEqual(events.defaultMaxListeners, originDefaultMaxListeners + 1);
assert.strictEqual(defaultMaxListeners, originDefaultMaxListeners + 1);
count = 0;
const p = {
get foo() { return ++count; },
set foo(v) {
Reflect.defineProperty(this, 'foo', {
configurable: true,
enumerable: true,
writable: true,
value: v,
});
},
};
util.__proto__ = p; // eslint-disable-line no-proto
syncBuiltinESMExports();
assert.strictEqual(util.foo, 1);
util.foo = 'bar';
syncBuiltinESMExports();
assert.strictEqual(count, 1);
assert.strictEqual(util.foo, 'bar');
assert.strictEqual(p.foo, 2);
p.foo = 'foo';
syncBuiltinESMExports();
assert.strictEqual(p.foo, 'foo');
events.defaultMaxListeners = originDefaultMaxListeners;
util.__proto__ = utilProto; // eslint-disable-line no-proto
Reflect.deleteProperty(util, 'foo');
Reflect.deleteProperty(Function.prototype, 'defaultMaxListeners');
syncBuiltinESMExports();
assert.throws(
() => Object.defineProperty(events, 'defaultMaxListeners', { value: 3 }),
/TypeError: Cannot redefine/
);

View File

@ -0,0 +1,10 @@
'use strict';
require('../common');
const { cache } = require;
Object.keys(cache).forEach((key) => {
delete cache[key];
});
// Require the same module again triggers the crash
require('../common');

View File

@ -0,0 +1,507 @@
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';
const setupArgs = [
'--no-warnings',
'--input-type=module',
'--eval',
];
const commonInput = 'import os from "node:os"; console.log(os)';
const commonArgs = [
...setupArgs,
commonInput,
];
describe('ESM: loader chaining', { concurrency: !process.env.TEST_PARALLEL }, () => {
it('should load unadulterated source when there are no loaders', async () => {
const { code, stderr, stdout } = await spawnPromisified(
execPath,
[
...setupArgs,
'import fs from "node:fs"; console.log(typeof fs?.constants?.F_OK )',
],
{ encoding: 'utf8' },
);
assert.strictEqual(stderr, '');
assert.match(stdout, /number/); // node:fs is an object
assert.strictEqual(code, 0);
});
it('should load properly different source when only load changes something', async () => {
const { code, stderr, stdout } = await spawnPromisified(
execPath,
[
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-passthru.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-load-foo-or-42.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-load-passthru.mjs'),
...commonArgs,
],
{ encoding: 'utf8' },
);
assert.strictEqual(stderr, '');
assert.match(stdout, /load passthru/);
assert.match(stdout, /resolve passthru/);
assert.match(stdout, /foo/);
assert.strictEqual(code, 0);
});
it('should result in proper output from multiple changes in resolve hooks', async () => {
const { code, stderr, stdout } = await spawnPromisified(
execPath,
[
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-shortcircuit.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-foo.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-42.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-load-foo-or-42.mjs'),
...commonArgs,
],
{ encoding: 'utf8' },
);
assert.strictEqual(stderr, '');
assert.match(stdout, /resolve 42/); // It did go thru resolve-42
assert.match(stdout, /foo/); // LIFO, so resolve-foo won
assert.strictEqual(code, 0);
});
it('should respect modified context within resolve chain', async () => {
const { code, stderr, stdout } = await spawnPromisified(
execPath,
[
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-shortcircuit.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-42.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-load-foo-or-42.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-load-receiving-modified-context.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-load-passing-modified-context.mjs'),
...commonArgs,
],
{ encoding: 'utf8' },
);
assert.strictEqual(stderr, '');
assert.match(stdout, /bar/);
assert.strictEqual(code, 0);
});
it('should accept only the correct arguments', async () => {
const { stdout } = await spawnPromisified(
execPath,
[
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-log-args.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-with-too-many-args.mjs'),
...commonArgs,
],
{ encoding: 'utf8' },
);
assert.match(stdout, /^resolve arg count: 3$/m);
assert.match(stdout, /specifier: 'node:os'/);
assert.match(stdout, /next: \[AsyncFunction: nextResolve\]/);
assert.match(stdout, /^load arg count: 3$/m);
assert.match(stdout, /url: 'node:os'/);
assert.match(stdout, /next: \[AsyncFunction: nextLoad\]/);
});
it('should result in proper output from multiple changes in resolve hooks', async () => {
const { code, stderr, stdout } = await spawnPromisified(
execPath,
[
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-shortcircuit.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-42.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-foo.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-load-foo-or-42.mjs'),
...commonArgs,
],
{ encoding: 'utf8' },
);
assert.strictEqual(stderr, '');
assert.match(stdout, /resolve foo/); // It did go thru resolve-foo
assert.match(stdout, /42/); // LIFO, so resolve-42 won
assert.strictEqual(code, 0);
});
it('should provide the correct "next" fn when multiple calls to next within same loader', async () => {
const { code, stderr, stdout } = await spawnPromisified(
execPath,
[
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-shortcircuit.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-foo.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-multiple-next-calls.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-load-foo-or-42.mjs'),
...commonArgs,
],
{ encoding: 'utf8' },
);
const countFoos = stdout.match(/resolve foo/g)?.length;
assert.strictEqual(stderr, '');
assert.strictEqual(countFoos, 2);
assert.strictEqual(code, 0);
});
it('should use the correct `name` for next<HookName>\'s function', async () => {
const { code, stderr, stdout } = await spawnPromisified(
execPath,
[
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-shortcircuit.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-42.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-load-foo-or-42.mjs'),
...commonArgs,
],
{ encoding: 'utf8' },
);
assert.strictEqual(stderr, '');
assert.match(stdout, /next<HookName>: nextResolve/);
assert.strictEqual(code, 0);
});
it('should throw for incomplete resolve chain, citing errant loader & hook', async () => {
const { code, stderr, stdout } = await spawnPromisified(
execPath,
[
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-incomplete.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-passthru.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-load-foo-or-42.mjs'),
...commonArgs,
],
{ encoding: 'utf8' },
);
assert.match(stdout, /resolve passthru/);
assert.match(stderr, /ERR_LOADER_CHAIN_INCOMPLETE/);
assert.match(stderr, /loader-resolve-incomplete\.mjs/);
assert.match(stderr, /'resolve'/);
assert.strictEqual(code, 1);
});
it('should NOT throw when nested resolve hook signaled a short circuit', async () => {
const { code, stderr, stdout } = await spawnPromisified(
execPath,
[
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-shortcircuit.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-next-modified.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-load-foo-or-42.mjs'),
...commonArgs,
],
{ encoding: 'utf8' },
);
assert.strictEqual(stderr, '');
assert.strictEqual(stdout.trim(), 'foo');
assert.strictEqual(code, 0);
});
it('should NOT throw when nested load hook signaled a short circuit', async () => {
const { code, stderr, stdout } = await spawnPromisified(
execPath,
[
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-shortcircuit.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-42.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-load-foo-or-42.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-load-next-modified.mjs'),
...commonArgs,
],
{ encoding: 'utf8' },
);
assert.strictEqual(stderr, '');
assert.match(stdout, /421/);
assert.strictEqual(code, 0);
});
it('should allow loaders to influence subsequent loader resolutions', async () => {
const { code, stderr } = await spawnPromisified(
execPath,
[
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-strip-xxx.mjs'),
'--loader',
'xxx/loader-resolve-strip-yyy.mjs',
...commonArgs,
],
{ encoding: 'utf8', cwd: fixtures.path('es-module-loaders') },
);
assert.strictEqual(stderr, '');
assert.strictEqual(code, 0);
});
it('should throw when the resolve chain is broken', async () => {
const { code, stderr, stdout } = await spawnPromisified(
execPath,
[
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-passthru.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-incomplete.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-load-foo-or-42.mjs'),
...commonArgs,
],
{ encoding: 'utf8' },
);
assert.doesNotMatch(stdout, /resolve passthru/);
assert.match(stderr, /ERR_LOADER_CHAIN_INCOMPLETE/);
assert.match(stderr, /loader-resolve-incomplete\.mjs/);
assert.match(stderr, /'resolve'/);
assert.strictEqual(code, 1);
});
it('should throw for incomplete load chain, citing errant loader & hook', async () => {
const { code, stderr, stdout } = await spawnPromisified(
execPath,
[
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-passthru.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-load-incomplete.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-load-passthru.mjs'),
...commonArgs,
],
{ encoding: 'utf8' },
);
assert.match(stdout, /load passthru/);
assert.match(stderr, /ERR_LOADER_CHAIN_INCOMPLETE/);
assert.match(stderr, /loader-load-incomplete\.mjs/);
assert.match(stderr, /'load'/);
assert.strictEqual(code, 1);
});
it('should throw when the load chain is broken', async () => {
const { code, stderr, stdout } = await spawnPromisified(
execPath,
[
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-passthru.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-load-passthru.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-load-incomplete.mjs'),
...commonArgs,
],
{ encoding: 'utf8' },
);
assert.doesNotMatch(stdout, /load passthru/);
assert.match(stderr, /ERR_LOADER_CHAIN_INCOMPLETE/);
assert.match(stderr, /loader-load-incomplete\.mjs/);
assert.match(stderr, /'load'/);
assert.strictEqual(code, 1);
});
it('should throw when invalid `specifier` argument passed to `nextResolve`', async () => {
const { code, stderr } = await spawnPromisified(
execPath,
[
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-passthru.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-bad-next-specifier.mjs'),
...commonArgs,
],
{ encoding: 'utf8' },
);
assert.strictEqual(code, 1);
assert.match(stderr, /ERR_INVALID_ARG_TYPE/);
assert.match(stderr, /loader-resolve-bad-next-specifier\.mjs/);
assert.match(stderr, /'resolve' hook's nextResolve\(\) specifier/);
});
it('should throw when resolve hook is invalid', async () => {
const { code, stderr } = await spawnPromisified(
execPath,
[
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-passthru.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-null-return.mjs'),
...commonArgs,
],
{ encoding: 'utf8' },
);
assert.strictEqual(code, 1);
assert.match(stderr, /ERR_INVALID_RETURN_VALUE/);
assert.match(stderr, /loader-resolve-null-return\.mjs/);
assert.match(stderr, /'resolve' hook's nextResolve\(\)/);
assert.match(stderr, /an object/);
assert.match(stderr, /got null/);
});
it('should throw when invalid `context` argument passed to `nextResolve`', async () => {
const { code, stderr } = await spawnPromisified(
execPath,
[
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-passthru.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-bad-next-context.mjs'),
...commonArgs,
],
{ encoding: 'utf8' },
);
assert.match(stderr, /ERR_INVALID_ARG_TYPE/);
assert.match(stderr, /loader-resolve-bad-next-context\.mjs/);
assert.match(stderr, /'resolve' hook's nextResolve\(\) context/);
assert.strictEqual(code, 1);
});
it('should throw when load hook is invalid', async () => {
const { code, stderr } = await spawnPromisified(
execPath,
[
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-load-passthru.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-load-null-return.mjs'),
...commonArgs,
],
{ encoding: 'utf8' },
);
assert.strictEqual(code, 1);
assert.match(stderr, /ERR_INVALID_RETURN_VALUE/);
assert.match(stderr, /loader-load-null-return\.mjs/);
assert.match(stderr, /'load' hook's nextLoad\(\)/);
assert.match(stderr, /an object/);
assert.match(stderr, /got null/);
});
it('should throw when invalid `url` argument passed to `nextLoad`', async () => {
const { code, stderr } = await spawnPromisified(
execPath,
[
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-load-passthru.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-load-bad-next-url.mjs'),
...commonArgs,
],
{ encoding: 'utf8' },
);
assert.match(stderr, /ERR_INVALID_ARG_TYPE/);
assert.match(stderr, /loader-load-bad-next-url\.mjs/);
assert.match(stderr, /'load' hook's nextLoad\(\) url/);
assert.strictEqual(code, 1);
});
it('should throw when invalid `url` argument passed to `nextLoad`', async () => {
const { code, stderr } = await spawnPromisified(
execPath,
[
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-load-passthru.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-load-impersonating-next-url.mjs'),
...commonArgs,
],
{ encoding: 'utf8' },
);
assert.match(stderr, /ERR_INVALID_ARG_VALUE/);
assert.match(stderr, /loader-load-impersonating-next-url\.mjs/);
assert.match(stderr, /'load' hook's nextLoad\(\) url/);
assert.strictEqual(code, 1);
});
it('should throw when invalid `context` argument passed to `nextLoad`', async () => {
const { code, stderr } = await spawnPromisified(
execPath,
[
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-load-passthru.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-load-bad-next-context.mjs'),
...commonArgs,
],
{ encoding: 'utf8' },
);
assert.match(stderr, /ERR_INVALID_ARG_TYPE/);
assert.match(stderr, /loader-load-bad-next-context\.mjs/);
assert.match(stderr, /'load' hook's nextLoad\(\) context/);
assert.strictEqual(code, 1);
});
it('should allow loaders to influence subsequent loader `import()` calls in `resolve`', async () => {
const { code, stderr, stdout } = await spawnPromisified(
execPath,
[
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-strip-xxx.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-dynamic-import.mjs'),
...commonArgs,
],
{ encoding: 'utf8' },
);
assert.strictEqual(stderr, '');
assert.match(stdout, /resolve dynamic import/); // It did go thru resolve-dynamic
assert.strictEqual(code, 0);
});
it('should allow loaders to influence subsequent loader `import()` calls in `load`', async () => {
const { code, stderr, stdout } = await spawnPromisified(
execPath,
[
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-strip-xxx.mjs'),
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-load-dynamic-import.mjs'),
...commonArgs,
],
{ encoding: 'utf8' },
);
assert.strictEqual(stderr, '');
assert.match(stdout, /load dynamic import/); // It did go thru load-dynamic
assert.strictEqual(code, 0);
});
});

View File

@ -0,0 +1,21 @@
// Flags: --experimental-loader ./test/fixtures/es-module-loaders/loader-with-custom-condition.mjs
import '../common/index.mjs';
import assert from 'assert';
import util from 'util';
import * as ns from '../fixtures/es-modules/conditional-exports.mjs';
assert.deepStrictEqual({ ...ns }, { default: 'from custom condition' });
assert.strictEqual(
util.inspect(ns, { showHidden: false }),
"[Module: null prototype] { default: 'from custom condition' }"
);
assert.strictEqual(
util.inspect(ns, { showHidden: true }),
'[Module: null prototype] {\n' +
" default: 'from custom condition',\n" +
" [Symbol(Symbol.toStringTag)]: 'Module'\n" +
'}'
);

View File

@ -0,0 +1,66 @@
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('default resolver', () => {
// In these tests `byop` is an acronym for "bring your own protocol", and is the
// protocol our byop-dummy-loader.mjs can load
it('should accept foreign schemas without exception (e.g. byop://something/or-other)', async () => {
const { code, stdout, stderr } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-loader',
fixtures.fileURL('/es-module-loaders/byop-dummy-loader.mjs'),
'--input-type=module',
'--eval',
"import 'byop://1/index.mjs'",
]);
assert.strictEqual(code, 0);
assert.strictEqual(stdout.trim(), 'index.mjs!');
assert.strictEqual(stderr, '');
});
it('should resolve foreign schemas by doing regular url absolutization', async () => {
const { code, stdout, stderr } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-loader',
fixtures.fileURL('/es-module-loaders/byop-dummy-loader.mjs'),
'--input-type=module',
'--eval',
"import 'byop://1/index2.mjs'",
]);
assert.strictEqual(code, 0);
assert.strictEqual(stdout.trim(), '42');
assert.strictEqual(stderr, '');
});
// In this test, `byoe` is an acronym for "bring your own extension"
it('should accept foreign extensions without exception (e.g. ..//something.byoe)', async () => {
const { code, stdout, stderr } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-loader',
fixtures.fileURL('/es-module-loaders/byop-dummy-loader.mjs'),
'--input-type=module',
'--eval',
"import 'byop://1/index.byoe'",
]);
assert.strictEqual(code, 0);
assert.strictEqual(stdout.trim(), 'index.byoe!');
assert.strictEqual(stderr, '');
});
it('should identify the parent module of an invalid URL host in import specifier', async () => {
if (process.platform === 'win32') return;
const { code, stderr } = await spawnPromisified(execPath, [
'--no-warnings',
fixtures.path('es-modules', 'invalid-posix-host.mjs'),
]);
assert.match(stderr, /ERR_INVALID_FILE_URL_HOST/);
assert.match(stderr, /file:\/\/hmm\.js/);
assert.match(stderr, /invalid-posix-host\.mjs/);
assert.strictEqual(code, 1);
});
});

View File

@ -0,0 +1,5 @@
// Flags: --experimental-loader ./test/fixtures/es-module-loaders/loader-with-dep.mjs
/* eslint-disable node-core/require-common-first, node-core/required-modules */
import '../fixtures/es-modules/test-esm-ok.mjs';
// We just test that this module doesn't fail loading

View File

@ -0,0 +1,97 @@
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';
// Helper function to assert the spawned process
async function assertSpawnedProcess(args, options = {}, expected = {}) {
const { code, signal, stderr, stdout } = await spawnPromisified(execPath, args, options);
if (expected.stderr) {
assert.match(stderr, expected.stderr);
}
if (expected.stdout) {
assert.match(stdout, expected.stdout);
}
assert.strictEqual(code, expected.code ?? 0);
assert.strictEqual(signal, expected.signal ?? null);
}
// Common expectation for experimental feature warning in stderr
const experimentalFeatureWarning = { stderr: /--entry-url is an experimental feature/ };
describe('--entry-url', { concurrency: true }, () => {
it('should reject loading a path that contains %', async () => {
await assertSpawnedProcess(
['--entry-url', './test-esm-double-encoding-native%20.mjs'],
{ cwd: fixtures.fileURL('es-modules') },
{
code: 1,
stderr: /ERR_MODULE_NOT_FOUND/,
}
);
});
it('should support loading properly encoded Unix path', async () => {
await assertSpawnedProcess(
['--entry-url', fixtures.fileURL('es-modules/test-esm-double-encoding-native%20.mjs').pathname],
{},
experimentalFeatureWarning
);
});
it('should support loading absolute URLs', async () => {
await assertSpawnedProcess(
['--entry-url', fixtures.fileURL('printA.js')],
{},
{
...experimentalFeatureWarning,
stdout: /^A\r?\n$/,
}
);
});
it('should support loading relative URLs', async () => {
await assertSpawnedProcess(
['--entry-url', 'es-modules/print-entrypoint.mjs?key=value#hash'],
{ cwd: fixtures.fileURL('./') },
{
...experimentalFeatureWarning,
stdout: /print-entrypoint\.mjs\?key=value#hash\r?\n$/,
}
);
});
it('should support loading `data:` URLs', async () => {
await assertSpawnedProcess(
['--entry-url', 'data:text/javascript,console.log(import.meta.url)'],
{},
{
...experimentalFeatureWarning,
stdout: /^data:text\/javascript,console\.log\(import\.meta\.url\)\r?\n$/,
}
);
});
it('should support loading TypeScript URLs', { skip: !process.config.variables.node_use_amaro }, async () => {
const typescriptUrls = [
'typescript/cts/test-require-ts-file.cts',
'typescript/mts/test-import-ts-file.mts',
];
for (const url of typescriptUrls) {
await assertSpawnedProcess(
['--entry-url', fixtures.fileURL(url)],
{},
{
...experimentalFeatureWarning,
stdout: /Hello, TypeScript!/,
}
);
}
});
});

View File

@ -0,0 +1,12 @@
// Flags: --experimental-loader ./test/fixtures/es-module-loaders/hooks-custom.mjs
import { mustCall } from '../common/index.mjs';
const done = mustCall();
// Test that the process doesn't exit because of a caught exception thrown as part of dynamic import().
for (let i = 0; i < 10; i++) {
await import('nonexistent/file.mjs').catch(() => {});
}
done();

View File

@ -0,0 +1,856 @@
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', 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, 13);
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';
register('data:text/javascript,export function initialize(){return new Promise(()=>{})}');
`,
]);
assert.strictEqual(stderr, '');
assert.strictEqual(stdout, '');
assert.strictEqual(code, 13);
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);
});
describe('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);
});
});

View File

@ -0,0 +1,89 @@
import { spawnPromisified } from '../common/index.mjs';
import fixtures from '../common/fixtures.js';
import assert from 'node:assert';
import http from 'node:http';
import path from 'node:path';
import { execPath } from 'node:process';
import { promisify } from 'node:util';
import { describe, it, after } from 'node:test';
const files = {
'main.mjs': 'export * from "./lib.mjs";',
'lib.mjs': 'export { sum } from "./sum.mjs";',
'sum.mjs': 'export function sum(a, b) { return a + b }',
'console.mjs': `
import { sum } from './sum.mjs';
globalThis.sum = sum;
console.log("loaded over http");
`,
};
const requestListener = ({ url }, rsp) => {
const filename = path.basename(url);
const content = files[filename];
if (content) {
return rsp
.writeHead(200, { 'Content-Type': 'application/javascript' })
.end(content);
}
return rsp
.writeHead(404)
.end();
};
const server = http.createServer(requestListener);
await promisify(server.listen.bind(server))({
host: '127.0.0.1',
port: 0,
});
const {
address: host,
port,
} = server.address();
describe('ESM: http import via loader', { concurrency: !process.env.TEST_PARALLEL }, () => {
it('should load using --import flag', async () => {
// ! MUST NOT use spawnSync to avoid blocking the event loop
const { code, signal, stderr, stdout } = await spawnPromisified(
execPath,
[
'--no-warnings',
'--loader',
fixtures.fileURL('es-module-loaders', 'http-loader.mjs'),
'--import', `http://${host}:${port}/console.mjs`,
'--eval',
'console.log(sum(1, 2))',
]
);
assert.strictEqual(stderr, '');
assert.match(stdout, /loaded over http\r?\n3/);
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
});
it('should load using import inside --eval code', async () => {
// ! MUST NOT use spawnSync to avoid blocking the event loop
const { code, signal, stderr, stdout } = await spawnPromisified(
execPath,
[
'--no-warnings',
'--loader',
fixtures.fileURL('es-module-loaders', 'http-loader.mjs'),
'--input-type=module',
'--eval',
`import * as main from 'http://${host}:${port}/main.mjs'; console.log(main)`,
]
);
assert.strictEqual(stderr, '');
assert.strictEqual(stdout, '[Module: null prototype] { sum: [Function: sum] }\n');
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
});
after(() => server.close());
});

View File

@ -0,0 +1,10 @@
// Flags: --experimental-loader ./test/fixtures/es-module-loaders/loader-invalid-format.mjs
import { expectsError, mustCall } from '../common/index.mjs';
import assert from 'assert';
import('../fixtures/es-modules/test-esm-ok.mjs')
.then(assert.fail, expectsError({
code: 'ERR_UNKNOWN_MODULE_FORMAT',
message: /Unknown module format: esm/
}))
.then(mustCall());

View File

@ -0,0 +1,10 @@
// Flags: --experimental-loader ./test/fixtures/es-module-loaders/loader-invalid-url.mjs
import { expectsError, mustCall } from '../common/index.mjs';
import assert from 'assert';
import('../fixtures/es-modules/test-esm-ok.mjs')
.then(assert.fail, (error) => {
expectsError({ code: 'ERR_INVALID_RETURN_PROPERTY_VALUE' })(error);
assert.match(error.message, /loader-invalid-url\.mjs/);
})
.then(mustCall());

View File

@ -0,0 +1,42 @@
import '../common/index.mjs';
import assert from 'node:assert/strict';
import { mock } from '../fixtures/es-module-loaders/mock.mjs';
mock('node:events', {
EventEmitter: 'This is mocked!'
});
// This resolves to node:events
// It is intercepted by mock-loader and doesn't return the normal value
assert.deepStrictEqual(await import('events'), Object.defineProperty({
__proto__: null,
EventEmitter: 'This is mocked!'
}, Symbol.toStringTag, {
enumerable: false,
value: 'Module'
}));
const mutator = mock('node:events', {
EventEmitter: 'This is mocked v2!'
});
// It is intercepted by mock-loader and doesn't return the normal value.
// This is resolved separately from the import above since the specifiers
// are different.
const mockedV2 = await import('node:events');
assert.deepStrictEqual(mockedV2, Object.defineProperty({
__proto__: null,
EventEmitter: 'This is mocked v2!'
}, Symbol.toStringTag, {
enumerable: false,
value: 'Module'
}));
mutator.EventEmitter = 'This is mocked v3!';
assert.deepStrictEqual(mockedV2, Object.defineProperty({
__proto__: null,
EventEmitter: 'This is mocked v3!'
}, Symbol.toStringTag, {
enumerable: false,
value: 'Module'
}));

View File

@ -0,0 +1,118 @@
'use strict';
// Flags: --expose-internals
require('../common');
const { strictEqual, throws } = require('assert');
const { createModuleLoader } = require('internal/modules/esm/loader');
const { LoadCache, ResolveCache } = require('internal/modules/esm/module_map');
const { ModuleJob } = require('internal/modules/esm/module_job');
const createDynamicModule = require(
'internal/modules/esm/create_dynamic_module');
const jsModuleDataUrl = 'data:text/javascript,export{}';
const jsonModuleDataUrl = 'data:application/json,""';
const stubJsModule = createDynamicModule([], ['default'], jsModuleDataUrl);
const stubJsonModule = createDynamicModule([], ['default'], jsonModuleDataUrl);
const loader = createModuleLoader(false);
const jsModuleJob = new ModuleJob(loader, stubJsModule.module, undefined,
() => new Promise(() => {}));
const jsonModuleJob = new ModuleJob(loader, stubJsonModule.module,
{ type: 'json' },
() => new Promise(() => {}));
// LoadCache.set and LoadCache.get store and retrieve module jobs for a
// specified url/type tuple; LoadCache.has correctly reports whether such jobs
// are stored in the map.
{
const moduleMap = new LoadCache();
moduleMap.set(jsModuleDataUrl, undefined, jsModuleJob);
moduleMap.set(jsonModuleDataUrl, 'json', jsonModuleJob);
strictEqual(moduleMap.get(jsModuleDataUrl), jsModuleJob);
strictEqual(moduleMap.get(jsonModuleDataUrl, 'json'), jsonModuleJob);
strictEqual(moduleMap.has(jsModuleDataUrl), true);
strictEqual(moduleMap.has(jsModuleDataUrl, 'javascript'), true);
strictEqual(moduleMap.has(jsonModuleDataUrl, 'json'), true);
strictEqual(moduleMap.has('unknown'), false);
// The types must match
strictEqual(moduleMap.has(jsModuleDataUrl, 'json'), false);
strictEqual(moduleMap.has(jsonModuleDataUrl, 'javascript'), false);
strictEqual(moduleMap.has(jsonModuleDataUrl), false);
strictEqual(moduleMap.has(jsModuleDataUrl, 'unknown'), false);
strictEqual(moduleMap.has(jsonModuleDataUrl, 'unknown'), false);
}
// LoadCache.get, LoadCache.has and LoadCache.set should only accept string
// values as url argument.
{
const moduleMap = new LoadCache();
const errorObj = {
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
message: /^The "url" argument must be of type string/
};
[{}, [], true, 1].forEach((value) => {
throws(() => moduleMap.get(value), errorObj);
throws(() => moduleMap.has(value), errorObj);
throws(() => moduleMap.set(value, undefined, jsModuleJob), errorObj);
});
}
// LoadCache.get, LoadCache.has and LoadCache.set should only accept string
// values (or the kAssertType symbol) as type argument.
{
const moduleMap = new LoadCache();
const errorObj = {
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
message: /^The "type" argument must be of type string/
};
[{}, [], true, 1].forEach((value) => {
throws(() => moduleMap.get(jsModuleDataUrl, value), errorObj);
throws(() => moduleMap.has(jsModuleDataUrl, value), errorObj);
throws(() => moduleMap.set(jsModuleDataUrl, value, jsModuleJob), errorObj);
});
}
// LoadCache.set should only accept ModuleJob values as job argument.
{
const moduleMap = new LoadCache();
[{}, [], true, 1].forEach((value) => {
throws(() => moduleMap.set('', undefined, value), {
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError',
message: /^The "job" argument must be an instance of ModuleJob/
});
});
}
{
const resolveMap = new ResolveCache();
strictEqual(resolveMap.serializeKey('./file', { __proto__: null }), './file::');
strictEqual(resolveMap.serializeKey('./file', { __proto__: null, type: 'json' }), './file::"type""json"');
strictEqual(resolveMap.serializeKey('./file::"type""json"', { __proto__: null }), './file::"type""json"::');
strictEqual(resolveMap.serializeKey('./file', { __proto__: null, c: 'd', a: 'b' }), './file::"a""b","c""d"');
strictEqual(resolveMap.serializeKey('./s', { __proto__: null, c: 'd', a: 'b', b: 'c' }), './s::"a""b","b""c","c""d"');
resolveMap.set('key1', 'parent1', 1);
resolveMap.set('key2', 'parent1', 2);
resolveMap.set('key2', 'parent2', 3);
strictEqual(resolveMap.get('key1', 'parent1'), 1);
strictEqual(resolveMap.get('key2', 'parent1'), 2);
strictEqual(resolveMap.get('key2', 'parent2'), 3);
}

View File

@ -0,0 +1,24 @@
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('ESM: nonexistent loader', () => {
it('should throw', async () => {
const { code, stderr } = await spawnPromisified(execPath, [
'--experimental-loader',
'i-dont-exist',
fixtures.path('print-error-message.js'),
]);
assert.notStrictEqual(code, 0);
// Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'i-dont-exist' imported from
assert.match(stderr, /ERR_MODULE_NOT_FOUND/);
assert.match(stderr, /'i-dont-exist'/);
assert.ok(!stderr.includes('Bad command or file name'));
});
});

View File

@ -0,0 +1,251 @@
import { spawnPromisified } from '../common/index.mjs';
import * as fixtures from '../common/fixtures.js';
import assert from 'node:assert';
import { execPath } from 'node:process';
import { describe, it } from 'node:test';
// This test ensures that the register function can register loaders
// programmatically.
const commonArgs = [
'--no-warnings',
'--input-type=module',
'--loader=data:text/javascript,',
];
const commonEvals = {
import: (module) => `await import(${JSON.stringify(module)});`,
register: (loader, parentURL = 'file:///') => `register(${JSON.stringify(loader)}, ${JSON.stringify(parentURL)});`,
dynamicImport: (module) => `await import(${JSON.stringify(`data:text/javascript,${encodeURIComponent(module)}`)});`,
staticImport: (module) => `import ${JSON.stringify(`data:text/javascript,${encodeURIComponent(module)}`)};`,
};
describe('ESM: programmatically register loaders', { concurrency: !process.env.TEST_PARALLEL }, () => {
it('works with only a dummy CLI argument', async () => {
const parentURL = fixtures.fileURL('es-module-loaders', 'loader-resolve-passthru.mjs');
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
...commonArgs,
'--eval',
"import { register } from 'node:module';" +
commonEvals.register(fixtures.fileURL('es-module-loaders', 'loader-resolve-passthru.mjs')) +
commonEvals.register(fixtures.fileURL('es-module-loaders', 'loader-load-passthru.mjs')) +
`register(${JSON.stringify('./loader-resolve-passthru.mjs')}, ${JSON.stringify({ parentURL })});` +
`register(${JSON.stringify('./loader-load-passthru.mjs')}, ${JSON.stringify({ parentURL })});` +
commonEvals.dynamicImport('console.log("Hello from dynamic import");'),
]);
assert.strictEqual(stderr, '');
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
const lines = stdout.split('\n');
assert.match(lines[0], /resolve passthru/);
assert.match(lines[1], /resolve passthru/);
assert.match(lines[2], /load passthru/);
assert.match(lines[3], /load passthru/);
assert.match(lines[4], /Hello from dynamic import/);
assert.strictEqual(lines[5], '');
});
describe('registering via --import', { concurrency: !process.env.TEST_PARALLEL }, () => {
for (const moduleType of ['mjs', 'cjs']) {
it(`should programmatically register a loader from a ${moduleType.toUpperCase()} file`, async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
...commonArgs,
'--import', fixtures.fileURL('es-module-loaders', `register-loader.${moduleType}`).href,
'--eval', commonEvals.staticImport('console.log("entry point")'),
]);
assert.strictEqual(stderr, '');
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
const [
passthruStdout,
entryPointStdout,
] = stdout.split('\n');
assert.match(passthruStdout, /resolve passthru/);
assert.match(entryPointStdout, /entry point/);
});
}
});
it('programmatically registered loaders are appended to an existing chaining', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
...commonArgs,
'--loader',
fixtures.fileURL('es-module-loaders', 'loader-resolve-passthru.mjs'),
'--eval',
"import { register } from 'node:module';" +
commonEvals.register(fixtures.fileURL('es-module-loaders', 'loader-load-passthru.mjs')) +
commonEvals.dynamicImport('console.log("Hello from dynamic import");'),
]);
assert.strictEqual(stderr, '');
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
const lines = stdout.split('\n');
assert.match(lines[0], /resolve passthru/);
assert.match(lines[1], /resolve passthru/);
assert.match(lines[2], /load passthru/);
assert.match(lines[3], /Hello from dynamic import/);
assert.strictEqual(lines[4], '');
});
it('works registering loaders across files', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
...commonArgs,
'--eval',
commonEvals.import(fixtures.fileURL('es-module-loaders', 'register-programmatically-loader-load.mjs')) +
commonEvals.import(fixtures.fileURL('es-module-loaders', 'register-programmatically-loader-resolve.mjs')) +
commonEvals.dynamicImport('console.log("Hello from dynamic import");'),
]);
assert.strictEqual(stderr, '');
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
const lines = stdout.split('\n');
assert.match(lines[0], /resolve passthru/);
assert.match(lines[1], /load passthru/);
assert.match(lines[2], /Hello from dynamic import/);
assert.strictEqual(lines[3], '');
});
it('works registering loaders across virtual files', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
...commonArgs,
'--eval',
commonEvals.import(fixtures.fileURL('es-module-loaders', 'register-programmatically-loader-load.mjs')) +
commonEvals.dynamicImport(
commonEvals.import(fixtures.fileURL('es-module-loaders', 'register-programmatically-loader-resolve.mjs')) +
commonEvals.dynamicImport('console.log("Hello from dynamic import");'),
),
]);
assert.strictEqual(stderr, '');
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
const lines = stdout.split('\n');
assert.match(lines[0], /resolve passthru/);
assert.match(lines[1], /load passthru/);
assert.match(lines[2], /Hello from dynamic import/);
assert.strictEqual(lines[3], '');
});
it('works registering the same loaders more them once', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
...commonArgs,
'--eval',
"import { register } from 'node:module';" +
commonEvals.register(fixtures.fileURL('es-module-loaders', 'loader-resolve-passthru.mjs')) +
commonEvals.register(fixtures.fileURL('es-module-loaders', 'loader-load-passthru.mjs')) +
commonEvals.register(fixtures.fileURL('es-module-loaders', 'loader-resolve-passthru.mjs')) +
commonEvals.register(fixtures.fileURL('es-module-loaders', 'loader-load-passthru.mjs')) +
commonEvals.dynamicImport('console.log("Hello from dynamic import");'),
]);
assert.strictEqual(stderr, '');
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
const lines = stdout.split('\n');
assert.match(lines[0], /resolve passthru/);
assert.match(lines[1], /resolve passthru/);
assert.match(lines[2], /load passthru/);
assert.match(lines[3], /load passthru/);
assert.match(lines[4], /Hello from dynamic import/);
assert.strictEqual(lines[5], '');
});
it('works registering loaders as package name', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
...commonArgs,
'--eval',
"import { register } from 'node:module';" +
commonEvals.register('resolve', fixtures.fileURL('es-module-loaders', 'package.json')) +
commonEvals.register('load', fixtures.fileURL('es-module-loaders', 'package.json')) +
commonEvals.dynamicImport('console.log("Hello from dynamic import");'),
]);
assert.strictEqual(stderr, '');
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
const lines = stdout.split('\n');
// Resolve occurs twice because it is first used to resolve the `load` loader
// _AND THEN_ the `register` module.
assert.match(lines[0], /resolve passthru/);
assert.match(lines[1], /resolve passthru/);
assert.match(lines[2], /load passthru/);
assert.match(lines[3], /Hello from dynamic import/);
assert.strictEqual(lines[4], '');
});
it('works without a CLI flag', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
'--no-warnings',
'--input-type=module',
'--eval',
"import { register } from 'node:module';" +
commonEvals.register(fixtures.fileURL('es-module-loaders', 'loader-load-passthru.mjs')) +
commonEvals.dynamicImport('console.log("Hello from dynamic import");'),
]);
assert.strictEqual(stderr, '');
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
const lines = stdout.split('\n');
assert.match(lines[0], /load passthru/);
assert.match(lines[1], /Hello from dynamic import/);
assert.strictEqual(lines[2], '');
});
it('does not work with a loader specifier that does not exist', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
...commonArgs,
'--eval',
"import { register } from 'node:module';" +
commonEvals.register('./not-found.mjs', import.meta.url) +
commonEvals.dynamicImport('console.log("Hello from dynamic import");'),
]);
assert.strictEqual(stdout, '');
assert.strictEqual(code, 1);
assert.strictEqual(signal, null);
assert.match(stderr, /ERR_MODULE_NOT_FOUND/);
});
it('does not work with a loader that got syntax errors', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
...commonArgs,
'--eval',
"import { register } from 'node:module';" +
commonEvals.register(fixtures.fileURL('es-module-loaders', 'syntax-error.mjs')) +
commonEvals.dynamicImport('console.log("Hello from dynamic import");'),
]);
assert.strictEqual(stdout, '');
assert.strictEqual(code, 1);
assert.strictEqual(signal, null);
assert.match(stderr, /SyntaxError/);
});
});

View File

@ -0,0 +1,48 @@
import { spawnPromisified } from '../common/index.mjs';
import * as tmpdir from '../common/tmpdir.js';
import * as fixtures from '../common/fixtures.mjs';
import { deepStrictEqual } from 'node:assert';
import { mkdir, rm, cp } from 'node:fs/promises';
import { execPath } from 'node:process';
tmpdir.refresh();
const base = tmpdir.fileURL(`test-esm-loader-resolve-type-${(Math.random() * Date.now()).toFixed(0)}`);
const moduleName = 'module-counter-by-type';
const moduleURL = new URL(`${base}/node_modules/${moduleName}`);
try {
await mkdir(moduleURL, { recursive: true });
await cp(
fixtures.path('es-modules', 'module-counter-by-type'),
moduleURL,
{ recursive: true }
);
const output = await spawnPromisified(
execPath,
[
'--no-warnings',
'--input-type=module',
'--eval',
`import { getModuleTypeStats } from ${JSON.stringify(fixtures.fileURL('es-module-loaders', 'hook-resolve-type.mjs'))};
const before = getModuleTypeStats();
await import(${JSON.stringify(moduleName)});
const after = getModuleTypeStats();
console.log(JSON.stringify({ before, after }));`,
],
{ cwd: base },
);
deepStrictEqual(output, {
code: 0,
signal: null,
stderr: '',
stdout: JSON.stringify({
before: { importedESM: 0, importedCJS: 0 },
// Dynamic import in the eval script should increment ESM counter but not CJS counter
after: { importedESM: 1, importedCJS: 0 },
}) + '\n',
});
} finally {
await rm(base, { recursive: true, force: true });
}

View File

@ -0,0 +1,20 @@
'use strict';
// Flags: --expose-internals
// This test ensures that search throws errors appropriately
require('../common');
const assert = require('assert');
const {
defaultResolve: resolve
} = require('internal/modules/esm/resolve');
assert.throws(
() => resolve('target'),
{
code: 'ERR_MODULE_NOT_FOUND',
name: 'Error',
message: /Cannot find package 'target'/
}
);

View File

@ -0,0 +1,304 @@
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 throwing errors', { concurrency: !process.env.TEST_PARALLEL }, () => {
it('throws on nonexistent modules', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-loader',
fixtures.fileURL('/es-module-loaders/hooks-custom.mjs'),
'--input-type=module',
'--eval',
'import "nonexistent/file.mjs"',
]);
assert.match(stderr, /ERR_MODULE_NOT_FOUND/);
assert.strictEqual(stdout, '');
assert.strictEqual(code, 1);
assert.strictEqual(signal, null);
});
it('throws on unknown extensions', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-loader',
fixtures.fileURL('/es-module-loaders/hooks-custom.mjs'),
'--input-type=module',
'--eval',
`import ${JSON.stringify(fixtures.fileURL('/es-modules/file.unknown'))}`,
]);
assert.match(stderr, /ERR_UNKNOWN_FILE_EXTENSION/);
assert.strictEqual(stdout, '');
assert.strictEqual(code, 1);
assert.strictEqual(signal, null);
});
it('throws on invalid return values', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-loader',
fixtures.fileURL('/es-module-loaders/hooks-custom.mjs'),
'--input-type=module',
'--eval',
'import "esmHook/badReturnVal.mjs"',
]);
assert.match(stderr, /ERR_INVALID_RETURN_VALUE/);
assert.strictEqual(stdout, '');
assert.strictEqual(code, 1);
assert.strictEqual(signal, null);
});
it('throws on boolean false', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-loader',
fixtures.fileURL('/es-module-loaders/hooks-custom.mjs'),
'--input-type=module',
'--eval',
'import "esmHook/format.false"',
]);
assert.match(stderr, /ERR_INVALID_RETURN_PROPERTY_VALUE/);
assert.strictEqual(stdout, '');
assert.strictEqual(code, 1);
assert.strictEqual(signal, null);
});
it('throws on boolean true', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-loader',
fixtures.fileURL('/es-module-loaders/hooks-custom.mjs'),
'--input-type=module',
'--eval',
'import "esmHook/format.true"',
]);
assert.match(stderr, /ERR_INVALID_RETURN_PROPERTY_VALUE/);
assert.strictEqual(stdout, '');
assert.strictEqual(code, 1);
assert.strictEqual(signal, null);
});
it('throws on invalid returned object', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-loader',
fixtures.fileURL('/es-module-loaders/hooks-custom.mjs'),
'--input-type=module',
'--eval',
'import "esmHook/badReturnFormatVal.mjs"',
]);
assert.match(stderr, /ERR_INVALID_RETURN_PROPERTY_VALUE/);
assert.strictEqual(stdout, '');
assert.strictEqual(code, 1);
assert.strictEqual(signal, null);
});
it('throws on unsupported format', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-loader',
fixtures.fileURL('/es-module-loaders/hooks-custom.mjs'),
'--input-type=module',
'--eval',
'import "esmHook/unsupportedReturnFormatVal.mjs"',
]);
assert.match(stderr, /ERR_UNKNOWN_MODULE_FORMAT/);
assert.strictEqual(stdout, '');
assert.strictEqual(code, 1);
assert.strictEqual(signal, null);
});
it('throws on invalid format property type', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-loader',
fixtures.fileURL('/es-module-loaders/hooks-custom.mjs'),
'--input-type=module',
'--eval',
'import "esmHook/badReturnSourceVal.mjs"',
]);
assert.match(stderr, /ERR_INVALID_RETURN_PROPERTY_VALUE/);
assert.strictEqual(stdout, '');
assert.strictEqual(code, 1);
assert.strictEqual(signal, null);
});
it('rejects dynamic imports for all of the error cases checked above', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-loader',
fixtures.fileURL('/es-module-loaders/hooks-custom.mjs'),
'--input-type=module',
'--eval',
`import assert from 'node:assert';
await Promise.allSettled([
import('nonexistent/file.mjs'),
import(${JSON.stringify(fixtures.fileURL('/es-modules/file.unknown'))}),
import('esmHook/badReturnVal.mjs'),
import('esmHook/format.false'),
import('esmHook/format.true'),
import('esmHook/badReturnFormatVal.mjs'),
import('esmHook/unsupportedReturnFormatVal.mjs'),
import('esmHook/badReturnSourceVal.mjs'),
]).then((results) => {
assert.strictEqual(results.every((result) => result.status === 'rejected'), true);
})`,
]);
assert.strictEqual(stderr, '');
assert.strictEqual(stdout, '');
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
});
});
describe('Loader hooks parsing modules', { concurrency: !process.env.TEST_PARALLEL }, () => {
it('can parse .js files as ESM', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-loader',
fixtures.fileURL('/es-module-loaders/hooks-custom.mjs'),
'--input-type=module',
'--eval',
`import assert from 'node:assert';
await import(${JSON.stringify(fixtures.fileURL('/es-module-loaders/js-as-esm.js'))})
.then((parsedModule) => {
assert.strictEqual(typeof parsedModule, 'object');
assert.strictEqual(parsedModule.namedExport, 'named-export');
})`,
]);
assert.strictEqual(stderr, '');
assert.strictEqual(stdout, '');
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
});
it('can define .ext files as ESM', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-loader',
fixtures.fileURL('/es-module-loaders/hooks-custom.mjs'),
'--input-type=module',
'--eval',
`import assert from 'node:assert';
await import(${JSON.stringify(fixtures.fileURL('/es-modules/file.ext'))})
.then((parsedModule) => {
assert.strictEqual(typeof parsedModule, 'object');
const { default: defaultExport } = parsedModule;
assert.strictEqual(typeof defaultExport, 'function');
assert.strictEqual(defaultExport.name, 'iAmReal');
assert.strictEqual(defaultExport(), true);
})`,
]);
assert.strictEqual(stderr, '');
assert.strictEqual(stdout, '');
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
});
it('can predetermine the format in the custom loader resolve hook', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-loader',
fixtures.fileURL('/es-module-loaders/hooks-custom.mjs'),
'--input-type=module',
'--eval',
`import assert from 'node:assert';
await import('esmHook/preknownFormat.pre')
.then((parsedModule) => {
assert.strictEqual(typeof parsedModule, 'object');
assert.strictEqual(parsedModule.default, 'hello world');
})`,
]);
assert.strictEqual(stderr, '');
assert.strictEqual(stdout, '');
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
});
it('can provide source for a nonexistent file', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-loader',
fixtures.fileURL('/es-module-loaders/hooks-custom.mjs'),
'--input-type=module',
'--eval',
`import assert from 'node:assert';
await import('esmHook/virtual.mjs')
.then((parsedModule) => {
assert.strictEqual(typeof parsedModule, 'object');
assert.strictEqual(typeof parsedModule.default, 'undefined');
assert.strictEqual(parsedModule.message, 'WOOHOO!');
})`,
]);
assert.strictEqual(stderr, '');
assert.strictEqual(stdout, '');
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
});
it('ensures that loaders have a separate context from userland', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-loader',
fixtures.fileURL('/es-module-loaders/hooks-custom.mjs'),
'--input-type=module',
'--eval',
`import assert from 'node:assert';
await import(${JSON.stringify(fixtures.fileURL('/es-modules/stateful.mjs'))})
.then(({ default: count }) => {
assert.strictEqual(count(), 1);
});`,
]);
assert.strictEqual(stderr, '');
assert.strictEqual(stdout, '');
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
});
it('ensures that user loaders are not bound to the internal loader', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-loader',
fixtures.fileURL('/es-module-loaders/loader-this-value-inside-hook-functions.mjs'),
'--input-type=module',
'--eval',
';', // Actual test is inside the loader module.
]);
assert.strictEqual(stderr, '');
assert.strictEqual(stdout, '');
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
});
it('throw maximum call stack error on the loader', async () => {
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
'--no-warnings',
'--experimental-loader',
fixtures.fileURL('/es-module-loaders/hooks-custom.mjs'),
'--input-type=module',
'--eval',
'await import("esmHook/maximumCallStack.mjs")',
]);
assert(stderr.includes('Maximum call stack size exceeded'));
assert.strictEqual(stdout, '');
assert.strictEqual(code, 1);
assert.strictEqual(signal, null);
});
});

View File

@ -0,0 +1,90 @@
// Flags: --experimental-loader ./test/fixtures/es-module-loaders/string-sources.mjs
import { mustCall, mustNotCall } from '../common/index.mjs';
import assert from 'assert';
import('test:Array').then(
mustNotCall('Should not accept Arrays'),
mustCall((e) => {
assert.strictEqual(e.code, 'ERR_INVALID_RETURN_PROPERTY_VALUE');
})
);
import('test:ArrayBuffer').then(
mustCall(),
mustNotCall('Should accept ArrayBuffers'),
);
import('test:BigInt64Array').then(
mustCall(),
mustNotCall('Should accept BigInt64Array'),
);
import('test:BigUint64Array').then(
mustCall(),
mustNotCall('Should accept BigUint64Array'),
);
import('test:Float32Array').then(
mustCall(),
mustNotCall('Should accept Float32Array'),
);
import('test:Float64Array').then(
mustCall(),
mustNotCall('Should accept Float64Array'),
);
import('test:Int8Array').then(
mustCall(),
mustNotCall('Should accept Int8Array'),
);
import('test:Int16Array').then(
mustCall(),
mustNotCall('Should accept Int16Array'),
);
import('test:Int32Array').then(
mustCall(),
mustNotCall('Should accept Int32Array'),
);
import('test:null').then(
mustNotCall('Should not accept null'),
mustCall((e) => {
assert.strictEqual(e.code, 'ERR_INVALID_RETURN_PROPERTY_VALUE');
})
);
import('test:Object').then(
mustNotCall('Should not stringify or valueOf Objects'),
mustCall((e) => {
assert.strictEqual(e.code, 'ERR_INVALID_RETURN_PROPERTY_VALUE');
})
);
import('test:SharedArrayBuffer').then(
mustCall(),
mustNotCall('Should accept SharedArrayBuffers'),
);
import('test:string').then(
mustCall(),
mustNotCall('Should accept strings'),
);
import('test:String').then(
mustNotCall('Should not accept wrapper Strings'),
mustCall((e) => {
assert.strictEqual(e.code, 'ERR_INVALID_RETURN_PROPERTY_VALUE');
})
);
import('test:Uint8ClampedArray').then(
mustCall(),
mustNotCall('Should accept Uint8ClampedArray'),
);
import('test:Uint16Array').then(
mustCall(),
mustNotCall('Should accept Uint16Array'),
);
import('test:Uint32Array').then(
mustCall(),
mustNotCall('Should accept Uint32Array'),
);
import('test:Uint8Array').then(
mustCall(),
mustNotCall('Should accept Uint8Arrays'),
);
import('test:undefined').then(
mustNotCall('Should not accept undefined'),
mustCall((e) => {
assert.strictEqual(e.code, 'ERR_INVALID_RETURN_PROPERTY_VALUE');
})
);

View File

@ -0,0 +1,43 @@
import { spawnPromisified } from '../common/index.mjs';
import { fileURL, path } from '../common/fixtures.mjs';
import { match, ok, notStrictEqual, strictEqual } from 'assert';
import { execPath } from 'node:process';
import { describe, it } from 'node:test';
describe('ESM: thenable loader hooks', { concurrency: !process.env.TEST_PARALLEL }, () => {
it('should behave as a normal promise resolution', async () => {
const { code, stderr } = await spawnPromisified(execPath, [
'--experimental-loader',
fileURL('es-module-loaders', 'thenable-load-hook.mjs').href,
path('es-modules', 'test-esm-ok.mjs'),
]);
strictEqual(code, 0);
ok(!stderr.includes('must not call'));
});
it('should crash the node process rejection with an error', async () => {
const { code, stderr } = await spawnPromisified(execPath, [
'--experimental-loader',
fileURL('es-module-loaders', 'thenable-load-hook-rejected.mjs').href,
path('es-modules', 'test-esm-ok.mjs'),
]);
notStrictEqual(code, 0);
match(stderr, /\sError: must crash the process\r?\n/);
ok(!stderr.includes('must not call'));
});
it('should just reject without an error (but NOT crash the node process)', async () => {
const { code, stderr } = await spawnPromisified(execPath, [
'--experimental-loader',
fileURL('es-module-loaders', 'thenable-load-hook-rejected-no-arguments.mjs').href,
path('es-modules', 'test-esm-ok.mjs'),
]);
notStrictEqual(code, 0);
match(stderr, /\sundefined\r?\n/);
ok(!stderr.includes('must not call'));
});
});

View File

@ -0,0 +1,10 @@
// Flags: --experimental-loader ./test/fixtures/es-module-loaders/preset-cjs-source.mjs
import '../common/index.mjs';
import * as fixtures from '../common/fixtures.mjs';
import assert from 'assert';
const { default: existingFileSource } = await import(fixtures.fileURL('es-modules', 'cjs-file.cjs'));
const { default: noSuchFileSource } = await import(new URL('./no-such-file.cjs', import.meta.url));
assert.strictEqual(existingFileSource, 'no .cjs file was read to get this source');
assert.strictEqual(noSuchFileSource, 'no .cjs file was read to get this source');

View File

@ -0,0 +1,20 @@
import { spawnPromisified } from '../common/index.mjs';
import { fileURL, path } from '../common/fixtures.mjs';
import { match, ok, notStrictEqual } from 'node:assert';
import { execPath } from 'node:process';
import { describe, it } from 'node:test';
describe('ESM: loader with syntax error', { concurrency: !process.env.TEST_PARALLEL }, () => {
it('should crash the node process', async () => {
const { code, stderr } = await spawnPromisified(execPath, [
'--experimental-loader',
fileURL('es-module-loaders', 'syntax-error.mjs').href,
path('print-error-message.js'),
]);
match(stderr, /SyntaxError \[Error\]:/);
ok(!stderr.includes('Bad command or file name'));
notStrictEqual(code, 0);
});
});

View File

@ -0,0 +1,88 @@
// Flags: --experimental-loader ./test/fixtures/es-module-loaders/hooks-custom.mjs
import '../common/index.mjs';
import assert from 'assert';
await assert.rejects(
import('nonexistent/file.mjs'),
{ code: 'ERR_MODULE_NOT_FOUND' },
);
await assert.rejects(
import('../fixtures/es-modules/file.unknown'),
{ code: 'ERR_UNKNOWN_FILE_EXTENSION' },
);
await assert.rejects(
import('esmHook/badReturnVal.mjs'),
{ code: 'ERR_INVALID_RETURN_VALUE' },
);
// Nullish values are allowed; booleans are not
await assert.rejects(
import('esmHook/format.false'),
{ code: 'ERR_INVALID_RETURN_PROPERTY_VALUE' },
);
await assert.rejects(
import('esmHook/format.true'),
{ code: 'ERR_INVALID_RETURN_PROPERTY_VALUE' },
);
await assert.rejects(
import('esmHook/badReturnFormatVal.mjs'),
{ code: 'ERR_INVALID_RETURN_PROPERTY_VALUE' },
);
await assert.rejects(
import('esmHook/unsupportedReturnFormatVal.mjs'),
{ code: 'ERR_UNKNOWN_MODULE_FORMAT' },
);
await assert.rejects(
import('esmHook/badReturnSourceVal.mjs'),
{ code: 'ERR_INVALID_RETURN_PROPERTY_VALUE' },
);
await assert.rejects(import('esmHook/commonJsNullSource.mjs'), {
code: 'ERR_INVALID_RETURN_PROPERTY_VALUE',
message: /"source".*'load'.*got type bigint/,
});
await import('../fixtures/es-module-loaders/js-as-esm.js')
.then((parsedModule) => {
assert.strictEqual(typeof parsedModule, 'object');
assert.strictEqual(parsedModule.namedExport, 'named-export');
});
// The custom loader tells node .ext files are MJS
await import('../fixtures/es-modules/file.ext')
.then((parsedModule) => {
assert.strictEqual(typeof parsedModule, 'object');
const { default: defaultExport } = parsedModule;
assert.strictEqual(typeof defaultExport, 'function');
assert.strictEqual(defaultExport.name, 'iAmReal');
assert.strictEqual(defaultExport(), true);
});
// The custom loader's resolve hook predetermines the format
await import('esmHook/preknownFormat.pre')
.then((parsedModule) => {
assert.strictEqual(typeof parsedModule, 'object');
assert.strictEqual(parsedModule.default, 'hello world');
});
// The custom loader provides source even though file does not actually exist
await import('esmHook/virtual.mjs')
.then((parsedModule) => {
assert.strictEqual(typeof parsedModule, 'object');
assert.strictEqual(typeof parsedModule.default, 'undefined');
assert.strictEqual(parsedModule.message, 'WOOHOO!');
});
// Ensure custom loaders have separate context from userland
// hooks-custom.mjs also increments count (which starts at 0)
// if both share context, count here would be 2
await import('../fixtures/es-modules/stateful.mjs')
.then(({ default: count }) => {
assert.strictEqual(count(), 1);
});

View File

@ -0,0 +1,189 @@
'use strict';
// Flags: --expose-internals
const common = require('../common');
if (!common.isWindows) {
common.skip('this test is Windows-specific.');
}
const fs = require('fs');
const path = require('path');
const tmpdir = require('../common/tmpdir');
const { describe, it } = require('node:test');
const { legacyMainResolve } = require('node:internal/modules/esm/resolve');
const { pathToFileURL } = require('node:url');
const { spawnPromisified } = require('../common');
const assert = require('assert');
const { execPath } = require('node:process');
describe('long path on Windows', () => {
it('check long path in ReadPackageJSON', () => {
// Module layout will be the following:
// package.json
// main
// main.js
const packageDirNameLen = Math.max(260 - tmpdir.path.length, 1);
const packageDirName = tmpdir.resolve('x'.repeat(packageDirNameLen));
const packageDirPath = path.resolve(packageDirName);
const packageJsonFilePath = path.join(packageDirPath, 'package.json');
const mainDirName = 'main';
const mainDirPath = path.resolve(packageDirPath, mainDirName);
const mainJsFile = 'main.js';
const mainJsFilePath = path.resolve(mainDirPath, mainJsFile);
tmpdir.refresh();
fs.mkdirSync(packageDirPath);
fs.writeFileSync(
packageJsonFilePath,
`{"main":"${mainDirName}/${mainJsFile}"}`
);
fs.mkdirSync(mainDirPath);
fs.writeFileSync(mainJsFilePath, 'console.log("hello world");');
require('internal/modules/run_main').executeUserEntryPoint(packageDirPath);
tmpdir.refresh();
});
it('check long path in LegacyMainResolve - 1', () => {
// Module layout will be the following:
// package.json
// index.js
const packageDirNameLen = Math.max(260 - tmpdir.path.length, 1);
const packageDirPath = tmpdir.resolve('y'.repeat(packageDirNameLen));
const packageJSPath = path.join(packageDirPath, 'package.json');
const indexJSPath = path.join(packageDirPath, 'index.js');
const packageConfig = { };
tmpdir.refresh();
fs.mkdirSync(packageDirPath);
fs.writeFileSync(packageJSPath, '');
fs.writeFileSync(indexJSPath, '');
const packageJsonUrl = pathToFileURL(
path.resolve(packageJSPath)
);
console.log(legacyMainResolve(packageJsonUrl, packageConfig, ''));
});
it('check long path in LegacyMainResolve - 2', () => {
// Module layout will be the following:
// package.json
// main.js
const packageDirNameLen = Math.max(260 - tmpdir.path.length, 1);
const packageDirPath = tmpdir.resolve('z'.repeat(packageDirNameLen));
const packageJSPath = path.join(packageDirPath, 'package.json');
const indexJSPath = path.join(packageDirPath, 'main.js');
const packageConfig = { main: 'main.js' };
tmpdir.refresh();
fs.mkdirSync(packageDirPath);
fs.writeFileSync(packageJSPath, '');
fs.writeFileSync(indexJSPath, '');
const packageJsonUrl = pathToFileURL(
path.resolve(packageJSPath)
);
console.log(legacyMainResolve(packageJsonUrl, packageConfig, ''));
});
it('check long path in GetNearestParentPackageJSON', async () => {
// Module layout will be the following:
// node_modules
// test-module
// package.json (path is less than 260 chars long)
// cjs
// package.json (path is more than 260 chars long)
// index.js
const testModuleDirPath = 'node_modules/test-module';
let cjsDirPath = path.join(testModuleDirPath, 'cjs');
let modulePackageJSPath = path.join(testModuleDirPath, 'package.json');
let cjsPackageJSPath = path.join(cjsDirPath, 'package.json');
let cjsIndexJSPath = path.join(cjsDirPath, 'index.js');
const tmpDirNameLen = Math.max(
260 - tmpdir.path.length - cjsPackageJSPath.length, 1);
const tmpDirPath = tmpdir.resolve('k'.repeat(tmpDirNameLen));
cjsDirPath = path.join(tmpDirPath, cjsDirPath);
modulePackageJSPath = path.join(tmpDirPath, modulePackageJSPath);
cjsPackageJSPath = path.join(tmpDirPath, cjsPackageJSPath);
cjsIndexJSPath = path.join(tmpDirPath, cjsIndexJSPath);
tmpdir.refresh();
fs.mkdirSync(cjsDirPath, { recursive: true });
fs.writeFileSync(
modulePackageJSPath,
`{
"type": "module"
}`
);
fs.writeFileSync(
cjsPackageJSPath,
`{
"type": "commonjs"
}`
);
fs.writeFileSync(
cjsIndexJSPath,
'const fs = require("fs");'
);
const { code, signal, stderr } = await spawnPromisified(execPath, [cjsIndexJSPath]);
assert.strictEqual(stderr.trim(), '');
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
});
it('check long path in GetNearestParentPackageJSONType', async () => {
// Module layout will be the following:
// node_modules
// test-module
// package.json (path is less than 260 chars long)
// cjs
// package.json (path is more than 260 chars long)
// index.js
const testModuleDirPath = 'node_modules/test-module';
let cjsDirPath = path.join(testModuleDirPath, 'cjs');
let modulePackageJSPath = path.join(testModuleDirPath, 'package.json');
let cjsPackageJSPath = path.join(cjsDirPath, 'package.json');
let cjsIndexJSPath = path.join(cjsDirPath, 'index.js');
const tmpDirNameLen = Math.max(260 - tmpdir.path.length - cjsPackageJSPath.length, 1);
const tmpDirPath = tmpdir.resolve('l'.repeat(tmpDirNameLen));
cjsDirPath = path.join(tmpDirPath, cjsDirPath);
modulePackageJSPath = path.join(tmpDirPath, modulePackageJSPath);
cjsPackageJSPath = path.join(tmpDirPath, cjsPackageJSPath);
cjsIndexJSPath = path.join(tmpDirPath, cjsIndexJSPath);
tmpdir.refresh();
fs.mkdirSync(cjsDirPath, { recursive: true });
fs.writeFileSync(
modulePackageJSPath,
`{
"type": "module"
}`
);
fs.writeFileSync(
cjsPackageJSPath,
`{
"type": "commonjs"
}`
);
fs.writeFileSync(cjsIndexJSPath, 'import fs from "node:fs/promises";');
const { code, signal, stderr } = await spawnPromisified(execPath, [cjsIndexJSPath]);
assert.ok(stderr.includes('Failed to load the ES module'));
assert.strictEqual(code, 1);
assert.strictEqual(signal, null);
});
});

View File

@ -0,0 +1,30 @@
import { mustNotCall } from '../common/index.mjs';
import * as fixtures from '../common/fixtures.mjs';
import assert from 'assert';
Object.defineProperty(Error.prototype, 'url', {
get: mustNotCall('get %Error.prototype%.url'),
set: mustNotCall('set %Error.prototype%.url'),
});
Object.defineProperty(Object.prototype, 'url', {
get: mustNotCall('get %Object.prototype%.url'),
set: mustNotCall('set %Object.prototype%.url'),
});
await assert.rejects(import('../fixtures/es-modules/pjson-main'), {
code: 'ERR_UNSUPPORTED_DIR_IMPORT',
url: fixtures.fileURL('es-modules/pjson-main').href,
});
await assert.rejects(import(`data:text/javascript,import${encodeURIComponent(JSON.stringify(fixtures.fileURL('es-modules/pjson-main')))}`), {
code: 'ERR_UNSUPPORTED_DIR_IMPORT',
url: fixtures.fileURL('es-modules/pjson-main').href,
});
await assert.rejects(import(`data:text/javascript,import${encodeURIComponent(JSON.stringify(fixtures.fileURL('es-modules/does-not-exist')))}`), {
code: 'ERR_MODULE_NOT_FOUND',
url: fixtures.fileURL('es-modules/does-not-exist').href,
});
assert.deepStrictEqual(
{ ...await import('../fixtures/es-modules/pjson-main/main.mjs') },
{ main: 'main' },
);

View File

@ -0,0 +1,55 @@
import { spawnPromisified } from '../common/index.mjs';
import { fixturesDir, fileURL as fixtureSubDir } from '../common/fixtures.mjs';
import { match, notStrictEqual } from 'node:assert';
import { execPath } from 'node:process';
import { describe, it } from 'node:test';
describe('ESM: module not found hint', { concurrency: !process.env.TEST_PARALLEL }, () => {
for (
const { input, expected, cwd = fixturesDir }
of [
{
input: 'import "./print-error-message"',
// Did you mean to import "./print-error-message.js"?
expected: / "\.\/print-error-message\.js"\?/,
},
{
input: 'import "./es-modules/folder%25with percentage#/index.js"',
// Did you mean to import "./es-modules/folder%2525with%20percentage%23/index.js"?
expected: / "\.\/es-modules\/folder%2525with%20percentage%23\/index\.js"\?/,
},
{
input: 'import "../folder%25with percentage#/index.js"',
// Did you mean to import "../es-modules/folder%2525with%20percentage%23/index.js"?
expected: / "\.\.\/folder%2525with%20percentage%23\/index\.js"\?/,
cwd: fixtureSubDir('es-modules/tla/'),
},
{
input: 'import obj from "some_module/obj"',
expected: / "some_module\/obj\.js"\?/,
},
{
input: 'import obj from "some_module/folder%25with percentage#/index.js"',
expected: / "some_module\/folder%2525with%20percentage%23\/index\.js"\?/,
},
{
input: 'import "@nodejsscope/pkg/index"',
expected: / "@nodejsscope\/pkg\/index\.js"\?/,
},
{
input: 'import obj from "lone_file.js"',
expected: /node_modules\/lone_file\.js"\?/,
},
]
) it('should cite a variant form', async () => {
const { code, stderr } = await spawnPromisified(execPath, [
'--input-type=module',
'--eval',
input,
], { cwd });
match(stderr, expected);
notStrictEqual(code, 0);
});
});

View File

@ -0,0 +1,9 @@
// Flags: --import ./test/fixtures/es-module-loaders/builtin-named-exports.mjs
import '../common/index.mjs';
import { readFile, __fromLoader } from 'fs';
import assert from 'assert';
import ok from '../fixtures/es-modules/test-esm-ok.mjs';
assert(ok);
assert(readFile);
assert(__fromLoader);

View File

@ -0,0 +1,13 @@
import '../common/index.mjs';
import * as fs from 'fs';
import assert from 'assert';
import Module from 'module';
const keys = Object.entries(
Object.getOwnPropertyDescriptors(new Module().require('fs')))
.filter(([ , d]) => d.enumerable)
.map(([name]) => name)
.concat('default')
.sort();
assert.deepStrictEqual(keys, Object.keys(fs).sort());

View File

@ -0,0 +1,26 @@
import { mustCall } from '../common/index.mjs';
import { Worker, isMainThread } from 'worker_threads';
import assert from 'assert';
import { fileURLToPath } from 'url';
import { requireFixture, importFixture } from '../fixtures/pkgexports.mjs';
if (isMainThread) {
const tests = [[], ['--no-addons']];
for (const execArgv of tests) {
new Worker(fileURLToPath(import.meta.url), { execArgv });
}
} else {
[requireFixture, importFixture].forEach((loadFixture) => {
loadFixture('pkgexports/no-addons').then(
mustCall((module) => {
const message = module.default;
if (!process.execArgv.includes('--no-addons')) {
assert.strictEqual(message, 'using native addons');
} else {
assert.strictEqual(message, 'not using native addons');
}
})
);
});
}

View File

@ -0,0 +1,20 @@
import { spawnPromisified } from '../common/index.mjs';
import { fileURL } from '../common/fixtures.mjs';
import { match, strictEqual } from 'node:assert';
import { execPath } from 'node:process';
import { describe, it } from 'node:test';
describe('ESM: non-js extensions fail', { concurrency: !process.env.TEST_PARALLEL }, () => {
it(async () => {
const { code, stderr, signal } = await spawnPromisified(execPath, [
'--input-type=module',
'--eval',
`import ${JSON.stringify(fileURL('es-modules', 'file.unknown'))}`,
]);
match(stderr, /ERR_UNKNOWN_FILE_EXTENSION/);
strictEqual(code, 1);
strictEqual(signal, null);
});
});

View File

@ -0,0 +1,19 @@
import { spawnPromisified } from '../common/index.mjs';
import { path } from '../common/fixtures.mjs';
import { strictEqual } from 'node:assert';
import { execPath } from 'node:process';
import { describe, it } from 'node:test';
describe('ESM: experiemental warning for import.meta.resolve', { concurrency: !process.env.TEST_PARALLEL }, () => {
it('should not warn when caught', async () => {
const { code, signal, stderr } = await spawnPromisified(execPath, [
'--experimental-import-meta-resolve',
path('es-modules/import-resolve-exports.mjs'),
]);
strictEqual(stderr, '');
strictEqual(code, 0);
strictEqual(signal, null);
});
});

View File

@ -0,0 +1,6 @@
import '../common/index.mjs';
import assert from 'assert';
import { posix } from 'path';
import pathPosix from 'path/posix';
assert.strictEqual(pathPosix, posix);

View File

@ -0,0 +1,6 @@
import '../common/index.mjs';
import assert from 'assert';
import { win32 } from 'path';
import pathWin32 from 'path/win32';
assert.strictEqual(pathWin32, win32);

View File

@ -0,0 +1,13 @@
import '../common/index.mjs';
import assert from 'node:assert';
import { importFixture } from '../fixtures/pkgexports.mjs';
await Promise.all([
'as%2Ff',
'as%5Cf',
'as\\df',
'@as@df',
].map((specifier) => assert.rejects(importFixture(specifier), {
code: 'ERR_INVALID_MODULE_SPECIFIER',
})));

View File

@ -0,0 +1,76 @@
'use strict';
const { spawnPromisified, skip } = require('../common');
const tmpdir = require('../common/tmpdir');
// Invoke the main file via a symlink. In this case --preserve-symlinks-main
// dictates that it'll resolve relative imports in the main file relative to
// the symlink, and not relative to the symlink target; the file structure set
// up below requires this to not crash when loading ./submodule_link.js
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const { execPath } = require('node:process');
const { describe, it } = require('node:test');
tmpdir.refresh();
const tmpDir = tmpdir.path;
fs.mkdirSync(path.join(tmpDir, 'nested'));
fs.mkdirSync(path.join(tmpDir, 'nested2'));
const entry = path.join(tmpDir, 'nested', 'entry.js');
const entry_link_absolute_path = path.join(tmpDir, 'index.js');
const submodule = path.join(tmpDir, 'nested2', 'submodule.js');
const submodule_link_absolute_path = path.join(tmpDir, 'submodule_link.js');
fs.writeFileSync(entry, `
const assert = require('assert');
// this import only resolves with --preserve-symlinks-main set
require('./submodule_link.js');
`);
fs.writeFileSync(submodule, '');
try {
fs.symlinkSync(entry, entry_link_absolute_path);
fs.symlinkSync(submodule, submodule_link_absolute_path);
} catch (err) {
if (err.code !== 'EPERM') throw err;
skip('insufficient privileges for symlinks');
}
describe('Invoke the main file via a symlink.', { concurrency: !process.env.TEST_PARALLEL }, () => {
it('should resolve relative imports in the main file', async () => {
const { code } = await spawnPromisified(execPath, [
'--preserve-symlinks',
'--preserve-symlinks-main',
entry_link_absolute_path,
]);
assert.strictEqual(code, 0);
});
it('should resolve relative imports in the main file when file extension is omitted', async () => {
const entry_link_absolute_path_without_ext = path.join(tmpDir, 'index');
const { code } = await spawnPromisified(execPath, [
'--preserve-symlinks',
'--preserve-symlinks-main',
entry_link_absolute_path_without_ext,
]);
assert.strictEqual(code, 0);
});
it('should resolve relative imports in the main file when filename(index.js) is omitted', async () => {
const { code } = await spawnPromisified(execPath, [
'--preserve-symlinks',
'--preserve-symlinks-main',
tmpDir,
]);
assert.strictEqual(code, 0);
});
});

View File

@ -0,0 +1,3 @@
// Flags: --experimental-loader ./test/fixtures/es-module-loaders/not-found-assert-loader.mjs
/* eslint-disable node-core/require-common-first, node-core/required-modules */
import './not-found.js';

View File

@ -0,0 +1,3 @@
// Flags: --experimental-loader ./test/fixtures/es-module-loaders/not-found-assert-loader.mjs
/* eslint-disable node-core/require-common-first, node-core/required-modules */
import './not-found.mjs';

Some files were not shown because too many files have changed in this diff Show More