Skip to content

Commit

Permalink
esm: consolidate ESM loader hooks
Browse files Browse the repository at this point in the history
doc: update ESM hook examples

esm: fix unsafe primordial

doc: fix ESM example linting

esm: allow source of type ArrayBuffer

doc: update ESM hook changelog to include resolve format

esm: allow all ArrayBuffers and TypedArrays for load hook source

doc: tidy code & API docs

doc: convert ESM source table header from Title Case to Sentence case

doc: add detailed explanation for getPackageType

esm: add caveat that ESMLoader::import() must NOT be renamed

esm: tidy code declaration of getFormat protocolHandlers

doc: correct ESM doc link (bad conflict resolution)

doc: update ESM hook limitation for CJS

esm: tweak preload description

doc: update ESM getPackageType() example explanation

PR-URL: #37468
Reviewed-By: Antoine du Hamel <[email protected]>
Reviewed-By: Guy Bedford <[email protected]>
Reviewed-By: Bradley Farias <[email protected]>
Reviewed-By: Geoffrey Booth <[email protected]>
JakobJingleheimer authored and GeoffreyBooth committed Sep 12, 2021
1 parent 540f9d9 commit df22736
Showing 46 changed files with 972 additions and 545 deletions.
312 changes: 163 additions & 149 deletions doc/api/esm.md

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
@@ -981,14 +981,14 @@ Module.prototype.load = function(filename) {
Module._extensions[extension](this, filename);
this.loaded = true;

const ESMLoader = asyncESM.ESMLoader;
const esmLoader = asyncESM.esmLoader;
// Create module entry at load time to snapshot exports correctly
const exports = this.exports;
// Preemptively cache
if ((module?.module === undefined ||
module.module.getStatus() < kEvaluated) &&
!ESMLoader.cjsCache.has(this))
ESMLoader.cjsCache.set(this, exports);
!esmLoader.cjsCache.has(this))
esmLoader.cjsCache.set(this, exports);
};


@@ -1022,7 +1022,7 @@ function wrapSafe(filename, content, cjsModuleInstance) {
lineOffset: 0,
displayErrors: true,
importModuleDynamically: async (specifier) => {
const loader = asyncESM.ESMLoader;
const loader = asyncESM.esmLoader;
return loader.import(specifier, normalizeReferrerURL(filename));
},
});
@@ -1037,7 +1037,7 @@ function wrapSafe(filename, content, cjsModuleInstance) {
], {
filename,
importModuleDynamically(specifier) {
const loader = asyncESM.ESMLoader;
const loader = asyncESM.esmLoader;
return loader.import(specifier, normalizeReferrerURL(filename));
},
});
36 changes: 23 additions & 13 deletions lib/internal/modules/esm/get_format.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
'use strict';
const {
ObjectAssign,
ObjectCreate,
ObjectPrototypeHasOwnProperty,
RegExpPrototypeExec,
StringPrototypeStartsWith,
} = primordials;
const { extname } = require('path');
const { getOptionValue } = require('internal/options');
@@ -36,26 +38,25 @@ if (experimentalWasmModules)
if (experimentalJsonModules)
extensionFormatMap['.json'] = legacyExtensionFormatMap['.json'] = 'json';

function defaultGetFormat(url, context, defaultGetFormatUnused) {
if (StringPrototypeStartsWith(url, 'node:')) {
return { format: 'builtin' };
}
const parsed = new URL(url);
if (parsed.protocol === 'data:') {
const protocolHandlers = ObjectAssign(ObjectCreate(null), {
'data:'(parsed) {
const { 1: mime } = RegExpPrototypeExec(
/^([^/]+\/[^;,]+)(?:[^,]*?)(;base64)?,/,
parsed.pathname,
) || [ , null ];
) || [, null];
const format = ({
'__proto__': null,
'text/javascript': 'module',
'application/json': experimentalJsonModules ? 'json' : null,
'application/wasm': experimentalWasmModules ? 'wasm' : null
})[mime] || null;
return { format };
} else if (parsed.protocol === 'file:') {

return format;
},
'file:'(parsed, url) {
const ext = extname(parsed.pathname);
let format;

if (ext === '.js') {
format = getPackageType(parsed.href) === 'module' ? 'module' : 'commonjs';
} else {
@@ -71,9 +72,18 @@ function defaultGetFormat(url, context, defaultGetFormatUnused) {
throw new ERR_UNKNOWN_FILE_EXTENSION(ext, fileURLToPath(url));
}
}
return { format: format || null };
}
return { format: null };

return format || null;
},
'node:'() { return 'builtin'; },
});

function defaultGetFormat(url, context) {
const parsed = new URL(url);

return ObjectPrototypeHasOwnProperty(protocolHandlers, parsed.protocol) ?
protocolHandlers[parsed.protocol](parsed, url) :
null;
}

module.exports = {
2 changes: 1 addition & 1 deletion lib/internal/modules/esm/get_source.js
Original file line number Diff line number Diff line change
@@ -40,6 +40,6 @@ async function defaultGetSource(url, { format } = {}, defaultGetSource) {
if (policy?.manifest) {
policy.manifest.assertIntegrity(parsed, source);
}
return { source };
return source;
}
exports.defaultGetSource = defaultGetSource;
32 changes: 32 additions & 0 deletions lib/internal/modules/esm/load.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use strict';

const { defaultGetFormat } = require('internal/modules/esm/get_format');
const { defaultGetSource } = require('internal/modules/esm/get_source');
const { translators } = require('internal/modules/esm/translators');

async function defaultLoad(url, context) {
let {
format,
source,
} = context;

if (!translators.has(format)) format = defaultGetFormat(url);

if (
format === 'builtin' ||
format === 'commonjs'
) {
source = null;
} else if (source == null) {
source = await defaultGetSource(url, { format });
}

return {
format,
source,
};
}

module.exports = {
defaultLoad,
};
553 changes: 378 additions & 175 deletions lib/internal/modules/esm/loader.js

Large diffs are not rendered by default.

10 changes: 6 additions & 4 deletions lib/internal/modules/esm/module_job.js
Original file line number Diff line number Diff line change
@@ -141,9 +141,11 @@ class ModuleJob {
const { 1: childSpecifier, 2: name } = StringPrototypeMatch(
e.message,
/module '(.*)' does not provide an export named '(.+)'/);
const childFileURL =
await this.loader.resolve(childSpecifier, parentFileUrl);
const format = await this.loader.getFormat(childFileURL);
const { url: childFileURL } = await this.loader.resolve(
childSpecifier, parentFileUrl,
);
const { format } = await this.loader.load(childFileURL);

if (format === 'commonjs') {
const importStatement = splitStack[1];
// TODO(@ctavan): The original error stack only provides the single
@@ -152,7 +154,7 @@ class ModuleJob {
// just parsing the error stack.
const oneLineNamedImports = StringPrototypeMatch(importStatement, /{.*}/);
const destructuringAssignment = oneLineNamedImports &&
StringPrototypeReplace(oneLineNamedImports, /\s+as\s+/g, ': ');
StringPrototypeReplace(oneLineNamedImports, /\s+as\s+/g, ': ');
e.message = `Named export '${name}' not found. The requested module` +
` '${childSpecifier}' is a CommonJS module, which may not support` +
' all module.exports as named exports.\nCommonJS modules can ' +
2 changes: 1 addition & 1 deletion lib/internal/modules/esm/resolve.js
Original file line number Diff line number Diff line change
@@ -114,7 +114,7 @@ function emitFolderMapDeprecation(match, pjsonUrl, isExports, base) {
* @returns
*/
function emitLegacyIndexDeprecation(url, packageJSONUrl, base, main) {
const { format } = defaultGetFormat(url);
const format = defaultGetFormat(url);
if (format !== 'module')
return;
const path = fileURLToPath(url);
7 changes: 0 additions & 7 deletions lib/internal/modules/esm/transform_source.js

This file was deleted.

51 changes: 19 additions & 32 deletions lib/internal/modules/esm/translators.js
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@ const {
ObjectGetPrototypeOf,
ObjectPrototypeHasOwnProperty,
ObjectKeys,
PromisePrototypeCatch,
PromisePrototypeThen,
PromiseReject,
SafeArrayIterator,
SafeMap,
@@ -38,10 +38,6 @@ const {
cjsParseCache
} = require('internal/modules/cjs/loader');
const internalURLModule = require('internal/url');
const { defaultGetSource } = require(
'internal/modules/esm/get_source');
const { defaultTransformSource } = require(
'internal/modules/esm/transform_source');
const createDynamicModule = require(
'internal/modules/esm/create_dynamic_module');
const { fileURLToPath, URL } = require('url');
@@ -112,13 +108,14 @@ function errPath(url) {
}

async function importModuleDynamically(specifier, { url }) {
return asyncESM.ESMLoader.import(specifier, url);
return asyncESM.esmLoader.import(specifier, url);
}

function createImportMetaResolve(defaultParentUrl) {
return async function resolve(specifier, parentUrl = defaultParentUrl) {
return PromisePrototypeCatch(
asyncESM.ESMLoader.resolve(specifier, parentUrl),
return PromisePrototypeThen(
asyncESM.esmLoader.resolve(specifier, parentUrl),
({ url }) => url,
(error) => (
error.code === 'ERR_UNSUPPORTED_DIR_IMPORT' ?
error.url : PromiseReject(error))
@@ -134,12 +131,8 @@ function initializeImportMeta(meta, { url }) {
}

// Strategy for loading a standard JavaScript module.
translators.set('module', async function moduleStrategy(url) {
let { source } = await this._getSource(
url, { format: 'module' }, defaultGetSource);
assertBufferSource(source, true, 'getSource');
({ source } = await this._transformSource(
source, { url, format: 'module' }, defaultTransformSource));
translators.set('module', async function moduleStrategy(url, source, isMain) {
assertBufferSource(source, true, 'load');
source = stringify(source);
maybeCacheSourceMap(url, source);
debug(`Translating StandardModule ${url}`);
@@ -172,7 +165,8 @@ function enrichCJSError(err, content, filename) {
// Strategy for loading a node-style CommonJS module
const isWindows = process.platform === 'win32';
const winSepRegEx = /\//g;
translators.set('commonjs', async function commonjsStrategy(url, isMain) {
translators.set('commonjs', async function commonjsStrategy(url, source,
isMain) {
debug(`Translating CJSModule ${url}`);

let filename = internalURLModule.fileURLToPath(new URL(url));
@@ -188,9 +182,9 @@ translators.set('commonjs', async function commonjsStrategy(url, isMain) {
debug(`Loading CJSModule ${url}`);

let exports;
if (asyncESM.ESMLoader.cjsCache.has(module)) {
exports = asyncESM.ESMLoader.cjsCache.get(module);
asyncESM.ESMLoader.cjsCache.delete(module);
if (asyncESM.esmLoader.cjsCache.has(module)) {
exports = asyncESM.esmLoader.cjsCache.get(module);
asyncESM.esmLoader.cjsCache.delete(module);
} else {
try {
exports = CJSModule._load(filename, undefined, isMain);
@@ -286,9 +280,9 @@ translators.set('builtin', async function builtinStrategy(url) {
});

// Strategy for loading a JSON file
translators.set('json', async function jsonStrategy(url) {
translators.set('json', async function jsonStrategy(url, source) {
emitExperimentalWarning('Importing JSON modules');
debug(`Translating JSONModule ${url}`);
assertBufferSource(source, true, 'load');
debug(`Loading JSONModule ${url}`);
const pathname = StringPrototypeStartsWith(url, 'file:') ?
fileURLToPath(url) : null;
@@ -305,11 +299,6 @@ translators.set('json', async function jsonStrategy(url) {
});
}
}
let { source } = await this._getSource(
url, { format: 'json' }, defaultGetSource);
assertBufferSource(source, true, 'getSource');
({ source } = await this._transformSource(
source, { url, format: 'json' }, defaultTransformSource));
source = stringify(source);
if (pathname) {
// A require call could have been called on the same file during loading and
@@ -347,15 +336,13 @@ translators.set('json', async function jsonStrategy(url) {
});

// Strategy for loading a wasm module
translators.set('wasm', async function(url) {
translators.set('wasm', async function(url, source) {
emitExperimentalWarning('Importing Web Assembly modules');
let { source } = await this._getSource(
url, { format: 'wasm' }, defaultGetSource);
assertBufferSource(source, false, 'getSource');
({ source } = await this._transformSource(
source, { url, format: 'wasm' }, defaultTransformSource));
assertBufferSource(source, false, 'transformSource');

assertBufferSource(source, false, 'load');

debug(`Translating WASMModule ${url}`);

let compiled;
try {
compiled = await WebAssembly.compile(source);
10 changes: 6 additions & 4 deletions lib/internal/modules/run_main.js
Original file line number Diff line number Diff line change
@@ -41,12 +41,14 @@ function shouldUseESMLoader(mainPath) {
}

function runMainESM(mainPath) {
const esmLoader = require('internal/process/esm_loader');
const { loadESM } = require('internal/process/esm_loader');
const { pathToFileURL } = require('internal/url');
handleMainPromise(esmLoader.loadESM((ESMLoader) => {

handleMainPromise(loadESM((esmLoader) => {
const main = path.isAbsolute(mainPath) ?
pathToFileURL(mainPath).href : mainPath;
return ESMLoader.import(main);
pathToFileURL(mainPath).href :
mainPath;
return esmLoader.import(main);
}));
}

52 changes: 33 additions & 19 deletions lib/internal/process/esm_loader.js
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@
const {
ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING,
} = require('internal/errors').codes;
const { Loader } = require('internal/modules/esm/loader');
const { ESMLoader } = require('internal/modules/esm/loader');
const {
hasUncaughtExceptionCaptureCallback,
} = require('internal/process/execution');
@@ -34,38 +34,52 @@ exports.importModuleDynamicallyCallback = async function(wrap, specifier) {
throw new ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING();
};

let ESMLoader = new Loader();
exports.ESMLoader = ESMLoader;
const esmLoader = new ESMLoader();

exports.esmLoader = esmLoader;

/**
* Causes side-effects: user-defined loader hooks are added to esmLoader.
* @returns {void}
*/
async function initializeLoader() {
const { getOptionValue } = require('internal/options');
const userLoader = getOptionValue('--experimental-loader');
if (!userLoader)
return;
// customLoaders CURRENTLY can be only 1 (a string)
// Once chaining is implemented, it will be string[]
const customLoaders = getOptionValue('--experimental-loader');

if (!customLoaders.length) return;

const { emitExperimentalWarning } = require('internal/util');
emitExperimentalWarning('--experimental-loader');

let cwd;
try {
cwd = process.cwd() + '/';
} catch {
cwd = 'file:///';
}
// If --experimental-loader is specified, create a loader with user hooks.
// Otherwise create the default loader.
const { emitExperimentalWarning } = require('internal/util');
emitExperimentalWarning('--experimental-loader');
return (async () => {
const hooks =
await ESMLoader.import(userLoader, pathToFileURL(cwd).href);
ESMLoader = new Loader();
ESMLoader.hook(hooks);
ESMLoader.runGlobalPreloadCode();
return exports.ESMLoader = ESMLoader;
})();

// A separate loader instance is necessary to avoid cross-contamination
// between internal Node.js and userland. For example, a module with internal
// state (such as a counter) should be independent.
const internalEsmLoader = new ESMLoader();

// Importation must be handled by internal loader to avoid poluting userland
const exports = await internalEsmLoader.import(
customLoaders,
pathToFileURL(cwd).href,
);

// Hooks must then be added to external/public loader
// (so they're triggered in userland)
await esmLoader.addCustomLoaders(exports);
}

exports.loadESM = async function loadESM(callback) {
try {
await initializeLoader();
await callback(ESMLoader);
await callback(esmLoader);
} catch (err) {
if (hasUncaughtExceptionCaptureCallback()) {
process._fatalException(err);
2 changes: 1 addition & 1 deletion lib/internal/process/execution.js
Original file line number Diff line number Diff line change
@@ -83,7 +83,7 @@ function evalScript(name, body, breakFirstLine, print) {
displayErrors: true,
[kVmBreakFirstLineSymbol]: !!breakFirstLine,
async importModuleDynamically(specifier) {
const loader = await asyncESM.ESMLoader;
const loader = await asyncESM.esmLoader;
return loader.import(specifier, baseUrl);
}
}));
4 changes: 2 additions & 2 deletions lib/repl.js
Original file line number Diff line number Diff line change
@@ -455,7 +455,7 @@ function REPLServer(prompt,
filename: file,
displayErrors: true,
importModuleDynamically: async (specifier) => {
return asyncESM.ESMLoader.import(specifier, parentURL);
return asyncESM.esmLoader.import(specifier, parentURL);
}
});
} catch (fallbackError) {
@@ -497,7 +497,7 @@ function REPLServer(prompt,
filename: file,
displayErrors: true,
importModuleDynamically: async (specifier) => {
return asyncESM.ESMLoader.import(specifier, parentURL);
return asyncESM.esmLoader.import(specifier, parentURL);
}
});
} catch (e) {
2 changes: 1 addition & 1 deletion test/es-module/test-esm-data-urls.js
Original file line number Diff line number Diff line change
@@ -99,7 +99,7 @@ function createBase64URL(mime, body) {
await import(plainESMURL);
common.mustNotCall()();
} catch (e) {
assert.strictEqual(e.code, 'ERR_INVALID_MODULE_SPECIFIER');
assert.strictEqual(e.code, 'ERR_INVALID_URL');
}
}
{
File renamed without changes.
6 changes: 0 additions & 6 deletions test/es-module/test-esm-get-source-loader.mjs

This file was deleted.

12 changes: 0 additions & 12 deletions test/es-module/test-esm-loader-get-format.mjs

This file was deleted.

4 changes: 2 additions & 2 deletions test/es-module/test-esm-loader-modulemap.js
Original file line number Diff line number Diff line change
@@ -7,15 +7,15 @@
require('../common');

const assert = require('assert');
const { Loader } = require('internal/modules/esm/loader');
const { ESMLoader } = require('internal/modules/esm/loader');
const ModuleMap = require('internal/modules/esm/module_map');
const ModuleJob = require('internal/modules/esm/module_job');
const createDynamicModule = require(
'internal/modules/esm/create_dynamic_module');

const stubModuleUrl = new URL('file://tmp/test');
const stubModule = createDynamicModule(['default'], stubModuleUrl);
const loader = new Loader();
const loader = new ESMLoader();
const moduleMap = new ModuleMap();
const moduleJob = new ModuleJob(loader, stubModule.module,
() => new Promise(() => {}));
40 changes: 40 additions & 0 deletions test/es-module/test-esm-loader-stringify-text.mjs
Original file line number Diff line number Diff line change
@@ -12,6 +12,34 @@ 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) => {
@@ -38,6 +66,18 @@ import('test:String').then(
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'),
83 changes: 83 additions & 0 deletions test/es-module/test-esm-loader.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// 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 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);
});
8 changes: 0 additions & 8 deletions test/es-module/test-esm-resolve-hook.mjs

This file was deleted.

6 changes: 0 additions & 6 deletions test/es-module/test-esm-transform-source-loader.mjs

This file was deleted.

18 changes: 6 additions & 12 deletions test/fixtures/es-module-loaders/builtin-named-exports-loader.mjs
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import module from 'module';

const GET_BUILTIN = `$__get_builtin_hole_${Date.now()}`;

export function getGlobalPreloadCode() {
export function globalPreload() {
return `Object.defineProperty(globalThis, ${JSON.stringify(GET_BUILTIN)}, {
value: (builtinName) => {
return getBuiltin(builtinName);
@@ -13,8 +13,9 @@ export function getGlobalPreloadCode() {
`;
}

export function resolve(specifier, context, defaultResolve) {
const def = defaultResolve(specifier, context);
export function resolve(specifier, context, next) {
const def = next(specifier, context);

if (def.url.startsWith('node:')) {
return {
url: `custom-${def.url}`,
@@ -23,22 +24,15 @@ export function resolve(specifier, context, defaultResolve) {
return def;
}

export function getSource(url, context, defaultGetSource) {
export function load(url, context, next) {
if (url.startsWith('custom-node:')) {
const urlObj = new URL(url);
return {
source: generateBuiltinModule(urlObj.pathname),
format: 'module',
};
}
return defaultGetSource(url, context);
}

export function getFormat(url, context, defaultGetFormat) {
if (url.startsWith('custom-node:')) {
return { format: 'module' };
}
return defaultGetFormat(url, context, defaultGetFormat);
return next(url, context);
}

function generateBuiltinModule(builtinName) {
10 changes: 0 additions & 10 deletions test/fixtures/es-module-loaders/get-source.mjs

This file was deleted.

69 changes: 69 additions & 0 deletions test/fixtures/es-module-loaders/hooks-custom.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import count from '../es-modules/stateful.mjs';


// Arbitrary instance of manipulating a module's internal state
// used to assert node-land and user-land have different contexts
count();

/**
* @param {string} url A fully resolved file url.
* @param {object} context Additional info.
* @param {function} next for now, next is defaultLoad a wrapper for
* defaultGetFormat + defaultGetSource
* @returns {{ format: string, source: (string|SharedArrayBuffer|Uint8Array) }}
*/
export function load(url, context, next) {
// Load all .js files as ESM, regardless of package scope
if (url.endsWith('.js')) return next(url, {
...context,
format: 'module',
});

if (url.endsWith('.ext')) return next(url, {
...context,
format: 'module',
});

if (url === 'esmHook/badReturnVal.mjs') return 'export function returnShouldBeObject() {}';

if (url === 'esmHook/badReturnFormatVal.mjs') return {
format: Array(0),
source: '',
}
if (url === 'esmHook/unsupportedReturnFormatVal.mjs') return {
format: 'foo', // Not one of the allowable inputs: no translator named 'foo'
source: '',
}

if (url === 'esmHook/badReturnSourceVal.mjs') return {
format: 'module',
source: Array(0),
}

if (url === 'esmHook/preknownFormat.pre') return {
format: context.format,
source: `const msg = 'hello world'; export default msg;`
};

if (url === 'esmHook/virtual.mjs') return {
format: 'module',
source: `export const message = 'Woohoo!'.toUpperCase();`,
};

return next(url, context, next);
}

export function resolve(specifier, context, next) {
let format = '';

if (specifier === 'esmHook/format.false') format = false;
if (specifier === 'esmHook/format.true') format = true;
if (specifier === 'esmHook/preknownFormat.pre') format = 'module';

if (specifier.startsWith('esmHook')) return {
format,
url: specifier,
};

return next(specifier, context, next);
}
22 changes: 22 additions & 0 deletions test/fixtures/es-module-loaders/hooks-obsolete.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export function dynamicInstantiate() {}
export function getFormat() {}
export function getSource() {}
export function transformSource() {}


export function load(url, context, next) {
if (url === 'whatever') return {
format: 'module',
source: '',
};

return next(url, context, next);
}

export function resolve(specifier, context, next) {
if (specifier === 'whatever') return {
url: specifier,
};

return next(specifier, context, next);
}
9 changes: 0 additions & 9 deletions test/fixtures/es-module-loaders/js-loader.mjs

This file was deleted.

7 changes: 4 additions & 3 deletions test/fixtures/es-module-loaders/loader-invalid-format.mjs
Original file line number Diff line number Diff line change
@@ -7,11 +7,12 @@ export async function resolve(specifier, { parentURL }, defaultResolve) {
return defaultResolve(specifier, {parentURL}, defaultResolve);
}

export function getFormat(url, context, defaultGetFormat) {
export async function load(url, context, next) {
if (url === 'file:///asdf') {
return {
format: 'esm'
format: 'esm',
source: '',
}
}
return defaultGetFormat(url, context, defaultGetFormat);
return next(url, context, next);
}
20 changes: 5 additions & 15 deletions test/fixtures/es-module-loaders/loader-unknown-builtin-module.mjs
Original file line number Diff line number Diff line change
@@ -1,17 +1,7 @@
export async function resolve(specifier, { parentURL }, defaultResolve) {
if (specifier === 'unknown-builtin-module') {
return {
url: 'node:unknown-builtin-module'
};
}
return defaultResolve(specifier, {parentURL}, defaultResolve);
}
export function resolve(specifier, context, next) {
if (specifier === 'unknown-builtin-module') return {
url: 'node:unknown-builtin-module'
};

export async function getFormat(url, context, defaultGetFormat) {
if (url === 'node:unknown-builtin-module') {
return {
format: 'builtin'
};
}
return defaultGetFormat(url, context, defaultGetFormat);
return next(specifier, context, next);
}
30 changes: 19 additions & 11 deletions test/fixtures/es-module-loaders/string-sources.mjs
Original file line number Diff line number Diff line change
@@ -2,29 +2,37 @@ const SOURCES = {
__proto__: null,
'test:Array': ['1', '2'], // both `1,2` and `12` are valid ESM
'test:ArrayBuffer': new ArrayBuffer(0),
'test:BigInt64Array': new BigInt64Array(0),
'test:BigUint64Array': new BigUint64Array(0),
'test:Float32Array': new Float32Array(0),
'test:Float64Array': new Float64Array(0),
'test:Int8Array': new Int8Array(0),
'test:Int16Array': new Int16Array(0),
'test:Int32Array': new Int32Array(0),
'test:null': null,
'test:Object': {},
'test:SharedArrayBuffer': new SharedArrayBuffer(0),
'test:string': '',
'test:String': new String(''),
'test:Uint8Array': new Uint8Array(0),
'test:Uint8ClampedArray': new Uint8ClampedArray(0),
'test:Uint16Array': new Uint16Array(0),
'test:Uint32Array': new Uint32Array(0),
'test:undefined': undefined,
}
export function resolve(specifier, context, defaultFn) {
export function resolve(specifier, context, next) {
if (specifier.startsWith('test:')) {
return { url: specifier };
}
return defaultFn(specifier, context);
return next(specifier, context);
}
export function getFormat(href, context, defaultFn) {

export function load(href, context, next) {
if (href.startsWith('test:')) {
return { format: 'module' };
return {
format: 'module',
source: SOURCES[href],
};
}
return defaultFn(href, context);
}
export function getSource(href, context, defaultFn) {
if (href.startsWith('test:')) {
return { source: SOURCES[href] };
}
return defaultFn(href, context);
return next(href, context);
}
14 changes: 0 additions & 14 deletions test/fixtures/es-module-loaders/transform-source.mjs

This file was deleted.

3 changes: 3 additions & 0 deletions test/fixtures/es-modules/file.ext
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// fixtures/es-module-loader.mjs tells node to treat this file like ESM

export default function iAmReal() { return true };
Empty file.
5 changes: 5 additions & 0 deletions test/fixtures/es-modules/stateful.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
let counter = 0;

export default function count() {
return ++counter;
}
3 changes: 2 additions & 1 deletion test/message/esm_display_syntax_error.out
Original file line number Diff line number Diff line change
@@ -3,7 +3,8 @@ await async () => 0;
^^^^^

SyntaxError: Unexpected reserved word
at Loader.moduleStrategy (node:internal/modules/esm/translators:*:*)
at ESMLoader.moduleStrategy (node:internal/modules/esm/translators:*:*)
at ESMLoader.moduleProvider (node:internal/modules/esm/loader:*:*)
at async link (node:internal/modules/esm/module_job:*:*)

Node.js *
5 changes: 3 additions & 2 deletions test/message/esm_display_syntax_error_import.out
Original file line number Diff line number Diff line change
@@ -4,8 +4,9 @@ file:///*/test/message/esm_display_syntax_error_import.mjs:5
SyntaxError: The requested module '../fixtures/es-module-loaders/module-named-exports.mjs' does not provide an export named 'notfound'
at ModuleJob._instantiate (node:internal/modules/esm/module_job:*:*)
at async ModuleJob.run (node:internal/modules/esm/module_job:*:*)
at async Loader.import (node:internal/modules/esm/loader:*:*)
at async Object.loadESM (node:internal/process/esm_loader:*:*)
at async Promise.all (index 0)
at async ESMLoader.import (node:internal/modules/esm/loader:*:*)
at async loadESM (node:internal/process/esm_loader:*:*)
at async handleMainPromise (node:internal/modules/run_main:*:*)

Node.js *
5 changes: 3 additions & 2 deletions test/message/esm_display_syntax_error_import_module.out
Original file line number Diff line number Diff line change
@@ -4,8 +4,9 @@ import { foo, notfound } from './module-named-exports.mjs';
SyntaxError: The requested module './module-named-exports.mjs' does not provide an export named 'notfound'
at ModuleJob._instantiate (node:internal/modules/esm/module_job:*:*)
at async ModuleJob.run (node:internal/modules/esm/module_job:*:*)
at async Loader.import (node:internal/modules/esm/loader:*:*)
at async Object.loadESM (node:internal/process/esm_loader:*:*)
at async Promise.all (index 0)
at async ESMLoader.import (node:internal/modules/esm/loader:*:*)
at async loadESM (node:internal/process/esm_loader:*:*)
at async handleMainPromise (node:internal/modules/run_main:*:*)

Node.js *
3 changes: 2 additions & 1 deletion test/message/esm_display_syntax_error_module.out
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ await async () => 0;
^^^^^^^^^^^^^

SyntaxError: Malformed arrow function parameter list
at Loader.moduleStrategy (node:internal/modules/esm/translators:*:*)
at ESMLoader.moduleStrategy (node:internal/modules/esm/translators:*:*)
at ESMLoader.moduleProvider (node:internal/modules/esm/loader:*:*)

Node.js *
12 changes: 6 additions & 6 deletions test/message/esm_loader_not_found.out
Original file line number Diff line number Diff line change
@@ -7,13 +7,13 @@ Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'i-dont-exist' imported from *
at new NodeError (node:internal/errors:*:*)
at packageResolve (node:internal/modules/esm/resolve:*:*)
at moduleResolve (node:internal/modules/esm/resolve:*:*)
at Loader.defaultResolve [as _resolve] (node:internal/modules/esm/resolve:*:*)
at Loader.resolve (node:internal/modules/esm/loader:*:*)
at Loader.getModuleJob (node:internal/modules/esm/loader:*:*)
at Loader.import (node:internal/modules/esm/loader:*:*)
at node:internal/process/esm_loader:*:*
at defaultResolve (node:internal/modules/esm/resolve:*:*)
at ESMLoader.resolve (node:internal/modules/esm/loader:*:*)
at ESMLoader.getModuleJob (node:internal/modules/esm/loader:*:*)
at ESMLoader.import (node:internal/modules/esm/loader:*:*)
at initializeLoader (node:internal/process/esm_loader:*:*)
at Object.loadESM (node:internal/process/esm_loader:*:*) {
at loadESM (node:internal/process/esm_loader:*:*)
at runMainESM (node:internal/modules/run_main:*:*) {
code: 'ERR_MODULE_NOT_FOUND'
}

6 changes: 3 additions & 3 deletions test/message/esm_loader_not_found_cjs_hint_bare.out
Original file line number Diff line number Diff line change
@@ -7,9 +7,9 @@ Did you mean to import some_module/obj.js?
at new NodeError (node:internal/errors:*:*)
at finalizeResolution (node:internal/modules/esm/resolve:*:*)
at moduleResolve (node:internal/modules/esm/resolve:*:*)
at Loader.defaultResolve [as _resolve] (node:internal/modules/esm/resolve:*:*)
at Loader.resolve (node:internal/modules/esm/loader:*:*)
at Loader.getModuleJob (node:internal/modules/esm/loader:*:*)
at defaultResolve (node:internal/modules/esm/resolve:*:*)
at ESMLoader.resolve (node:internal/modules/esm/loader:*:*)
at ESMLoader.getModuleJob (node:internal/modules/esm/loader:*:*)
at ModuleWrap.<anonymous> (node:internal/modules/esm/module_job:*:*)
at link (node:internal/modules/esm/module_job:*:*) {
code: 'ERR_MODULE_NOT_FOUND'
12 changes: 6 additions & 6 deletions test/message/esm_loader_not_found_cjs_hint_relative.out
Original file line number Diff line number Diff line change
@@ -9,13 +9,13 @@ Did you mean to import ./test/common/fixtures.js?
at new NodeError (node:internal/errors:*:*)
at finalizeResolution (node:internal/modules/esm/resolve:*:*)
at moduleResolve (node:internal/modules/esm/resolve:*:*)
at Loader.defaultResolve [as _resolve] (node:internal/modules/esm/resolve:*:*)
at Loader.resolve (node:internal/modules/esm/loader:*:*)
at Loader.getModuleJob (node:internal/modules/esm/loader:*:*)
at Loader.import (node:internal/modules/esm/loader:*:*)
at node:internal/process/esm_loader:*:*
at defaultResolve (node:internal/modules/esm/resolve:*:*)
at ESMLoader.resolve (node:internal/modules/esm/loader:*:*)
at ESMLoader.getModuleJob (node:internal/modules/esm/loader:*:*)
at ESMLoader.import (node:internal/modules/esm/loader:*:*)
at initializeLoader (node:internal/process/esm_loader:*:*)
at Object.loadESM (node:internal/process/esm_loader:*:*) {
at loadESM (node:internal/process/esm_loader:*:*)
at runMainESM (node:internal/modules/run_main:*:*) {
code: 'ERR_MODULE_NOT_FOUND'
}

3 changes: 2 additions & 1 deletion test/message/esm_loader_syntax_error.out
Original file line number Diff line number Diff line change
@@ -5,7 +5,8 @@ await async () => 0;
^^^^^^^^^^^^^

SyntaxError: Malformed arrow function parameter list
at Loader.moduleStrategy (node:internal/modules/esm/translators:*:*)
at ESMLoader.moduleStrategy (node:internal/modules/esm/translators:*:*)
at ESMLoader.moduleProvider (node:internal/modules/esm/loader:*:*)
at async link (node:internal/modules/esm/module_job:*:*)

Node.js *
4 changes: 4 additions & 0 deletions test/message/test-esm-loader-obsolete-hooks.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Flags: --no-warnings --throw-deprecation --experimental-loader ./test/fixtures/es-module-loaders/hooks-obsolete.mjs
/* eslint-disable node-core/require-common-first, node-core/required-modules */

await import('whatever');
11 changes: 11 additions & 0 deletions test/message/test-esm-loader-obsolete-hooks.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
node:internal/process/warning:*
throw warning;
^

DeprecationWarning: Obsolete loader hook(s) supplied and will be ignored: dynamicInstantiate, getFormat, getSource, transformSource
at Function.pluckHooks (node:internal/modules/esm/loader:*:*)
at ESMLoader.addCustomLoaders (node:internal/modules/esm/loader:*:*)
at initializeLoader (node:internal/process/esm_loader:*:*)
at async loadESM (node:internal/process/esm_loader:*:*)
at async handleMainPromise (node:internal/modules/run_main:*:*)
Node.js *
2 changes: 1 addition & 1 deletion test/parallel/test-bootstrap-modules.js
Original file line number Diff line number Diff line change
@@ -72,10 +72,10 @@ const expectedModules = new Set([
'NativeModule internal/modules/esm/get_format',
'NativeModule internal/modules/esm/get_source',
'NativeModule internal/modules/esm/loader',
'NativeModule internal/modules/esm/load',
'NativeModule internal/modules/esm/module_job',
'NativeModule internal/modules/esm/module_map',
'NativeModule internal/modules/esm/resolve',
'NativeModule internal/modules/esm/transform_source',
'NativeModule internal/modules/esm/translators',
'NativeModule internal/process/esm_loader',
'NativeModule internal/options',

0 comments on commit df22736

Please sign in to comment.