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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,402 @@
'use strict';
const {
ArrayPrototypeFindIndex,
ArrayPrototypePush,
ArrayPrototypeSplice,
ObjectAssign,
ObjectFreeze,
StringPrototypeStartsWith,
Symbol,
} = primordials;
const {
isAnyArrayBuffer,
isArrayBufferView,
} = require('internal/util/types');
const { BuiltinModule } = require('internal/bootstrap/realm');
const {
ERR_INVALID_RETURN_PROPERTY_VALUE,
} = require('internal/errors').codes;
const { validateFunction } = require('internal/validators');
const { isAbsolute } = require('path');
const { pathToFileURL, fileURLToPath } = require('internal/url');
let debug = require('internal/util/debuglog').debuglog('module_hooks', (fn) => {
debug = fn;
});
/**
* @typedef {import('internal/modules/cjs/loader.js').Module} Module
*/
/**
* @typedef {(
* specifier: string,
* context: Partial<ModuleResolveContext>,
* ) => ModuleResolveResult
* } NextResolve
* @typedef {(
* specifier: string,
* context: ModuleResolveContext,
* nextResolve: NextResolve,
* ) => ModuleResolveResult
* } ResolveHook
* @typedef {(
* url: string,
* context: Partial<ModuleLoadContext>,
* ) => ModuleLoadResult
* } NextLoad
* @typedef {(
* url: string,
* context: ModuleLoadContext,
* nextLoad: NextLoad,
* ) => ModuleLoadResult
* } LoadHook
*/
// Use arrays for better insertion and iteration performance, we don't care
// about deletion performance as much.
/** @type {ResolveHook[]} */
const resolveHooks = [];
/** @type {LoadHook[]} */
const loadHooks = [];
const hookId = Symbol('kModuleHooksIdKey');
let nextHookId = 0;
class ModuleHooks {
/**
* @param {ResolveHook|undefined} resolve User-provided hook.
* @param {LoadHook|undefined} load User-provided hook.
*/
constructor(resolve, load) {
this[hookId] = Symbol(`module-hook-${nextHookId++}`);
// Always initialize all hooks, if it's unspecified it'll be an owned undefined.
this.resolve = resolve;
this.load = load;
if (resolve) {
ArrayPrototypePush(resolveHooks, this);
}
if (load) {
ArrayPrototypePush(loadHooks, this);
}
ObjectFreeze(this);
}
// TODO(joyeecheung): we may want methods that allow disabling/enabling temporarily
// which just sets the item in the array to undefined temporarily.
// TODO(joyeecheung): this can be the [Symbol.dispose] implementation to pair with
// `using` when the explicit resource management proposal is shipped by V8.
/**
* Deregister the hook instance.
*/
deregister() {
const id = this[hookId];
let index = ArrayPrototypeFindIndex(resolveHooks, (hook) => hook[hookId] === id);
if (index !== -1) {
ArrayPrototypeSplice(resolveHooks, index, 1);
}
index = ArrayPrototypeFindIndex(loadHooks, (hook) => hook[hookId] === id);
if (index !== -1) {
ArrayPrototypeSplice(loadHooks, index, 1);
}
}
};
/**
* TODO(joyeecheung): taken an optional description?
* @param {{ resolve?: ResolveHook, load?: LoadHook }} hooks User-provided hooks
* @returns {ModuleHooks}
*/
function registerHooks(hooks) {
const { resolve, load } = hooks;
if (resolve) {
validateFunction(resolve, 'hooks.resolve');
}
if (load) {
validateFunction(load, 'hooks.load');
}
return new ModuleHooks(resolve, load);
}
/**
* @param {string} filename
* @returns {string}
*/
function convertCJSFilenameToURL(filename) {
if (!filename) { return filename; }
const builtinId = BuiltinModule.normalizeRequirableId(filename);
if (builtinId) {
return `node:${builtinId}`;
}
// Handle the case where filename is neither a path, nor a built-in id,
// which is possible via monkey-patching.
if (isAbsolute(filename)) {
return pathToFileURL(filename).href;
}
return filename;
}
/**
* @param {string} url
* @returns {string}
*/
function convertURLToCJSFilename(url) {
if (!url) { return url; }
const builtinId = BuiltinModule.normalizeRequirableId(url);
if (builtinId) {
return builtinId;
}
if (StringPrototypeStartsWith(url, 'file://')) {
return fileURLToPath(url);
}
return url;
}
/**
* Convert a list of hooks into a function that can be used to do an operation through
* a chain of hooks. If any of the hook returns without calling the next hook, it
* must return shortCircuit: true to stop the chain from continuing to avoid
* forgetting to invoke the next hook by mistake.
* @param {ModuleHooks[]} hooks A list of hooks whose last argument is `nextHook`.
* @param {'load'|'resolve'} name Name of the hook in ModuleHooks.
* @param {Function} defaultStep The default step in the chain.
* @param {Function} validate A function that validates and sanitize the result returned by the chain.
*/
function buildHooks(hooks, name, defaultStep, validate, mergedContext) {
let lastRunIndex = hooks.length;
/**
* Helper function to wrap around invocation of user hook or the default step
* in order to fill in missing arguments or check returned results.
* Due to the merging of the context, this must be a closure.
* @param {number} index Index in the chain. Default step is 0, last added hook is 1,
* and so on.
* @param {Function} userHookOrDefault Either the user hook or the default step to invoke.
* @param {Function|undefined} next The next wrapped step. If this is the default step, it's undefined.
* @returns {Function} Wrapped hook or default step.
*/
function wrapHook(index, userHookOrDefault, next) {
return function nextStep(arg0, context) {
lastRunIndex = index;
if (context && context !== mergedContext) {
ObjectAssign(mergedContext, context);
}
const hookResult = userHookOrDefault(arg0, mergedContext, next);
if (lastRunIndex > 0 && lastRunIndex === index && !hookResult.shortCircuit) {
throw new ERR_INVALID_RETURN_PROPERTY_VALUE('true', name, 'shortCircuit',
hookResult.shortCircuit);
}
return validate(arg0, mergedContext, hookResult);
};
}
const chain = [wrapHook(0, defaultStep)];
for (let i = 0; i < hooks.length; ++i) {
const wrappedHook = wrapHook(i + 1, hooks[i][name], chain[i]);
ArrayPrototypePush(chain, wrappedHook);
}
return chain[chain.length - 1];
}
/**
* @typedef {object} ModuleResolveResult
* @property {string} url Resolved URL of the module.
* @property {string|undefined} format Format of the module.
* @property {ImportAttributes|undefined} importAttributes Import attributes for the request.
* @property {boolean|undefined} shortCircuit Whether the next hook has been skipped.
*/
/**
* Validate the result returned by a chain of resolve hook.
* @param {string} specifier Specifier passed into the hooks.
* @param {ModuleResolveContext} context Context passed into the hooks.
* @param {ModuleResolveResult} result Result produced by resolve hooks.
* @returns {ModuleResolveResult}
*/
function validateResolve(specifier, context, result) {
const { url, format, importAttributes } = result;
if (typeof url !== 'string') {
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
'a URL string',
'resolve',
'url',
url,
);
}
if (format && typeof format !== 'string') {
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
'a string',
'resolve',
'format',
format,
);
}
if (importAttributes && typeof importAttributes !== 'object') {
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
'an object',
'resolve',
'importAttributes',
importAttributes,
);
}
return {
__proto__: null,
url,
format,
importAttributes,
};
}
/**
* @typedef {object} ModuleLoadResult
* @property {string|undefined} format Format of the loaded module.
* @property {string|ArrayBuffer|TypedArray} source Source code of the module.
* @property {boolean|undefined} shortCircuit Whether the next hook has been skipped.
*/
/**
* Validate the result returned by a chain of resolve hook.
* @param {string} url URL passed into the hooks.
* @param {ModuleLoadContext} context Context passed into the hooks.
* @param {ModuleLoadResult} result Result produced by load hooks.
* @returns {ModuleLoadResult}
*/
function validateLoad(url, context, result) {
const { source, format } = result;
// To align with module.register(), the load hooks are still invoked for
// the builtins even though the default load step only provides null as source,
// and any source content for builtins provided by the user hooks are ignored.
if (!StringPrototypeStartsWith(url, 'node:') &&
typeof result.source !== 'string' &&
!isAnyArrayBuffer(source) &&
!isArrayBufferView(source)) {
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
'a string, an ArrayBuffer, or a TypedArray',
'load',
'source',
source,
);
}
if (typeof format !== 'string' && format !== undefined) {
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
'a string',
'load',
'format',
format,
);
}
return {
__proto__: null,
format,
source,
};
}
class ModuleResolveContext {
/**
* Context for the resolve hook.
* @param {string|undefined} parentURL Parent URL.
* @param {ImportAttributes|undefined} importAttributes Import attributes.
* @param {string[]} conditions Conditions.
*/
constructor(parentURL, importAttributes, conditions) {
this.parentURL = parentURL;
this.importAttributes = importAttributes;
this.conditions = conditions;
// TODO(joyeecheung): a field to differentiate between require and import?
}
};
class ModuleLoadContext {
/**
* Context for the load hook.
* @param {string|undefined} format URL.
* @param {ImportAttributes|undefined} importAttributes Import attributes.
* @param {string[]} conditions Conditions.
*/
constructor(format, importAttributes, conditions) {
this.format = format;
this.importAttributes = importAttributes;
this.conditions = conditions;
}
};
let decoder;
/**
* Load module source for a url, through a hooks chain if it exists.
* @param {string} url
* @param {string|undefined} originalFormat
* @param {ImportAttributes|undefined} importAttributes
* @param {string[]} conditions
* @param {(url: string, context: ModuleLoadContext) => ModuleLoadResult} defaultLoad
* @returns {ModuleLoadResult}
*/
function loadWithHooks(url, originalFormat, importAttributes, conditions, defaultLoad) {
debug('loadWithHooks', url, originalFormat);
const context = new ModuleLoadContext(originalFormat, importAttributes, conditions);
if (loadHooks.length === 0) {
return defaultLoad(url, context);
}
const runner = buildHooks(loadHooks, 'load', defaultLoad, validateLoad, context);
const result = runner(url, context);
const { source, format } = result;
if (!isAnyArrayBuffer(source) && !isArrayBufferView(source)) {
return result;
}
switch (format) {
// Text formats:
case undefined:
case 'module':
case 'commonjs':
case 'json':
case 'module-typescript':
case 'commonjs-typescript':
case 'typescript': {
decoder ??= new (require('internal/encoding').TextDecoder)();
result.source = decoder.decode(source);
break;
}
default:
break;
}
return result;
}
/**
* Resolve module request to a url, through a hooks chain if it exists.
* @param {string} specifier
* @param {string|undefined} parentURL
* @param {ImportAttributes|undefined} importAttributes
* @param {string[]} conditions
* @param {(specifier: string, context: ModuleResolveContext) => ModuleResolveResult} defaultResolve
* @returns {ModuleResolveResult}
*/
function resolveWithHooks(specifier, parentURL, importAttributes, conditions, defaultResolve) {
debug('resolveWithHooks', specifier, parentURL, importAttributes);
const context = new ModuleResolveContext(parentURL, importAttributes, conditions);
if (resolveHooks.length === 0) {
return defaultResolve(specifier, context);
}
const runner = buildHooks(resolveHooks, 'resolve', defaultResolve, validateResolve, context);
return runner(specifier, context);
}
module.exports = {
convertCJSFilenameToURL,
convertURLToCJSFilename,
loadHooks,
loadWithHooks,
registerHooks,
resolveHooks,
resolveWithHooks,
};

View File

@ -0,0 +1,116 @@
'use strict';
const {
ArrayPrototypeFilter,
ArrayPrototypeIncludes,
ObjectKeys,
ObjectPrototypeHasOwnProperty,
ObjectValues,
} = primordials;
const { validateString } = require('internal/validators');
const {
ERR_IMPORT_ATTRIBUTE_TYPE_INCOMPATIBLE,
ERR_IMPORT_ATTRIBUTE_MISSING,
ERR_IMPORT_ATTRIBUTE_UNSUPPORTED,
} = require('internal/errors').codes;
// The HTML spec has an implied default type of `'javascript'`.
const kImplicitTypeAttribute = 'javascript';
/**
* Define a map of module formats to import attributes types (the value of
* `type` in `with { type: 'json' }`).
* @type {Map<string, string>}
*/
const formatTypeMap = {
'__proto__': null,
'builtin': kImplicitTypeAttribute,
'commonjs': kImplicitTypeAttribute,
'json': 'json',
'module': kImplicitTypeAttribute,
'wasm': kImplicitTypeAttribute, // It's unclear whether the HTML spec will require an type attribute or not for Wasm; see https://github.com/WebAssembly/esm-integration/issues/42
};
/**
* The HTML spec disallows the default type to be explicitly specified
* (for now); so `import './file.js'` is okay but
* `import './file.js' with { type: 'javascript' }` throws.
* @type {Array<string, string>}
*/
const supportedTypeAttributes = ArrayPrototypeFilter(
ObjectValues(formatTypeMap),
(type) => type !== kImplicitTypeAttribute);
/**
* Test a module's import attributes.
* @param {string} url The URL of the imported module, for error reporting.
* @param {string} format One of Node's supported translators
* @param {Record<string, string>} importAttributes Validations for the
* module import.
* @returns {true}
* @throws {TypeError} If the format and type attribute are incompatible.
*/
function validateAttributes(url, format,
importAttributes = { __proto__: null }) {
const keys = ObjectKeys(importAttributes);
for (let i = 0; i < keys.length; i++) {
if (keys[i] !== 'type') {
throw new ERR_IMPORT_ATTRIBUTE_UNSUPPORTED(keys[i], importAttributes[keys[i]], url);
}
}
const validType = formatTypeMap[format];
switch (validType) {
case undefined:
// Ignore attributes for module formats we don't recognize, to allow new
// formats in the future.
return true;
case kImplicitTypeAttribute:
// This format doesn't allow an import type attribute, so the property
// must not be set on the import attributes object.
if (!ObjectPrototypeHasOwnProperty(importAttributes, 'type')) {
return true;
}
return handleInvalidType(url, importAttributes.type);
case importAttributes.type:
// The type attribute is the valid type for this format.
return true;
default:
// There is an expected type for this format, but the value of
// `importAttributes.type` might not have been it.
if (!ObjectPrototypeHasOwnProperty(importAttributes, 'type')) {
// `type` wasn't specified at all.
throw new ERR_IMPORT_ATTRIBUTE_MISSING(url, 'type', validType);
}
return handleInvalidType(url, importAttributes.type);
}
}
/**
* Throw the correct error depending on what's wrong with the type attribute.
* @param {string} url The resolved URL for the module to be imported
* @param {string} type The value of the import attributes' `type` property
*/
function handleInvalidType(url, type) {
// `type` might have not been a string.
validateString(type, 'type');
// `type` might not have been one of the types we understand.
if (!ArrayPrototypeIncludes(supportedTypeAttributes, type)) {
throw new ERR_IMPORT_ATTRIBUTE_UNSUPPORTED('type', type, url);
}
// `type` was the wrong value for this format.
throw new ERR_IMPORT_ATTRIBUTE_TYPE_INCOMPATIBLE(url, type);
}
module.exports = {
kImplicitTypeAttribute,
validateAttributes,
};

View File

@ -0,0 +1,95 @@
'use strict';
const {
ArrayPrototypeJoin,
ArrayPrototypeMap,
JSONStringify,
SafeSet,
} = primordials;
let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
debug = fn;
});
/**
* Creates an import statement for a given module path and index.
* @param {string} impt - The module path to import.
* @param {number} index - The index of the import statement.
*/
function createImport(impt, index) {
const imptPath = JSONStringify(impt);
return `import * as $import_${index} from ${imptPath};
import.meta.imports[${imptPath}] = $import_${index};`;
}
/**
* Creates an export for a given module.
* @param {string} expt - The name of the export.
* @param {number} index - The index of the export statement.
*/
function createExport(expt, index) {
const nameStringLit = JSONStringify(expt);
return `let $export_${index};
export { $export_${index} as ${nameStringLit} };
import.meta.exports[${nameStringLit}] = {
get: () => $export_${index},
set: (v) => $export_${index} = v,
};`;
}
/**
* Creates a dynamic module with the given imports, exports, URL, and evaluate function.
* @param {string[]} imports - An array of imports.
* @param {string[]} exports - An array of exports.
* @param {string} [url=''] - The URL of the module.
* @param {(reflect: DynamicModuleReflect) => void} evaluate - The function to evaluate the module.
* @typedef {object} DynamicModuleReflect
* @property {Record<string, Record<string, any>>} imports - The imports of the module.
* @property {string[]} exports - The exports of the module.
* @property {(cb: (reflect: DynamicModuleReflect) => void) => void} onReady - Callback to evaluate the module.
*/
const createDynamicModule = (imports, exports, url = '', evaluate) => {
debug('creating ESM facade for %s with exports: %j', url, exports);
const source = `
${ArrayPrototypeJoin(ArrayPrototypeMap(imports, createImport), '\n')}
${ArrayPrototypeJoin(ArrayPrototypeMap(exports, createExport), '\n')}
import.meta.done();
`;
const { registerModule, compileSourceTextModule } = require('internal/modules/esm/utils');
const m = compileSourceTextModule(`${url}`, source);
const readyfns = new SafeSet();
/** @type {DynamicModuleReflect} */
const reflect = {
exports: { __proto__: null },
onReady: (cb) => { readyfns.add(cb); },
};
if (imports.length) {
reflect.imports = { __proto__: null };
}
registerModule(m, {
__proto__: null,
initializeImportMeta: (meta, wrap) => {
meta.exports = reflect.exports;
if (reflect.imports) {
meta.imports = reflect.imports;
}
meta.done = () => {
evaluate(reflect);
reflect.onReady = (cb) => cb(reflect);
for (const fn of readyfns) {
readyfns.delete(fn);
fn(reflect);
}
};
},
});
return {
module: m,
reflect,
};
};
module.exports = createDynamicModule;

View File

@ -0,0 +1,74 @@
'use strict';
const {
RegExpPrototypeExec,
} = primordials;
const { getOptionValue } = require('internal/options');
const { getValidatedPath } = require('internal/fs/utils');
const fsBindings = internalBinding('fs');
const { internal: internalConstants } = internalBinding('constants');
const experimentalWasmModules = getOptionValue('--experimental-wasm-modules');
const experimentalAddonModules = getOptionValue('--experimental-addon-modules');
const extensionFormatMap = {
'__proto__': null,
'.cjs': 'commonjs',
'.js': 'module',
'.json': 'json',
'.mjs': 'module',
};
if (experimentalWasmModules) {
extensionFormatMap['.wasm'] = 'wasm';
}
if (experimentalAddonModules) {
extensionFormatMap['.node'] = 'addon';
}
if (getOptionValue('--experimental-strip-types')) {
extensionFormatMap['.ts'] = 'module-typescript';
extensionFormatMap['.mts'] = 'module-typescript';
extensionFormatMap['.cts'] = 'commonjs-typescript';
}
/**
* @param {string} mime
* @returns {string | null}
*/
function mimeToFormat(mime) {
if (
RegExpPrototypeExec(
/^\s*(text|application)\/javascript\s*(;\s*charset=utf-?8\s*)?$/i,
mime,
) !== null
) { return 'module'; }
if (mime === 'application/json') { return 'json'; }
if (experimentalWasmModules && mime === 'application/wasm') { return 'wasm'; }
return null;
}
/**
* For extensionless files in a `module` package scope, we check the file contents to disambiguate between ES module
* JavaScript and Wasm.
* We do this by taking advantage of the fact that all Wasm files start with the header `0x00 0x61 0x73 0x6d` (`_asm`).
* @param {URL} url
*/
function getFormatOfExtensionlessFile(url) {
if (!experimentalWasmModules) { return 'module'; }
const path = getValidatedPath(url);
switch (fsBindings.getFormatOfExtensionlessFile(path)) {
case internalConstants.EXTENSIONLESS_FORMAT_WASM:
return 'wasm';
default:
return 'module';
}
}
module.exports = {
extensionFormatMap,
getFormatOfExtensionlessFile,
mimeToFormat,
};

View File

@ -0,0 +1,217 @@
'use strict';
const {
ObjectPrototypeHasOwnProperty,
RegExpPrototypeExec,
SafeSet,
StringPrototypeCharCodeAt,
StringPrototypeIncludes,
StringPrototypeSlice,
} = primordials;
const { getOptionValue } = require('internal/options');
const {
extensionFormatMap,
getFormatOfExtensionlessFile,
mimeToFormat,
} = require('internal/modules/esm/formats');
const detectModule = getOptionValue('--experimental-detect-module');
const { containsModuleSyntax } = internalBinding('contextify');
const { getPackageScopeConfig, getPackageType } = require('internal/modules/package_json_reader');
const { fileURLToPath } = require('internal/url');
const { ERR_UNKNOWN_FILE_EXTENSION } = require('internal/errors').codes;
const protocolHandlers = {
'__proto__': null,
'data:': getDataProtocolModuleFormat,
'file:': getFileProtocolModuleFormat,
'node:'() { return 'builtin'; },
};
/**
* Determine whether the given ambiguous source contains CommonJS or ES module syntax.
* @param {string | Buffer | undefined} source
* @param {URL} url
*/
function detectModuleFormat(source, url) {
if (!source) { return detectModule ? null : 'commonjs'; }
if (!detectModule) { return 'commonjs'; }
return containsModuleSyntax(`${source}`, fileURLToPath(url), url) ? 'module' : 'commonjs';
}
/**
* @param {URL} parsed
* @returns {string | null}
*/
function getDataProtocolModuleFormat(parsed) {
const { 1: mime } = RegExpPrototypeExec(
/^([^/]+\/[^;,]+)(?:[^,]*?)(;base64)?,/,
parsed.pathname,
) || [ null, null, null ];
return mimeToFormat(mime);
}
const DOT_CODE = 46;
const SLASH_CODE = 47;
/**
* Returns the file extension from a URL. Should give similar result to
* `require('node:path').extname(require('node:url').fileURLToPath(url))`
* when used with a `file:` URL.
* @param {URL} url
* @returns {string}
*/
function extname(url) {
const { pathname } = url;
for (let i = pathname.length - 1; i > 0; i--) {
switch (StringPrototypeCharCodeAt(pathname, i)) {
case SLASH_CODE:
return '';
case DOT_CODE:
return StringPrototypeCharCodeAt(pathname, i - 1) === SLASH_CODE ? '' : StringPrototypeSlice(pathname, i);
}
}
return '';
}
/**
* Determine whether the given file URL is under a `node_modules` folder.
* This function assumes that the input has already been verified to be a `file:` URL,
* and is a file rather than a folder.
* @param {URL} url
*/
function underNodeModules(url) {
if (url.protocol !== 'file:') { return false; } // We determine module types for other protocols based on MIME header
return StringPrototypeIncludes(url.pathname, '/node_modules/');
}
let typelessPackageJsonFilesWarnedAbout;
function warnTypelessPackageJsonFile(pjsonPath, url) {
typelessPackageJsonFilesWarnedAbout ??= new SafeSet();
if (!underNodeModules(url) && !typelessPackageJsonFilesWarnedAbout.has(pjsonPath)) {
const warning = `Module type of ${url} is not specified and it doesn't parse as CommonJS.\n` +
'Reparsing as ES module because module syntax was detected. This incurs a performance overhead.\n' +
`To eliminate this warning, add "type": "module" to ${pjsonPath}.`;
process.emitWarning(warning, {
code: 'MODULE_TYPELESS_PACKAGE_JSON',
});
typelessPackageJsonFilesWarnedAbout.add(pjsonPath);
}
}
/**
* @param {URL} url
* @param {{parentURL: string; source?: Buffer}} context
* @param {boolean} ignoreErrors
* @returns {string}
*/
function getFileProtocolModuleFormat(url, context = { __proto__: null }, ignoreErrors) {
const { source } = context;
const ext = extname(url);
if (ext === '.js') {
const { type: packageType, pjsonPath, exists: foundPackageJson } = getPackageScopeConfig(url);
if (packageType !== 'none') {
return packageType;
}
// The controlling `package.json` file has no `type` field.
// `source` is undefined when this is called from `defaultResolve`;
// but this gets called again from `defaultLoad`/`defaultLoadSync`.
// For ambiguous files (.js, no type field) we return undefined from `resolve` and re-run the check in `load`.
const format = detectModuleFormat(source, url);
if (format === 'module' && foundPackageJson) {
// This module has a .js extension, a package.json with no `type` field, and ESM syntax.
// Warn about the missing `type` field so that the user can avoid the performance penalty of detection.
warnTypelessPackageJsonFile(pjsonPath, url);
}
return format;
}
if (ext === '.ts' && getOptionValue('--experimental-strip-types')) {
const { type: packageType, pjsonPath, exists: foundPackageJson } = getPackageScopeConfig(url);
if (packageType !== 'none') {
return `${packageType}-typescript`;
}
// The controlling `package.json` file has no `type` field.
// `source` is undefined when this is called from `defaultResolve`;
// but this gets called again from `defaultLoad`/`defaultLoadSync`.
// Since experimental-strip-types depends on detect-module, we always return null if source is undefined.
if (!source) { return null; }
const { stringify } = require('internal/modules/helpers');
const { stripTypeScriptModuleTypes } = require('internal/modules/typescript');
const stringifiedSource = stringify(source);
const parsedSource = stripTypeScriptModuleTypes(stringifiedSource, fileURLToPath(url));
const detectedFormat = detectModuleFormat(parsedSource, url);
const format = `${detectedFormat}-typescript`;
if (format === 'module-typescript' && foundPackageJson) {
// This module has a .js extension, a package.json with no `type` field, and ESM syntax.
// Warn about the missing `type` field so that the user can avoid the performance penalty of detection.
warnTypelessPackageJsonFile(pjsonPath, url);
}
return format;
}
if (ext === '') {
const packageType = getPackageType(url);
if (packageType === 'module') {
return getFormatOfExtensionlessFile(url);
}
if (packageType !== 'none') {
return packageType; // 'commonjs' or future package types
}
// The controlling `package.json` file has no `type` field.
if (!source) {
return null;
}
const format = getFormatOfExtensionlessFile(url);
if (format === 'wasm') {
return format;
}
return detectModuleFormat(source, url);
}
const format = extensionFormatMap[ext];
if (format) { return format; }
// Explicit undefined return indicates load hook should rerun format check
if (ignoreErrors) { return undefined; }
const filepath = fileURLToPath(url);
throw new ERR_UNKNOWN_FILE_EXTENSION(ext, filepath);
}
/**
* @param {URL} url
* @param {{parentURL: string}} context
* @returns {Promise<string> | string | undefined} only works when enabled
*/
function defaultGetFormatWithoutErrors(url, context) {
const protocol = url.protocol;
if (!ObjectPrototypeHasOwnProperty(protocolHandlers, protocol)) {
return null;
}
return protocolHandlers[protocol](url, context, true);
}
/**
* @param {URL} url
* @param {{parentURL: string}} context
* @returns {Promise<string> | string | undefined} only works when enabled
*/
function defaultGetFormat(url, context) {
const protocol = url.protocol;
if (!ObjectPrototypeHasOwnProperty(protocolHandlers, protocol)) {
return null;
}
return protocolHandlers[protocol](url, context, false);
}
module.exports = {
defaultGetFormat,
defaultGetFormatWithoutErrors,
extensionFormatMap,
extname,
};

View File

@ -0,0 +1,762 @@
'use strict';
const {
ArrayPrototypePush,
ArrayPrototypePushApply,
AtomicsLoad,
AtomicsWait,
AtomicsWaitAsync,
Int32Array,
ObjectAssign,
ObjectDefineProperty,
ObjectSetPrototypeOf,
Promise,
ReflectSet,
SafeSet,
StringPrototypeSlice,
StringPrototypeToUpperCase,
globalThis,
} = primordials;
const {
SharedArrayBuffer,
} = globalThis;
const {
ERR_INTERNAL_ASSERTION,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_RETURN_PROPERTY_VALUE,
ERR_INVALID_RETURN_VALUE,
ERR_LOADER_CHAIN_INCOMPLETE,
ERR_METHOD_NOT_IMPLEMENTED,
ERR_WORKER_UNSERIALIZABLE_ERROR,
} = require('internal/errors').codes;
const { exitCodes: { kUnsettledTopLevelAwait } } = internalBinding('errors');
const { URLParse } = require('internal/url');
const { canParse: URLCanParse } = internalBinding('url');
const { receiveMessageOnPort } = require('worker_threads');
const {
isAnyArrayBuffer,
isArrayBufferView,
} = require('internal/util/types');
const {
validateObject,
validateString,
} = require('internal/validators');
const {
kEmptyObject,
} = require('internal/util');
const {
defaultResolve,
throwIfInvalidParentURL,
} = require('internal/modules/esm/resolve');
const {
getDefaultConditions,
loaderWorkerId,
} = require('internal/modules/esm/utils');
const { deserializeError } = require('internal/error_serdes');
const {
SHARED_MEMORY_BYTE_LENGTH,
WORKER_TO_MAIN_THREAD_NOTIFICATION,
} = require('internal/modules/esm/shared_constants');
let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
debug = fn;
});
let importMetaInitializer;
let importAssertionAlreadyWarned = false;
function emitImportAssertionWarning() {
if (!importAssertionAlreadyWarned) {
importAssertionAlreadyWarned = true;
process.emitWarning('Use `importAttributes` instead of `importAssertions`', 'ExperimentalWarning');
}
}
function defineImportAssertionAlias(context) {
return ObjectDefineProperty(context, 'importAssertions', {
__proto__: null,
configurable: true,
get() {
emitImportAssertionWarning();
return this.importAttributes;
},
set(value) {
emitImportAssertionWarning();
return ReflectSet(this, 'importAttributes', value);
},
});
}
/**
* @typedef {object} ExportedHooks
* @property {Function} resolve Resolve hook.
* @property {Function} load Load hook.
*/
/**
* @typedef {object} KeyedHook
* @property {Function} fn The hook function.
* @property {URL['href']} url The URL of the module.
* @property {KeyedHook?} next The next hook in the chain.
*/
// [2] `validate...()`s throw the wrong error
class Hooks {
#chains = {
/**
* Phase 1 of 2 in ESM loading.
* The output of the `resolve` chain of hooks is passed into the `load` chain of hooks.
* @private
* @property {KeyedHook[]} resolve Last-in-first-out collection of resolve hooks.
*/
resolve: [
{
fn: defaultResolve,
url: 'node:internal/modules/esm/resolve',
},
],
/**
* Phase 2 of 2 in ESM loading.
* @private
* @property {KeyedHook[]} load Last-in-first-out collection of loader hooks.
*/
load: [
{
fn: require('internal/modules/esm/load').defaultLoad,
url: 'node:internal/modules/esm/load',
},
],
};
// Cache URLs we've already validated to avoid repeated validation
#validatedUrls = new SafeSet();
allowImportMetaResolve = false;
/**
* Import and register custom/user-defined module loader hook(s).
* @param {string} urlOrSpecifier
* @param {string} parentURL
* @param {any} [data] Arbitrary data to be passed from the custom
* loader (user-land) to the worker.
*/
async register(urlOrSpecifier, parentURL, data, isInternal) {
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
const keyedExports = isInternal ?
require(urlOrSpecifier) :
await cascadedLoader.import(
urlOrSpecifier,
parentURL,
kEmptyObject,
);
await this.addCustomLoader(urlOrSpecifier, keyedExports, data);
}
/**
* Collect custom/user-defined module loader hook(s).
* @param {string} url Custom loader specifier
* @param {Record<string, unknown>} exports
* @param {any} [data] Arbitrary data to be passed from the custom loader (user-land)
* to the worker.
* @returns {any | Promise<any>} User data, ignored unless it's a promise, in which case it will be awaited.
*/
addCustomLoader(url, exports, data) {
const {
initialize,
resolve,
load,
} = pluckHooks(exports);
if (resolve) {
const next = this.#chains.resolve[this.#chains.resolve.length - 1];
ArrayPrototypePush(this.#chains.resolve, { __proto__: null, fn: resolve, url, next });
}
if (load) {
const next = this.#chains.load[this.#chains.load.length - 1];
ArrayPrototypePush(this.#chains.load, { __proto__: null, fn: load, url, next });
}
return initialize?.(data);
}
/**
* Resolve the location of the module.
*
* Internally, this behaves like a backwards iterator, wherein the stack of
* hooks starts at the top and each call to `nextResolve()` moves down 1 step
* until it reaches the bottom or short-circuits.
* @param {string} originalSpecifier The specified URL path of the module to
* be resolved.
* @param {string} [parentURL] The URL path of the module's parent.
* @param {ImportAttributes} [importAttributes] Attributes from the import
* statement or expression.
* @returns {Promise<{ format: string, url: URL['href'] }>}
*/
async resolve(
originalSpecifier,
parentURL,
importAttributes = { __proto__: null },
) {
throwIfInvalidParentURL(parentURL);
const chain = this.#chains.resolve;
const context = {
conditions: getDefaultConditions(),
importAttributes,
parentURL,
};
const meta = {
chainFinished: null,
context,
hookErrIdentifier: '',
hookName: 'resolve',
shortCircuited: false,
};
const validateArgs = (hookErrIdentifier, suppliedSpecifier, ctx) => {
validateString(
suppliedSpecifier,
`${hookErrIdentifier} specifier`,
); // non-strings can be coerced to a URL string
if (ctx) { validateObject(ctx, `${hookErrIdentifier} context`); }
};
const validateOutput = (hookErrIdentifier, output) => {
if (typeof output !== 'object' || output === null) { // [2]
throw new ERR_INVALID_RETURN_VALUE(
'an object',
hookErrIdentifier,
output,
);
}
};
const nextResolve = nextHookFactory(chain[chain.length - 1], meta, { validateArgs, validateOutput });
const resolution = await nextResolve(originalSpecifier, defineImportAssertionAlias(context));
const { hookErrIdentifier } = meta; // Retrieve the value after all settled
validateOutput(hookErrIdentifier, resolution);
if (resolution?.shortCircuit === true) { meta.shortCircuited = true; }
if (!meta.chainFinished && !meta.shortCircuited) {
throw new ERR_LOADER_CHAIN_INCOMPLETE(hookErrIdentifier);
}
let resolvedImportAttributes;
const {
format,
url,
} = resolution;
if (typeof url !== 'string') {
// non-strings can be coerced to a URL string
// validateString() throws a less-specific error
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
'a URL string',
hookErrIdentifier,
'url',
url,
);
}
// Avoid expensive URL instantiation for known-good URLs
if (!this.#validatedUrls.has(url)) {
// No need to convert to string, since the type is already validated
if (!URLCanParse(url)) {
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
'a URL string',
hookErrIdentifier,
'url',
url,
);
}
this.#validatedUrls.add(url);
}
if (!('importAttributes' in resolution) && ('importAssertions' in resolution)) {
emitImportAssertionWarning();
resolvedImportAttributes = resolution.importAssertions;
} else {
resolvedImportAttributes = resolution.importAttributes;
}
if (
resolvedImportAttributes != null &&
typeof resolvedImportAttributes !== 'object'
) {
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
'an object',
hookErrIdentifier,
'importAttributes',
resolvedImportAttributes,
);
}
if (
format != null &&
typeof format !== 'string' // [2]
) {
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
'a string',
hookErrIdentifier,
'format',
format,
);
}
return {
__proto__: null,
format,
importAttributes: resolvedImportAttributes,
url,
};
}
resolveSync(_originalSpecifier, _parentURL, _importAttributes) {
throw new ERR_METHOD_NOT_IMPLEMENTED('resolveSync()');
}
/**
* Provide source that is understood by one of Node's translators.
*
* Internally, this behaves like a backwards iterator, wherein the stack of
* hooks starts at the top and each call to `nextLoad()` moves down 1 step
* until it reaches the bottom or short-circuits.
* @param {URL['href']} url The URL/path of the module to be loaded
* @param {object} context Metadata about the module
* @returns {Promise<{ format: ModuleFormat, source: ModuleSource }>}
*/
async load(url, context = {}) {
const chain = this.#chains.load;
const meta = {
chainFinished: null,
context,
hookErrIdentifier: '',
hookName: 'load',
shortCircuited: false,
};
const validateArgs = (hookErrIdentifier, nextUrl, ctx) => {
if (typeof nextUrl !== 'string') {
// Non-strings can be coerced to a URL string
// validateString() throws a less-specific error
throw new ERR_INVALID_ARG_TYPE(
`${hookErrIdentifier} url`,
'a URL string',
nextUrl,
);
}
// Avoid expensive URL instantiation for known-good URLs
if (!this.#validatedUrls.has(nextUrl)) {
// No need to convert to string, since the type is already validated
if (!URLCanParse(nextUrl)) {
throw new ERR_INVALID_ARG_VALUE(
`${hookErrIdentifier} url`,
nextUrl,
'should be a URL string',
);
}
this.#validatedUrls.add(nextUrl);
}
if (ctx) { validateObject(ctx, `${hookErrIdentifier} context`); }
};
const validateOutput = (hookErrIdentifier, output) => {
if (typeof output !== 'object' || output === null) { // [2]
throw new ERR_INVALID_RETURN_VALUE(
'an object',
hookErrIdentifier,
output,
);
}
};
const nextLoad = nextHookFactory(chain[chain.length - 1], meta, { validateArgs, validateOutput });
const loaded = await nextLoad(url, defineImportAssertionAlias(context));
const { hookErrIdentifier } = meta; // Retrieve the value after all settled
validateOutput(hookErrIdentifier, loaded);
if (loaded?.shortCircuit === true) { meta.shortCircuited = true; }
if (!meta.chainFinished && !meta.shortCircuited) {
throw new ERR_LOADER_CHAIN_INCOMPLETE(hookErrIdentifier);
}
const {
format,
source,
} = loaded;
let responseURL = loaded.responseURL;
if (responseURL === undefined) {
responseURL = url;
}
let responseURLObj;
if (typeof responseURL === 'string') {
responseURLObj = URLParse(responseURL);
}
if (responseURLObj?.href !== responseURL) {
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
'undefined or a fully resolved URL string',
hookErrIdentifier,
'responseURL',
responseURL,
);
}
if (format == null) {
require('internal/modules/esm/load').throwUnknownModuleFormat(url, format);
}
if (typeof format !== 'string') { // [2]
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
'a string',
hookErrIdentifier,
'format',
format,
);
}
if (
source != null &&
typeof source !== 'string' &&
!isAnyArrayBuffer(source) &&
!isArrayBufferView(source)
) {
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
'a string, an ArrayBuffer, or a TypedArray',
hookErrIdentifier,
'source',
source,
);
}
return {
__proto__: null,
format,
responseURL,
source,
};
}
forceLoadHooks() {
// No-op
}
importMetaInitialize(meta, context, loader) {
importMetaInitializer ??= require('internal/modules/esm/initialize_import_meta').initializeImportMeta;
meta = importMetaInitializer(meta, context, loader);
return meta;
}
}
ObjectSetPrototypeOf(Hooks.prototype, null);
/**
* There may be multiple instances of Hooks/HooksProxy, but there is only 1 Internal worker, so
* there is only 1 MessageChannel.
*/
let MessageChannel;
class HooksProxy {
/**
* Shared memory. Always use Atomics method to read or write to it.
* @type {Int32Array}
*/
#lock;
/**
* The InternalWorker instance, which lets us communicate with the loader thread.
*/
#worker;
/**
* The last notification ID received from the worker. This is used to detect
* if the worker has already sent a notification before putting the main
* thread to sleep, to avoid a race condition.
* @type {number}
*/
#workerNotificationLastId = 0;
/**
* Track how many async responses the main thread should expect.
* @type {number}
*/
#numberOfPendingAsyncResponses = 0;
#isReady = false;
constructor() {
const { InternalWorker } = require('internal/worker');
MessageChannel ??= require('internal/worker/io').MessageChannel;
const lock = new SharedArrayBuffer(SHARED_MEMORY_BYTE_LENGTH);
this.#lock = new Int32Array(lock);
this.#worker = new InternalWorker(loaderWorkerId, {
stderr: false,
stdin: false,
stdout: false,
trackUnmanagedFds: false,
workerData: {
lock,
},
});
this.#worker.unref(); // ! Allows the process to eventually exit.
this.#worker.on('exit', process.exit);
}
waitForWorker() {
if (!this.#isReady) {
const { kIsOnline } = require('internal/worker');
if (!this.#worker[kIsOnline]) {
debug('wait for signal from worker');
AtomicsWait(this.#lock, WORKER_TO_MAIN_THREAD_NOTIFICATION, 0);
const response = this.#worker.receiveMessageSync();
if (response == null || response.message.status === 'exit') { return; }
// ! This line catches initialization errors in the worker thread.
this.#unwrapMessage(response);
}
this.#isReady = true;
}
}
/**
* Invoke a remote method asynchronously.
* @param {string} method Method to invoke
* @param {any[]} [transferList] Objects in `args` to be transferred
* @param {any[]} args Arguments to pass to `method`
* @returns {Promise<any>}
*/
async makeAsyncRequest(method, transferList, ...args) {
this.waitForWorker();
MessageChannel ??= require('internal/worker/io').MessageChannel;
const asyncCommChannel = new MessageChannel();
// Pass work to the worker.
debug('post async message to worker', { method, args, transferList });
const finalTransferList = [asyncCommChannel.port2];
if (transferList) {
ArrayPrototypePushApply(finalTransferList, transferList);
}
this.#worker.postMessage({
__proto__: null,
method, args,
port: asyncCommChannel.port2,
}, finalTransferList);
if (this.#numberOfPendingAsyncResponses++ === 0) {
// On the next lines, the main thread will await a response from the worker thread that might
// come AFTER the last task in the event loop has run its course and there would be nothing
// left keeping the thread alive (and once the main thread dies, the whole process stops).
// However we want to keep the process alive until the worker thread responds (or until the
// event loop of the worker thread is also empty), so we ref the worker until we get all the
// responses back.
this.#worker.ref();
}
let response;
do {
debug('wait for async response from worker', { method, args });
await AtomicsWaitAsync(this.#lock, WORKER_TO_MAIN_THREAD_NOTIFICATION, this.#workerNotificationLastId).value;
this.#workerNotificationLastId = AtomicsLoad(this.#lock, WORKER_TO_MAIN_THREAD_NOTIFICATION);
response = receiveMessageOnPort(asyncCommChannel.port1);
} while (response == null);
debug('got async response from worker', { method, args }, this.#lock);
if (--this.#numberOfPendingAsyncResponses === 0) {
// We got all the responses from the worker, its job is done (until next time).
this.#worker.unref();
}
const body = this.#unwrapMessage(response);
asyncCommChannel.port1.close();
return body;
}
/**
* Invoke a remote method synchronously.
* @param {string} method Method to invoke
* @param {any[]} [transferList] Objects in `args` to be transferred
* @param {any[]} args Arguments to pass to `method`
* @returns {any}
*/
makeSyncRequest(method, transferList, ...args) {
this.waitForWorker();
// Pass work to the worker.
debug('post sync message to worker', { method, args, transferList });
this.#worker.postMessage({ __proto__: null, method, args }, transferList);
let response;
do {
debug('wait for sync response from worker', { method, args });
// Sleep until worker responds.
AtomicsWait(this.#lock, WORKER_TO_MAIN_THREAD_NOTIFICATION, this.#workerNotificationLastId);
this.#workerNotificationLastId = AtomicsLoad(this.#lock, WORKER_TO_MAIN_THREAD_NOTIFICATION);
response = this.#worker.receiveMessageSync();
} while (response == null);
debug('got sync response from worker', { method, args });
if (response.message.status === 'never-settle') {
process.exit(kUnsettledTopLevelAwait);
} else if (response.message.status === 'exit') {
process.exit(response.message.body);
}
return this.#unwrapMessage(response);
}
#unwrapMessage(response) {
if (response.message.status === 'never-settle') {
return new Promise(() => {});
}
const { status, body } = response.message;
if (status === 'error') {
if (body == null || typeof body !== 'object') { throw body; }
if (body.serializationFailed || body.serialized == null) {
throw new ERR_WORKER_UNSERIALIZABLE_ERROR();
}
// eslint-disable-next-line no-restricted-syntax
throw deserializeError(body.serialized);
} else {
return body;
}
}
#importMetaInitializer = require('internal/modules/esm/initialize_import_meta').initializeImportMeta;
importMetaInitialize(meta, context, loader) {
this.#importMetaInitializer(meta, context, loader);
}
}
ObjectSetPrototypeOf(HooksProxy.prototype, null);
// TODO(JakobJingleheimer): Remove this when loaders go "stable".
let globalPreloadWarningWasEmitted = false;
/**
* A utility function to pluck the hooks from a user-defined loader.
* @param {import('./loader.js).ModuleExports} exports
* @returns {ExportedHooks}
*/
function pluckHooks({
globalPreload,
initialize,
resolve,
load,
}) {
const acceptedHooks = { __proto__: null };
if (resolve) {
acceptedHooks.resolve = resolve;
}
if (load) {
acceptedHooks.load = load;
}
if (initialize) {
acceptedHooks.initialize = initialize;
} else if (globalPreload && !globalPreloadWarningWasEmitted) {
process.emitWarning(
'`globalPreload` has been removed; use `initialize` instead.',
'UnsupportedWarning',
);
globalPreloadWarningWasEmitted = true;
}
return acceptedHooks;
}
/**
* A utility function to iterate through a hook chain, track advancement in the
* chain, and generate and supply the `next<HookName>` argument to the custom
* hook.
* @param {KeyedHook} current The (currently) first hook in the chain (this shifts
* on every call).
* @param {object} meta Properties that change as the current hook advances
* along the chain.
* @param {boolean} meta.chainFinished Whether the end of the chain has been
* reached AND invoked.
* @param {string} meta.hookErrIdentifier A user-facing identifier to help
* pinpoint where an error occurred. Ex "file:///foo.mjs 'resolve'".
* @param {string} meta.hookName The kind of hook the chain is (ex 'resolve')
* @param {boolean} meta.shortCircuited Whether a hook signaled a short-circuit.
* @param {(hookErrIdentifier, hookArgs) => void} validate A wrapper function
* containing all validation of a custom loader hook's intermediary output. Any
* validation within MUST throw.
* @returns {function next<HookName>(...hookArgs)} The next hook in the chain.
*/
function nextHookFactory(current, meta, { validateArgs, validateOutput }) {
// First, prepare the current
const { hookName } = meta;
const {
fn: hook,
url: hookFilePath,
next,
} = current;
// ex 'nextResolve'
const nextHookName = `next${
StringPrototypeToUpperCase(hookName[0]) +
StringPrototypeSlice(hookName, 1)
}`;
let nextNextHook;
if (next) {
nextNextHook = nextHookFactory(next, meta, { validateArgs, validateOutput });
} else {
// eslint-disable-next-line func-name-matching
nextNextHook = function chainAdvancedTooFar() {
throw new ERR_INTERNAL_ASSERTION(
`ESM custom loader '${hookName}' advanced beyond the end of the chain.`,
);
};
}
return ObjectDefineProperty(
async (arg0 = undefined, context) => {
// Update only when hook is invoked to avoid fingering the wrong filePath
meta.hookErrIdentifier = `${hookFilePath} '${hookName}'`;
validateArgs(`${meta.hookErrIdentifier} hook's ${nextHookName}()`, arg0, context);
const outputErrIdentifier = `${hookFilePath} '${hookName}' hook's ${nextHookName}()`;
// Set when next<HookName> is actually called, not just generated.
if (!next) { meta.chainFinished = true; }
if (context) { // `context` has already been validated, so no fancy check needed.
ObjectAssign(meta.context, context);
}
const output = await hook(arg0, meta.context, nextNextHook);
validateOutput(outputErrIdentifier, output);
if (output?.shortCircuit === true) { meta.shortCircuited = true; }
return output;
},
'name',
{ __proto__: null, value: nextHookName },
);
}
exports.Hooks = Hooks;
exports.HooksProxy = HooksProxy;

View File

@ -0,0 +1,81 @@
'use strict';
const {
StringPrototypeStartsWith,
} = primordials;
const { getOptionValue } = require('internal/options');
const {
setLazyPathHelpers,
} = internalBinding('modules');
const experimentalImportMetaResolve = getOptionValue('--experimental-import-meta-resolve');
/**
* Generate a function to be used as import.meta.resolve for a particular module.
* @param {string} defaultParentURL The default base to use for resolution
* @param {typeof import('./loader.js').ModuleLoader} loader Reference to the current module loader
* @param {bool} allowParentURL Whether to permit parentURL second argument for contextual resolution
* @returns {(specifier: string) => string} Function to assign to import.meta.resolve
*/
function createImportMetaResolve(defaultParentURL, loader, allowParentURL) {
/**
* @param {string} specifier
* @param {URL['href']} [parentURL] When `--experimental-import-meta-resolve` is specified, a
* second argument can be provided.
*/
return function resolve(specifier, parentURL = defaultParentURL) {
let url;
if (!allowParentURL) {
parentURL = defaultParentURL;
}
try {
({ url } = loader.resolveSync(specifier, parentURL));
return url;
} catch (error) {
switch (error?.code) {
case 'ERR_UNSUPPORTED_DIR_IMPORT':
case 'ERR_MODULE_NOT_FOUND':
({ url } = error);
if (url) {
return url;
}
}
throw error;
}
};
}
/**
* Create the `import.meta` object for a module.
* @param {object} meta
* @param {{url: string, isMain?: boolean}} context
* @param {typeof import('./loader.js').ModuleLoader} loader Reference to the current module loader
* @returns {{dirname?: string, filename?: string, url: string, resolve?: Function}}
*/
function initializeImportMeta(meta, context, loader) {
const { url, isMain } = context;
// Alphabetical
if (StringPrototypeStartsWith(url, 'file:') === true) {
// dirname
// filename
setLazyPathHelpers(meta, url);
}
meta.main = !!isMain;
if (!loader || loader.allowImportMetaResolve) {
meta.resolve = createImportMetaResolve(url, loader, experimentalImportMetaResolve);
}
meta.url = url;
return meta;
}
module.exports = {
initializeImportMeta,
};

View File

@ -0,0 +1,236 @@
'use strict';
const {
RegExpPrototypeExec,
} = primordials;
const {
kEmptyObject,
} = require('internal/util');
const { defaultGetFormat } = require('internal/modules/esm/get_format');
const { validateAttributes, emitImportAssertionWarning } = require('internal/modules/esm/assert');
const { readFileSync } = require('fs');
const { Buffer: { from: BufferFrom } } = require('buffer');
const { URL } = require('internal/url');
const {
ERR_INVALID_URL,
ERR_UNKNOWN_MODULE_FORMAT,
ERR_UNSUPPORTED_ESM_URL_SCHEME,
} = require('internal/errors').codes;
const {
dataURLProcessor,
} = require('internal/data_url');
/**
* @param {URL} url URL to the module
* @param {ESModuleContext} context used to decorate error messages
* @returns {Promise<{ responseURL: string, source: string | BufferView }>}
*/
async function getSource(url, context) {
const { protocol, href } = url;
const responseURL = href;
let source;
if (protocol === 'file:') {
const { readFile: readFileAsync } = require('internal/fs/promises').exports;
source = await readFileAsync(url);
} else if (protocol === 'data:') {
const result = dataURLProcessor(url);
if (result === 'failure') {
throw new ERR_INVALID_URL(responseURL, null);
}
source = BufferFrom(result.body);
} else {
const supportedSchemes = ['file', 'data'];
throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(url, supportedSchemes);
}
return { __proto__: null, responseURL, source };
}
/**
* @param {URL} url URL to the module
* @param {ESModuleContext} context used to decorate error messages
* @returns {{ responseURL: string, source: string | BufferView }}
*/
function getSourceSync(url, context) {
const { protocol, href } = url;
const responseURL = href;
let source;
if (protocol === 'file:') {
source = readFileSync(url);
} else if (protocol === 'data:') {
const result = dataURLProcessor(url);
if (result === 'failure') {
throw new ERR_INVALID_URL(responseURL);
}
source = BufferFrom(result.body);
} else {
const supportedSchemes = ['file', 'data'];
throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(url, supportedSchemes);
}
return { __proto__: null, responseURL, source };
}
/**
* Node.js default load hook.
* @param {string} url
* @param {LoadContext} context
* @returns {LoadReturn}
*/
async function defaultLoad(url, context = kEmptyObject) {
let responseURL = url;
let {
importAttributes,
format,
source,
} = context;
if (importAttributes == null && !('importAttributes' in context) && 'importAssertions' in context) {
emitImportAssertionWarning();
importAttributes = context.importAssertions;
// Alias `importAssertions` to `importAttributes`
context = {
...context,
importAttributes,
};
}
const urlInstance = new URL(url);
throwIfUnsupportedURLScheme(urlInstance);
if (urlInstance.protocol === 'node:') {
source = null;
format ??= 'builtin';
} else if (format === 'addon') {
// Skip loading addon file content. It must be loaded with dlopen from file system.
source = null;
} else if (format !== 'commonjs') {
if (source == null) {
({ responseURL, source } = await getSource(urlInstance, context));
context = { __proto__: context, source };
}
if (format == null) {
// Now that we have the source for the module, run `defaultGetFormat` to detect its format.
format = await defaultGetFormat(urlInstance, context);
if (format === 'commonjs') {
// For backward compatibility reasons, we need to discard the source in
// order for the CJS loader to re-fetch it.
source = null;
}
}
}
validateAttributes(url, format, importAttributes);
return {
__proto__: null,
format,
responseURL,
source,
};
}
/**
* @typedef LoadContext
* @property {string} [format] A hint (possibly returned from `resolve`)
* @property {string | Buffer | ArrayBuffer} [source] source
* @property {Record<string, string>} [importAttributes] import attributes
*/
/**
* @typedef LoadReturn
* @property {string} format format
* @property {URL['href']} responseURL The module's fully resolved URL
* @property {Buffer} source source
*/
/**
* @param {URL['href']} url
* @param {LoadContext} [context]
* @returns {LoadReturn}
*/
function defaultLoadSync(url, context = kEmptyObject) {
let responseURL = url;
const { importAttributes } = context;
let {
format,
source,
} = context;
const urlInstance = new URL(url);
throwIfUnsupportedURLScheme(urlInstance, false);
if (urlInstance.protocol === 'node:') {
source = null;
} else if (source == null) {
({ responseURL, source } = getSourceSync(urlInstance, context));
context.source = source;
}
format ??= defaultGetFormat(urlInstance, context);
validateAttributes(url, format, importAttributes);
return {
__proto__: null,
format,
responseURL,
source,
};
}
/**
* throws an error if the protocol is not one of the protocols
* that can be loaded in the default loader
* @param {URL} parsed
*/
function throwIfUnsupportedURLScheme(parsed) {
// Avoid accessing the `protocol` property due to the lazy getters.
const protocol = parsed?.protocol;
if (
protocol &&
protocol !== 'file:' &&
protocol !== 'data:' &&
protocol !== 'node:' &&
(
protocol !== 'https:' &&
protocol !== 'http:'
)
) {
const schemes = ['file', 'data', 'node'];
throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(parsed, schemes);
}
}
/**
* For a falsy `format` returned from `load`, throw an error.
* This could happen from either a custom user loader _or_ from the default loader, because the default loader tries to
* determine formats for data URLs.
* @param {string} url The resolved URL of the module
* @param {null | undefined | false | 0 | -0 | 0n | ''} format Falsy format returned from `load`
*/
function throwUnknownModuleFormat(url, format) {
const dataUrl = RegExpPrototypeExec(
/^data:([^/]+\/[^;,]+)(?:[^,]*?)(;base64)?,/,
url,
);
throw new ERR_UNKNOWN_MODULE_FORMAT(
dataUrl ? dataUrl[1] : format,
url);
}
module.exports = {
defaultLoad,
defaultLoadSync,
getSourceSync,
throwUnknownModuleFormat,
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,503 @@
'use strict';
const {
Array,
ArrayPrototypeJoin,
ArrayPrototypeSome,
FunctionPrototype,
ObjectSetPrototypeOf,
PromisePrototypeThen,
PromiseResolve,
RegExpPrototypeExec,
RegExpPrototypeSymbolReplace,
SafePromiseAllReturnArrayLike,
SafePromiseAllReturnVoid,
SafeSet,
StringPrototypeIncludes,
StringPrototypeSplit,
StringPrototypeStartsWith,
globalThis,
} = primordials;
let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
debug = fn;
});
const {
ModuleWrap,
kErrored,
kEvaluated,
kEvaluationPhase,
kInstantiated,
kUninstantiated,
} = internalBinding('module_wrap');
const {
privateSymbols: {
entry_point_module_private_symbol,
},
} = internalBinding('util');
const { decorateErrorStack, kEmptyObject } = require('internal/util');
const {
getSourceMapsSupport,
} = require('internal/source_map/source_map_cache');
const assert = require('internal/assert');
const resolvedPromise = PromiseResolve();
const {
setHasStartedUserESMExecution,
urlToFilename,
} = require('internal/modules/helpers');
const { getOptionValue } = require('internal/options');
const noop = FunctionPrototype;
const {
ERR_REQUIRE_ASYNC_MODULE,
} = require('internal/errors').codes;
let hasPausedEntry = false;
const CJSGlobalLike = [
'require',
'module',
'exports',
'__filename',
'__dirname',
];
const isCommonJSGlobalLikeNotDefinedError = (errorMessage) =>
ArrayPrototypeSome(
CJSGlobalLike,
(globalLike) => errorMessage === `${globalLike} is not defined`,
);
/**
*
* @param {Error} e
* @param {string} url
* @returns {void}
*/
const explainCommonJSGlobalLikeNotDefinedError = (e, url) => {
if (e?.name === 'ReferenceError' &&
isCommonJSGlobalLikeNotDefinedError(e.message)) {
e.message += ' in ES module scope';
if (StringPrototypeStartsWith(e.message, 'require ')) {
e.message += ', you can use import instead';
}
const packageConfig =
StringPrototypeStartsWith(url, 'file://') &&
RegExpPrototypeExec(/\.js(\?[^#]*)?(#.*)?$/, url) !== null &&
require('internal/modules/package_json_reader')
.getPackageScopeConfig(url);
if (packageConfig.type === 'module') {
e.message +=
'\nThis file is being treated as an ES module because it has a ' +
`'.js' file extension and '${packageConfig.pjsonPath}' contains ` +
'"type": "module". To treat it as a CommonJS script, rename it ' +
'to use the \'.cjs\' file extension.';
}
}
};
class ModuleJobBase {
constructor(url, importAttributes, phase, isMain, inspectBrk) {
assert(typeof phase === 'number');
this.importAttributes = importAttributes;
this.phase = phase;
this.isMain = isMain;
this.inspectBrk = inspectBrk;
this.url = url;
}
}
/* A ModuleJob tracks the loading of a single Module, and the ModuleJobs of
* its dependencies, over time. */
class ModuleJob extends ModuleJobBase {
#loader = null;
/**
* @param {ModuleLoader} loader The ESM loader.
* @param {string} url URL of the module to be wrapped in ModuleJob.
* @param {ImportAttributes} importAttributes Import attributes from the import statement.
* @param {ModuleWrap|Promise<ModuleWrap>} moduleOrModulePromise Translated ModuleWrap for the module.
* @param {number} phase The phase to load the module to.
* @param {boolean} isMain Whether the module is the entry point.
* @param {boolean} inspectBrk Whether this module should be evaluated with the
* first line paused in the debugger (because --inspect-brk is passed).
* @param {boolean} isForRequireInImportedCJS Whether this is created for require() in imported CJS.
*/
constructor(loader, url, importAttributes = { __proto__: null }, moduleOrModulePromise,
phase = kEvaluationPhase, isMain, inspectBrk, isForRequireInImportedCJS = false) {
super(url, importAttributes, phase, isMain, inspectBrk);
this.#loader = loader;
// Expose the promise to the ModuleWrap directly for linking below.
if (isForRequireInImportedCJS) {
this.module = moduleOrModulePromise;
assert(this.module instanceof ModuleWrap);
this.modulePromise = PromiseResolve(this.module);
} else {
this.modulePromise = moduleOrModulePromise;
}
if (this.phase === kEvaluationPhase) {
// Promise for the list of all dependencyJobs.
this.linked = this.#link();
// This promise is awaited later anyway, so silence
// 'unhandled rejection' warnings.
PromisePrototypeThen(this.linked, undefined, noop);
}
// instantiated == deep dependency jobs wrappers are instantiated,
// and module wrapper is instantiated.
this.instantiated = undefined;
}
/**
* Ensure that this ModuleJob is moving towards the required phase
* (does not necessarily mean it is ready at that phase - run does that)
* @param {number} phase
*/
ensurePhase(phase) {
if (this.phase < phase) {
this.phase = phase;
this.linked = this.#link();
PromisePrototypeThen(this.linked, undefined, noop);
}
}
/**
* Iterates the module requests and links with the loader.
* @returns {Promise<ModuleJob[]>} Dependency module jobs.
*/
async #link() {
this.module = await this.modulePromise;
assert(this.module instanceof ModuleWrap);
const moduleRequests = this.module.getModuleRequests();
// Explicitly keeping track of dependency jobs is needed in order
// to flatten out the dependency graph below in `_instantiate()`,
// so that circular dependencies can't cause a deadlock by two of
// these `link` callbacks depending on each other.
// Create an ArrayLike to avoid calling into userspace with `.then`
// when returned from the async function.
const evaluationDepJobs = Array(moduleRequests.length);
ObjectSetPrototypeOf(evaluationDepJobs, null);
// Specifiers should be aligned with the moduleRequests array in order.
const specifiers = Array(moduleRequests.length);
const modulePromises = Array(moduleRequests.length);
// Track each loop for whether it is an evaluation phase or source phase request.
let isEvaluation;
// Iterate with index to avoid calling into userspace with `Symbol.iterator`.
for (
let idx = 0, eidx = 0;
// Use the let-scoped eidx value to update the executionDepJobs length at the end of the loop.
idx < moduleRequests.length || (evaluationDepJobs.length = eidx, false);
idx++, eidx += isEvaluation
) {
const { specifier, phase, attributes } = moduleRequests[idx];
isEvaluation = phase === kEvaluationPhase;
// TODO(joyeecheung): resolve all requests first, then load them in another
// loop so that hooks can pre-fetch sources off-thread.
const dependencyJobPromise = this.#loader.getModuleJobForImport(
specifier, this.url, attributes, phase,
);
const modulePromise = PromisePrototypeThen(dependencyJobPromise, (job) => {
debug(`async link() ${this.url} -> ${specifier}`, job);
if (phase === kEvaluationPhase) {
evaluationDepJobs[eidx] = job;
}
return job.modulePromise;
});
modulePromises[idx] = modulePromise;
specifiers[idx] = specifier;
}
const modules = await SafePromiseAllReturnArrayLike(modulePromises);
this.module.link(specifiers, modules);
return evaluationDepJobs;
}
#instantiate() {
if (this.instantiated === undefined) {
this.instantiated = this.#_instantiate();
}
return this.instantiated;
}
async #_instantiate() {
const jobsInGraph = new SafeSet();
const addJobsToDependencyGraph = async (moduleJob) => {
debug(`async addJobsToDependencyGraph() ${this.url}`, moduleJob);
if (jobsInGraph.has(moduleJob)) {
return;
}
jobsInGraph.add(moduleJob);
const dependencyJobs = await moduleJob.linked;
return SafePromiseAllReturnVoid(dependencyJobs, addJobsToDependencyGraph);
};
await addJobsToDependencyGraph(this);
try {
if (!hasPausedEntry && this.inspectBrk) {
hasPausedEntry = true;
const initWrapper = internalBinding('inspector').callAndPauseOnStart;
initWrapper(this.module.instantiate, this.module);
} else {
this.module.instantiate();
}
} catch (e) {
decorateErrorStack(e);
// TODO(@bcoe): Add source map support to exception that occurs as result
// of missing named export. This is currently not possible because
// stack trace originates in module_job, not the file itself. A hidden
// symbol with filename could be set in node_errors.cc to facilitate this.
if (!getSourceMapsSupport().enabled &&
StringPrototypeIncludes(e.message,
' does not provide an export named')) {
const splitStack = StringPrototypeSplit(e.stack, '\n', 2);
const parentFileUrl = RegExpPrototypeSymbolReplace(
/:\d+$/,
splitStack[0],
'',
);
const { 1: childSpecifier, 2: name } = RegExpPrototypeExec(
/module '(.*)' does not provide an export named '(.+)'/,
e.message);
const { url: childFileURL } = await this.#loader.resolve(
childSpecifier,
parentFileUrl,
kEmptyObject,
);
let format;
try {
// This might throw for non-CommonJS modules because we aren't passing
// in the import attributes and some formats require them; but we only
// care about CommonJS for the purposes of this error message.
({ format } =
await this.#loader.load(childFileURL));
} catch {
// Continue regardless of error.
}
if (format === 'commonjs') {
const importStatement = splitStack[1];
// TODO(@ctavan): The original error stack only provides the single
// line which causes the error. For multi-line import statements we
// cannot generate an equivalent object destructuring assignment by
// just parsing the error stack.
const oneLineNamedImports = RegExpPrototypeExec(/{.*}/, importStatement);
const destructuringAssignment = oneLineNamedImports &&
RegExpPrototypeSymbolReplace(/\s+as\s+/g, oneLineNamedImports, ': ');
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 ' +
'always be imported via the default export, for example using:' +
`\n\nimport pkg from '${childSpecifier}';\n${
destructuringAssignment ?
`const ${destructuringAssignment} = pkg;\n` : ''}`;
const newStack = StringPrototypeSplit(e.stack, '\n');
newStack[3] = `SyntaxError: ${e.message}`;
e.stack = ArrayPrototypeJoin(newStack, '\n');
}
}
throw e;
}
for (const dependencyJob of jobsInGraph) {
// Calling `this.module.instantiate()` instantiates not only the
// ModuleWrap in this module, but all modules in the graph.
dependencyJob.instantiated = resolvedPromise;
}
}
runSync(parent) {
assert(this.phase === kEvaluationPhase);
assert(this.module instanceof ModuleWrap);
let status = this.module.getStatus();
debug('ModuleJob.runSync', this.module);
// FIXME(joyeecheung): this cannot fully handle < kInstantiated. Make the linking
// fully synchronous instead.
if (status === kUninstantiated) {
this.module.async = this.module.instantiateSync();
status = this.module.getStatus();
}
if (status === kInstantiated || status === kErrored) {
const filename = urlToFilename(this.url);
const parentFilename = urlToFilename(parent?.filename);
this.module.async ??= this.module.isGraphAsync();
if (this.module.async && !getOptionValue('--experimental-print-required-tla')) {
throw new ERR_REQUIRE_ASYNC_MODULE(filename, parentFilename);
}
if (status === kInstantiated) {
setHasStartedUserESMExecution();
const namespace = this.module.evaluateSync(filename, parentFilename);
return { __proto__: null, module: this.module, namespace };
}
throw this.module.getError();
} else if (status === kEvaluated) {
return { __proto__: null, module: this.module, namespace: this.module.getNamespaceSync() };
}
assert.fail(`Unexpected module status ${status}.`);
}
async run(isEntryPoint = false) {
assert(this.phase === kEvaluationPhase);
await this.#instantiate();
if (isEntryPoint) {
globalThis[entry_point_module_private_symbol] = this.module;
}
const timeout = -1;
const breakOnSigint = false;
setHasStartedUserESMExecution();
try {
await this.module.evaluate(timeout, breakOnSigint);
} catch (e) {
explainCommonJSGlobalLikeNotDefinedError(e, this.module.url);
throw e;
}
return { __proto__: null, module: this.module };
}
}
/**
* This is a fully synchronous job and does not spawn additional threads in any way.
* All the steps are ensured to be synchronous and it throws on instantiating
* an asynchronous graph. It also disallows CJS <-> ESM cycles.
*
* This is used for ES modules loaded via require(esm). Modules loaded by require() in
* imported CJS are handled by ModuleJob with the isForRequireInImportedCJS set to true instead.
* The two currently have different caching behaviors.
* TODO(joyeecheung): consolidate this with the isForRequireInImportedCJS variant of ModuleJob.
*/
class ModuleJobSync extends ModuleJobBase {
#loader = null;
/**
* @param {ModuleLoader} loader The ESM loader.
* @param {string} url URL of the module to be wrapped in ModuleJob.
* @param {ImportAttributes} importAttributes Import attributes from the import statement.
* @param {ModuleWrap} moduleWrap Translated ModuleWrap for the module.
* @param {number} phase The phase to load the module to.
* @param {boolean} isMain Whether the module is the entry point.
* @param {boolean} inspectBrk Whether this module should be evaluated with the
* first line paused in the debugger (because --inspect-brk is passed).
*/
constructor(loader, url, importAttributes, moduleWrap, phase = kEvaluationPhase, isMain,
inspectBrk) {
super(url, importAttributes, phase, isMain, inspectBrk, true);
this.#loader = loader;
this.module = moduleWrap;
assert(this.module instanceof ModuleWrap);
this.linked = undefined;
this.type = importAttributes.type;
if (phase === kEvaluationPhase) {
this.#link();
}
}
/**
* Ensure that this ModuleJob is at the required phase
* @param {number} phase
*/
ensurePhase(phase) {
if (this.phase < phase) {
this.phase = phase;
this.#link();
}
}
#link() {
// Store itself into the cache first before linking in case there are circular
// references in the linking.
this.#loader.loadCache.set(this.url, this.type, this);
try {
const moduleRequests = this.module.getModuleRequests();
// Specifiers should be aligned with the moduleRequests array in order.
const specifiers = Array(moduleRequests.length);
const modules = Array(moduleRequests.length);
const evaluationDepJobs = Array(moduleRequests.length);
let j = 0;
for (let i = 0; i < moduleRequests.length; ++i) {
const { specifier, attributes, phase } = moduleRequests[i];
const job = this.#loader.getModuleJobForRequire(specifier, this.url, attributes, phase);
specifiers[i] = specifier;
modules[i] = job.module;
if (phase === kEvaluationPhase) {
evaluationDepJobs[j++] = job;
}
}
evaluationDepJobs.length = j;
this.module.link(specifiers, modules);
this.linked = evaluationDepJobs;
} finally {
// Restore it - if it succeeds, we'll reset in the caller; Otherwise it's
// not cached and if the error is caught, subsequent attempt would still fail.
this.#loader.loadCache.delete(this.url, this.type);
}
}
get modulePromise() {
return PromiseResolve(this.module);
}
async run() {
assert(this.phase === kEvaluationPhase);
// This path is hit by a require'd module that is imported again.
const status = this.module.getStatus();
if (status > kInstantiated) {
if (this.evaluationPromise) {
await this.evaluationPromise;
}
return { __proto__: null, module: this.module };
} else if (status === kInstantiated) {
// The evaluation may have been canceled because instantiateSync() detected TLA first.
// But when it is imported again, it's fine to re-evaluate it asynchronously.
const timeout = -1;
const breakOnSigint = false;
this.evaluationPromise = this.module.evaluate(timeout, breakOnSigint);
await this.evaluationPromise;
this.evaluationPromise = undefined;
return { __proto__: null, module: this.module };
}
assert.fail('Unexpected status of a module that is imported again after being required. ' +
`Status = ${status}`);
}
runSync(parent) {
assert(this.phase === kEvaluationPhase);
// TODO(joyeecheung): add the error decoration logic from the async instantiate.
this.module.async = this.module.instantiateSync();
// If --experimental-print-required-tla is true, proceeds to evaluation even
// if it's async because we want to search for the TLA and help users locate
// them.
// TODO(joyeecheung): track the asynchroniticy using v8::Module::HasTopLevelAwait()
// and we'll be able to throw right after compilation of the modules, using acron
// to find and print the TLA.
const parentFilename = urlToFilename(parent?.filename);
const filename = urlToFilename(this.url);
if (this.module.async && !getOptionValue('--experimental-print-required-tla')) {
throw new ERR_REQUIRE_ASYNC_MODULE(filename, parentFilename);
}
setHasStartedUserESMExecution();
try {
const namespace = this.module.evaluateSync(filename, parentFilename);
return { __proto__: null, module: this.module, namespace };
} catch (e) {
explainCommonJSGlobalLikeNotDefinedError(e, this.module.url);
throw e;
}
}
}
ObjectSetPrototypeOf(ModuleJobBase.prototype, null);
module.exports = {
ModuleJob, ModuleJobSync, ModuleJobBase,
};

View File

@ -0,0 +1,128 @@
'use strict';
const {
ArrayPrototypeJoin,
ArrayPrototypeMap,
ArrayPrototypeSort,
JSONStringify,
ObjectKeys,
SafeMap,
} = primordials;
const { kImplicitTypeAttribute } = require('internal/modules/esm/assert');
let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
debug = fn;
});
const { ERR_INVALID_ARG_TYPE } = require('internal/errors').codes;
const { validateString } = require('internal/validators');
/**
* Cache the results of the `resolve` step of the module resolution and loading process.
* Future resolutions of the same input (specifier, parent URL and import attributes)
* must return the same result if the first attempt was successful, per
* https://tc39.es/ecma262/#sec-HostLoadImportedModule.
* This cache is *not* used when custom loaders are registered.
*/
class ResolveCache extends SafeMap {
constructor(i) { super(i); } // eslint-disable-line no-useless-constructor
/**
* Generates the internal serialized cache key and returns it along the actual cache object.
*
* It is exposed to allow more efficient read and overwrite a cache entry.
* @param {string} specifier
* @param {Record<string,string>} importAttributes
* @returns {string}
*/
serializeKey(specifier, importAttributes) {
// To serialize the ModuleRequest (specifier + list of import attributes),
// we need to sort the attributes by key, then stringifying,
// so that different import statements with the same attributes are always treated
// as identical.
const keys = ObjectKeys(importAttributes);
if (keys.length === 0) {
return specifier + '::';
}
return specifier + '::' + ArrayPrototypeJoin(
ArrayPrototypeMap(
ArrayPrototypeSort(keys),
(key) => JSONStringify(key) + JSONStringify(importAttributes[key])),
',');
}
#getModuleCachedImports(parentURL) {
let internalCache = super.get(parentURL);
if (internalCache == null) {
super.set(parentURL, internalCache = { __proto__: null });
}
return internalCache;
}
/**
* @param {string} serializedKey
* @param {string} parentURL
* @returns {import('./loader').ModuleExports | Promise<import('./loader').ModuleExports>}
*/
get(serializedKey, parentURL) {
return this.#getModuleCachedImports(parentURL)[serializedKey];
}
/**
* @param {string} serializedKey
* @param {string} parentURL
* @param {{ format: string, url: URL['href'] }} result
*/
set(serializedKey, parentURL, result) {
this.#getModuleCachedImports(parentURL)[serializedKey] = result;
return this;
}
has(serializedKey, parentURL) {
return serializedKey in this.#getModuleCachedImports(parentURL);
}
}
/**
* Cache the results of the `load` step of the module resolution and loading process.
*/
class LoadCache extends SafeMap {
constructor(i) { super(i); } // eslint-disable-line no-useless-constructor
get(url, type = kImplicitTypeAttribute) {
validateString(url, 'url');
validateString(type, 'type');
return super.get(url)?.[type];
}
set(url, type = kImplicitTypeAttribute, job) {
validateString(url, 'url');
validateString(type, 'type');
const { ModuleJobBase } = require('internal/modules/esm/module_job');
if (job instanceof ModuleJobBase !== true &&
typeof job !== 'function') {
throw new ERR_INVALID_ARG_TYPE('job', 'ModuleJob', job);
}
debug(`Storing ${url} (${
type === kImplicitTypeAttribute ? 'implicit type' : type
}) in ModuleLoadMap`);
const cachedJobsForUrl = super.get(url) ?? { __proto__: null };
cachedJobsForUrl[type] = job;
return super.set(url, cachedJobsForUrl);
}
has(url, type = kImplicitTypeAttribute) {
validateString(url, 'url');
validateString(type, 'type');
return super.get(url)?.[type] !== undefined;
}
delete(url, type = kImplicitTypeAttribute) {
const cached = super.get(url);
if (cached) {
cached[type] = undefined;
}
}
}
module.exports = {
LoadCache,
ResolveCache,
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
// This file contains the definition for the constant values that must be
// available to both the main thread and the loader thread.
'use strict';
/*
The shared memory area is divided in 1 32-bit long section. It has to be 32-bit long as
`Atomics.notify` only works with `Int32Array` objects.
--32-bits--
^
|
|
WORKER_TO_MAIN_THREAD_NOTIFICATION
WORKER_TO_MAIN_THREAD_NOTIFICATION is only used to send notifications, its value is going to
increase every time the worker sends a notification to the main thread.
*/
module.exports = {
WORKER_TO_MAIN_THREAD_NOTIFICATION: 0,
SHARED_MEMORY_BYTE_LENGTH: 1 * 4,
};

View File

@ -0,0 +1,609 @@
'use strict';
const {
ArrayPrototypePush,
FunctionPrototypeCall,
JSONParse,
ObjectAssign,
ObjectPrototypeHasOwnProperty,
ReflectApply,
SafeArrayIterator,
SafeMap,
SafeSet,
SafeWeakMap,
StringPrototypeIncludes,
StringPrototypeReplaceAll,
StringPrototypeSlice,
StringPrototypeStartsWith,
globalThis: { WebAssembly },
} = primordials;
const {
compileFunctionForCJSLoader,
} = internalBinding('contextify');
const { BuiltinModule } = require('internal/bootstrap/realm');
const assert = require('internal/assert');
const { readFileSync } = require('fs');
const { dirname, extname } = require('path');
const {
assertBufferSource,
loadBuiltinModule,
stringify,
stripBOM,
urlToFilename,
} = require('internal/modules/helpers');
const { stripTypeScriptModuleTypes } = require('internal/modules/typescript');
const {
kIsCachedByESMLoader,
Module: CJSModule,
wrapModuleLoad,
kModuleSource,
kModuleExport,
kModuleExportNames,
findLongestRegisteredExtension,
resolveForCJSWithHooks,
loadSourceForCJSWithHooks,
} = require('internal/modules/cjs/loader');
const { fileURLToPath, pathToFileURL, URL } = require('internal/url');
let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
debug = fn;
});
const { emitExperimentalWarning, kEmptyObject, setOwnProperty, isWindows } = require('internal/util');
const {
ERR_INVALID_RETURN_PROPERTY_VALUE,
ERR_UNKNOWN_BUILTIN_MODULE,
} = require('internal/errors').codes;
const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache');
const moduleWrap = internalBinding('module_wrap');
const { ModuleWrap } = moduleWrap;
// Lazy-loading to avoid circular dependencies.
let getSourceSync;
/**
* @param {Parameters<typeof import('./load').getSourceSync>[0]} url
* @returns {ReturnType<typeof import('./load').getSourceSync>}
*/
function getSource(url) {
getSourceSync ??= require('internal/modules/esm/load').getSourceSync;
return getSourceSync(url);
}
/** @type {import('deps/cjs-module-lexer/lexer.js').parse} */
let cjsParse;
/**
* Initializes the CommonJS module lexer parser using the JavaScript version.
* TODO(joyeecheung): Use `require('internal/deps/cjs-module-lexer/dist/lexer').initSync()`
* when cjs-module-lexer 1.4.0 is rolled in.
*/
function initCJSParseSync() {
if (cjsParse === undefined) {
cjsParse = require('internal/deps/cjs-module-lexer/lexer').parse;
}
}
const translators = new SafeMap();
exports.translators = translators;
/**
* Converts a URL to a file path if the URL protocol is 'file:'.
* @param {string} url - The URL to convert.
*/
function errPath(url) {
const parsed = new URL(url);
if (parsed.protocol === 'file:') {
return fileURLToPath(parsed);
}
return url;
}
// Strategy for loading a standard JavaScript module.
translators.set('module', function moduleStrategy(url, source, isMain) {
assertBufferSource(source, true, 'load');
source = stringify(source);
debug(`Translating StandardModule ${url}`);
const { compileSourceTextModule } = require('internal/modules/esm/utils');
const context = isMain ? { isMain } : undefined;
const module = compileSourceTextModule(url, source, this, context);
return module;
});
/**
* Loads a CommonJS module via the ESM Loader sync CommonJS translator.
* This translator creates its own version of the `require` function passed into CommonJS modules.
* Any monkey patches applied to the CommonJS Loader will not affect this module.
* Any `require` calls in this module will load all children in the same way.
* @param {import('internal/modules/cjs/loader').Module} module - The module to load.
* @param {string} source - The source code of the module.
* @param {string} url - The URL of the module.
* @param {string} filename - The filename of the module.
* @param {boolean} isMain - Whether the module is the entrypoint
*/
function loadCJSModule(module, source, url, filename, isMain) {
const compileResult = compileFunctionForCJSLoader(source, filename, false /* is_sea_main */, false);
const { function: compiledWrapper, sourceMapURL, sourceURL } = compileResult;
// Cache the source map for the cjs module if present.
if (sourceMapURL) {
maybeCacheSourceMap(url, source, module, false, sourceURL, sourceMapURL);
}
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
const __dirname = dirname(filename);
// eslint-disable-next-line func-name-matching,func-style
const requireFn = function require(specifier) {
let importAttributes = kEmptyObject;
if (!StringPrototypeStartsWith(specifier, 'node:') && !BuiltinModule.normalizeRequirableId(specifier)) {
// TODO: do not depend on the monkey-patchable CJS loader here.
const path = CJSModule._resolveFilename(specifier, module);
switch (extname(path)) {
case '.json':
importAttributes = { __proto__: null, type: 'json' };
break;
case '.node':
return wrapModuleLoad(specifier, module);
default:
// fall through
}
specifier = `${pathToFileURL(path)}`;
}
const job = cascadedLoader.getModuleJobForRequireInImportedCJS(specifier, url, importAttributes);
job.runSync();
return cjsCache.get(job.url).exports;
};
setOwnProperty(requireFn, 'resolve', function resolve(specifier) {
if (!StringPrototypeStartsWith(specifier, 'node:')) {
const path = CJSModule._resolveFilename(specifier, module);
if (specifier !== path) {
specifier = `${pathToFileURL(path)}`;
}
}
const { url: resolvedURL } = cascadedLoader.resolveSync(specifier, url, kEmptyObject);
return urlToFilename(resolvedURL);
});
setOwnProperty(requireFn, 'main', process.mainModule);
ReflectApply(compiledWrapper, module.exports,
[module.exports, requireFn, module, filename, __dirname]);
setOwnProperty(module, 'loaded', true);
}
// TODO: can we use a weak map instead?
const cjsCache = new SafeMap();
/**
* Creates a ModuleWrap object for a CommonJS module.
* @param {string} url - The URL of the module.
* @param {string} source - The source code of the module.
* @param {boolean} isMain - Whether the module is the main module.
* @param {string} format - Format of the module.
* @param {typeof loadCJSModule} [loadCJS=loadCJSModule] - The function to load the CommonJS module.
* @returns {ModuleWrap} The ModuleWrap object for the CommonJS module.
*/
function createCJSModuleWrap(url, source, isMain, format, loadCJS = loadCJSModule) {
debug(`Translating CJSModule ${url}`);
const filename = urlToFilename(url);
// In case the source was not provided by the `load` step, we need fetch it now.
source = stringify(source ?? getSource(new URL(url)).source);
const { exportNames, module } = cjsPreparseModuleExports(filename, source, format);
cjsCache.set(url, module);
const wrapperNames = [...exportNames];
if (!exportNames.has('default')) {
ArrayPrototypePush(wrapperNames, 'default');
}
if (!exportNames.has('module.exports')) {
ArrayPrototypePush(wrapperNames, 'module.exports');
}
if (isMain) {
setOwnProperty(process, 'mainModule', module);
}
return new ModuleWrap(url, undefined, wrapperNames, function() {
debug(`Loading CJSModule ${url}`);
if (!module.loaded) {
loadCJS(module, source, url, filename, !!isMain);
}
let exports;
if (module[kModuleExport] !== undefined) {
exports = module[kModuleExport];
module[kModuleExport] = undefined;
} else {
({ exports } = module);
}
for (const exportName of exportNames) {
if (exportName === 'default' || exportName === 'module.exports' ||
!ObjectPrototypeHasOwnProperty(exports, exportName)) {
continue;
}
// We might trigger a getter -> dont fail.
let value;
try {
value = exports[exportName];
} catch {
// Continue regardless of error.
}
this.setExport(exportName, value);
}
this.setExport('default', exports);
this.setExport('module.exports', exports);
}, module);
}
/**
* Creates a ModuleWrap object for a CommonJS module without source texts.
* @param {string} url - The URL of the module.
* @param {boolean} isMain - Whether the module is the main module.
* @returns {ModuleWrap} The ModuleWrap object for the CommonJS module.
*/
function createCJSNoSourceModuleWrap(url, isMain) {
debug(`Translating CJSModule without source ${url}`);
const filename = urlToFilename(url);
const module = cjsEmplaceModuleCacheEntry(filename);
cjsCache.set(url, module);
if (isMain) {
setOwnProperty(process, 'mainModule', module);
}
// Addon export names are not known until the addon is loaded.
const exportNames = ['default', 'module.exports'];
return new ModuleWrap(url, undefined, exportNames, function evaluationCallback() {
debug(`Loading CJSModule ${url}`);
if (!module.loaded) {
wrapModuleLoad(filename, null, isMain);
}
/** @type {import('./loader').ModuleExports} */
let exports;
if (module[kModuleExport] !== undefined) {
exports = module[kModuleExport];
module[kModuleExport] = undefined;
} else {
({ exports } = module);
}
this.setExport('default', exports);
this.setExport('module.exports', exports);
}, module);
}
translators.set('commonjs-sync', function requireCommonJS(url, source, isMain) {
initCJSParseSync();
return createCJSModuleWrap(url, source, isMain, 'commonjs', (module, source, url, filename, isMain) => {
assert(module === CJSModule._cache[filename]);
wrapModuleLoad(filename, null, isMain);
});
});
// Handle CommonJS modules referenced by `require` calls.
// This translator function must be sync, as `require` is sync.
translators.set('require-commonjs', (url, source, isMain) => {
initCJSParseSync();
assert(cjsParse);
return createCJSModuleWrap(url, source, isMain, 'commonjs');
});
// Handle CommonJS modules referenced by `require` calls.
// This translator function must be sync, as `require` is sync.
translators.set('require-commonjs-typescript', (url, source, isMain) => {
assert(cjsParse);
const code = stripTypeScriptModuleTypes(stringify(source), url);
return createCJSModuleWrap(url, code, isMain, 'commonjs-typescript');
});
// Handle CommonJS modules referenced by `import` statements or expressions,
// or as the initial entry point when the ESM loader handles a CommonJS entry.
translators.set('commonjs', function commonjsStrategy(url, source, isMain) {
if (!cjsParse) {
initCJSParseSync();
}
// For backward-compatibility, it's possible to return a nullish value for
// CJS source associated with a file: URL. In this case, the source is
// obtained by calling the monkey-patchable CJS loader.
const cjsLoader = source == null ? (module, source, url, filename, isMain) => {
assert(module === CJSModule._cache[filename]);
wrapModuleLoad(filename, undefined, isMain);
} : loadCJSModule;
try {
// We still need to read the FS to detect the exports.
source ??= readFileSync(new URL(url), 'utf8');
} catch {
// Continue regardless of error.
}
return createCJSModuleWrap(url, source, isMain, 'commonjs', cjsLoader);
});
/**
* Get or create an entry in the CJS module cache for the given filename.
* @param {string} filename CJS module filename
* @returns {CJSModule} the cached CJS module entry
*/
function cjsEmplaceModuleCacheEntry(filename, exportNames) {
// TODO: Do we want to keep hitting the user mutable CJS loader here?
let cjsMod = CJSModule._cache[filename];
if (cjsMod) {
return cjsMod;
}
cjsMod = new CJSModule(filename);
cjsMod.filename = filename;
cjsMod.paths = CJSModule._nodeModulePaths(cjsMod.path);
cjsMod[kIsCachedByESMLoader] = true;
CJSModule._cache[filename] = cjsMod;
return cjsMod;
}
/**
* Pre-parses a CommonJS module's exports and re-exports.
* @param {string} filename - The filename of the module.
* @param {string} [source] - The source code of the module.
* @param {string} [format]
*/
function cjsPreparseModuleExports(filename, source, format) {
const module = cjsEmplaceModuleCacheEntry(filename);
if (module[kModuleExportNames] !== undefined) {
return { module, exportNames: module[kModuleExportNames] };
}
if (source === undefined) {
({ source } = loadSourceForCJSWithHooks(module, filename, format));
}
module[kModuleSource] = source;
debug(`Preparsing exports of ${filename}`);
let exports, reexports;
try {
({ exports, reexports } = cjsParse(source || ''));
} catch {
exports = [];
reexports = [];
}
const exportNames = new SafeSet(new SafeArrayIterator(exports));
// Set first for cycles.
module[kModuleExportNames] = exportNames;
// If there are any re-exports e.g. `module.exports = { ...require(...) }`,
// pre-parse the dependencies to find transitively exported names.
if (reexports.length) {
module.filename ??= filename;
module.paths ??= CJSModule._nodeModulePaths(dirname(filename));
for (let i = 0; i < reexports.length; i++) {
debug(`Preparsing re-exports of '${filename}'`);
const reexport = reexports[i];
let resolved;
let format;
try {
({ format, filename: resolved } = resolveForCJSWithHooks(reexport, module, false));
} catch (e) {
debug(`Failed to resolve '${reexport}', skipping`, e);
continue;
}
if (format === 'commonjs' ||
(!BuiltinModule.normalizeRequirableId(resolved) && findLongestRegisteredExtension(resolved) === '.js')) {
const { exportNames: reexportNames } = cjsPreparseModuleExports(resolved, undefined, format);
for (const name of reexportNames) {
exportNames.add(name);
}
}
}
}
return { module, exportNames };
}
// Strategy for loading a node builtin CommonJS module that isn't
// through normal resolution
translators.set('builtin', function builtinStrategy(url) {
debug(`Translating BuiltinModule ${url}`);
// Slice 'node:' scheme
const id = StringPrototypeSlice(url, 5);
const module = loadBuiltinModule(id, url);
cjsCache.set(url, module);
if (!StringPrototypeStartsWith(url, 'node:') || !module) {
throw new ERR_UNKNOWN_BUILTIN_MODULE(url);
}
debug(`Loading BuiltinModule ${url}`);
return module.getESMFacade();
});
// Strategy for loading a JSON file
translators.set('json', function jsonStrategy(url, source) {
assertBufferSource(source, true, 'load');
debug(`Loading JSONModule ${url}`);
const pathname = StringPrototypeStartsWith(url, 'file:') ?
fileURLToPath(url) : null;
const shouldCheckAndPopulateCJSModuleCache =
// We want to involve the CJS loader cache only for `file:` URL with no search query and no hash.
pathname && !StringPrototypeIncludes(url, '?') && !StringPrototypeIncludes(url, '#');
let modulePath;
let module;
if (shouldCheckAndPopulateCJSModuleCache) {
modulePath = isWindows ?
StringPrototypeReplaceAll(pathname, '/', '\\') : pathname;
module = CJSModule._cache[modulePath];
if (module?.loaded) {
const exports = module.exports;
return new ModuleWrap(url, undefined, ['default'], function() {
this.setExport('default', exports);
});
}
}
source = stringify(source);
if (shouldCheckAndPopulateCJSModuleCache) {
// A require call could have been called on the same file during loading and
// that resolves synchronously. To make sure we always return the identical
// export, we have to check again if the module already exists or not.
// TODO: remove CJS loader from here as well.
module = CJSModule._cache[modulePath];
if (module?.loaded) {
const exports = module.exports;
return new ModuleWrap(url, undefined, ['default'], function() {
this.setExport('default', exports);
});
}
}
try {
const exports = JSONParse(stripBOM(source));
module = {
exports,
loaded: true,
};
} catch (err) {
// TODO (BridgeAR): We could add a NodeCore error that wraps the JSON
// parse error instead of just manipulating the original error message.
// That would allow to add further properties and maybe additional
// debugging information.
err.message = errPath(url) + ': ' + err.message;
throw err;
}
if (shouldCheckAndPopulateCJSModuleCache) {
CJSModule._cache[modulePath] = module;
}
cjsCache.set(url, module);
return new ModuleWrap(url, undefined, ['default'], function() {
debug(`Parsing JSONModule ${url}`);
this.setExport('default', module.exports);
});
});
// Strategy for loading a wasm module
// This logic should collapse into WebAssembly Module Record in future.
/**
* @type {WeakMap<
* import('internal/modules/esm/utils').ModuleNamespaceObject,
* WebAssembly.Instance
* >} [[Instance]] slot proxy for WebAssembly Module Record
*/
const wasmInstances = new SafeWeakMap();
translators.set('wasm', async function(url, source) {
emitExperimentalWarning('Importing WebAssembly modules');
assertBufferSource(source, false, 'load');
debug(`Translating WASMModule ${url}`);
let compiled;
try {
// TODO(joyeecheung): implement a translator that just uses
// compiled = new WebAssembly.Module(source) to compile it
// synchronously.
compiled = await WebAssembly.compile(source);
} catch (err) {
err.message = errPath(url) + ': ' + err.message;
throw err;
}
const importsList = new SafeSet();
const wasmGlobalImports = [];
for (const impt of WebAssembly.Module.imports(compiled)) {
if (impt.kind === 'global') {
ArrayPrototypePush(wasmGlobalImports, impt);
}
importsList.add(impt.module);
}
const exportsList = new SafeSet();
const wasmGlobalExports = new SafeSet();
for (const expt of WebAssembly.Module.exports(compiled)) {
if (expt.kind === 'global') {
wasmGlobalExports.add(expt.name);
}
exportsList.add(expt.name);
}
const createDynamicModule = require('internal/modules/esm/create_dynamic_module');
const { module } = createDynamicModule([...importsList], [...exportsList], url, (reflect) => {
for (const impt of importsList) {
const importNs = reflect.imports[impt];
const wasmInstance = wasmInstances.get(importNs);
if (wasmInstance) {
const wrappedModule = ObjectAssign({ __proto__: null }, reflect.imports[impt]);
for (const { module, name } of wasmGlobalImports) {
if (module !== impt) {
continue;
}
// Import of Wasm module global -> get direct WebAssembly.Global wrapped value.
// JS API validations otherwise remain the same.
wrappedModule[name] = wasmInstance[name];
}
reflect.imports[impt] = wrappedModule;
}
}
// In cycles importing unexecuted Wasm, wasmInstance will be undefined, which will fail during
// instantiation, since all bindings will be in the Temporal Deadzone (TDZ).
const { exports } = new WebAssembly.Instance(compiled, reflect.imports);
wasmInstances.set(module.getNamespace(), exports);
for (const expt of exportsList) {
let val = exports[expt];
// Unwrap WebAssembly.Global for JS bindings
if (wasmGlobalExports.has(expt)) {
try {
// v128 will throw in GetGlobalValue, see:
// https://webassembly.github.io/esm-integration/js-api/index.html#getglobalvalue
val = val.value;
} catch {
// v128 doesn't support ToJsValue() -> use undefined (ideally should stay in TDZ)
continue;
}
}
reflect.exports[expt].set(val);
}
});
// WebAssembly modules support source phase imports, to import the compiled module
// separate from the linked instance.
module.setModuleSourceObject(compiled);
return module;
});
// Strategy for loading a addon
translators.set('addon', function translateAddon(url, source, isMain) {
emitExperimentalWarning('Importing addons');
// The addon must be loaded from file system with dlopen. Assert
// the source is null.
if (source !== null) {
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
'null',
'load',
'source',
source);
}
debug(`Translating addon ${url}`);
return createCJSNoSourceModuleWrap(url, isMain);
});
// Strategy for loading a commonjs TypeScript module
translators.set('commonjs-typescript', function(url, source) {
assertBufferSource(source, true, 'load');
const code = stripTypeScriptModuleTypes(stringify(source), url);
debug(`Translating TypeScript ${url}`);
return FunctionPrototypeCall(translators.get('commonjs'), this, url, code, false);
});
// Strategy for loading an esm TypeScript module
translators.set('module-typescript', function(url, source) {
assertBufferSource(source, true, 'load');
const code = stripTypeScriptModuleTypes(stringify(source), url);
debug(`Translating TypeScript ${url}`);
return FunctionPrototypeCall(translators.get('module'), this, url, code, false);
});

View File

@ -0,0 +1,378 @@
'use strict';
const {
ArrayIsArray,
ObjectFreeze,
SafeSet,
SafeWeakMap,
} = primordials;
const {
privateSymbols: {
host_defined_option_symbol,
},
} = internalBinding('util');
const {
source_text_module_default_hdo,
vm_dynamic_import_default_internal,
vm_dynamic_import_main_context_default,
vm_dynamic_import_missing_flag,
vm_dynamic_import_no_callback,
} = internalBinding('symbols');
const {
ModuleWrap,
setImportModuleDynamicallyCallback,
setInitializeImportMetaObjectCallback,
} = internalBinding('module_wrap');
const {
maybeCacheSourceMap,
} = require('internal/source_map/source_map_cache');
const {
ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG,
ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING,
ERR_INVALID_ARG_VALUE,
} = require('internal/errors').codes;
const { getOptionValue } = require('internal/options');
const {
loadPreloadModules,
initializeFrozenIntrinsics,
} = require('internal/process/pre_execution');
const {
emitExperimentalWarning,
getCWDURL,
kEmptyObject,
} = require('internal/util');
const assert = require('internal/assert');
const {
normalizeReferrerURL,
} = require('internal/modules/helpers');
let defaultConditions;
/**
* Returns the default conditions for ES module loading.
*/
function getDefaultConditions() {
assert(defaultConditions !== undefined);
return defaultConditions;
}
/** @type {Set<string>} */
let defaultConditionsSet;
/**
* Returns the default conditions for ES module loading, as a Set.
*/
function getDefaultConditionsSet() {
assert(defaultConditionsSet !== undefined);
return defaultConditionsSet;
}
/**
* Initializes the default conditions for ESM module loading.
* This function is called during pre-execution, before any user code is run.
*/
function initializeDefaultConditions() {
const userConditions = getOptionValue('--conditions');
const noAddons = getOptionValue('--no-addons');
const addonConditions = noAddons ? [] : ['node-addons'];
const moduleConditions = getOptionValue('--experimental-require-module') ? ['module-sync'] : [];
defaultConditions = ObjectFreeze([
'node',
'import',
...moduleConditions,
...addonConditions,
...userConditions,
]);
defaultConditionsSet = new SafeSet(defaultConditions);
}
/**
* @param {string[]} [conditions]
* @returns {Set<string>}
*/
function getConditionsSet(conditions) {
if (conditions !== undefined && conditions !== getDefaultConditions()) {
if (!ArrayIsArray(conditions)) {
throw new ERR_INVALID_ARG_VALUE('conditions', conditions,
'expected an array');
}
return new SafeSet(conditions);
}
return getDefaultConditionsSet();
}
/**
* @typedef {{
* [Symbol.toStringTag]: 'Module',
* }} ModuleNamespaceObject
*/
/**
* @callback ImportModuleDynamicallyCallback
* @param {string} specifier
* @param {ModuleWrap|ContextifyScript|Function|vm.Module} callbackReferrer
* @param {Record<string, string>} attributes
* @param {number} phase
* @returns {Promise<ModuleNamespaceObject>}
*/
/**
* @callback InitializeImportMetaCallback
* @param {object} meta
* @param {ModuleWrap|ContextifyScript|Function|vm.Module} callbackReferrer
*/
/**
* @typedef {{
* callbackReferrer: ModuleWrap|ContextifyScript|Function|vm.Module
* initializeImportMeta? : InitializeImportMetaCallback,
* importModuleDynamically? : ImportModuleDynamicallyCallback
* }} ModuleRegistry
*/
/**
* @type {WeakMap<symbol, ModuleRegistry>}
*/
const moduleRegistries = new SafeWeakMap();
/**
* @typedef {ContextifyScript|Function|ModuleWrap|ContextifiedObject} Referrer
* A referrer can be a Script Record, a Cyclic Module Record, or a Realm Record
* as defined in https://tc39.es/ecma262/#sec-HostLoadImportedModule.
*
* In Node.js, a referrer is represented by a wrapper object of these records.
* A referrer object has a field |host_defined_option_symbol| initialized with
* a symbol.
*/
/**
* V8 would make sure that as long as import() can still be initiated from
* the referrer, the symbol referenced by |host_defined_option_symbol| should
* be alive, which in term would keep the settings object alive through the
* WeakMap, and in turn that keeps the referrer object alive, which would be
* passed into the callbacks.
* The reference goes like this:
* [v8::internal::Script] (via host defined options) ----1--> [idSymbol]
* [callbackReferrer] (via host_defined_option_symbol) ------2------^ |
* ^----------3---- (via WeakMap)------
* 1+3 makes sure that as long as import() can still be initiated, the
* referrer wrap is still around and can be passed into the callbacks.
* 2 is only there so that we can get the id symbol to configure the
* weak map.
* @param {Referrer} referrer The referrer to
* get the id symbol from. This is different from callbackReferrer which
* could be set by the caller.
* @param {ModuleRegistry} registry
*/
function registerModule(referrer, registry) {
const idSymbol = referrer[host_defined_option_symbol];
if (idSymbol === vm_dynamic_import_no_callback ||
idSymbol === vm_dynamic_import_missing_flag ||
idSymbol === vm_dynamic_import_main_context_default ||
idSymbol === vm_dynamic_import_default_internal) {
// The referrer is compiled without custom callbacks, so there is
// no registry to hold on to. We'll throw
// ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING when a callback is
// needed.
return;
}
// To prevent it from being GC'ed.
registry.callbackReferrer ??= referrer;
moduleRegistries.set(idSymbol, registry);
}
/**
* Proxy the import meta handling to the default loader for source text modules.
* @param {Record<string, string | Function>} meta - The import.meta object to initialize.
* @param {ModuleWrap} wrap - The ModuleWrap of the SourceTextModule where `import.meta` is referenced.
*/
function defaultInitializeImportMetaForModule(meta, wrap) {
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
return cascadedLoader.importMetaInitialize(meta, { url: wrap.url, isMain: wrap.isMain });
}
/**
* Defines the `import.meta` object for a given module.
* @param {symbol} symbol - Reference to the module.
* @param {Record<string, string | Function>} meta - The import.meta object to initialize.
* @param {ModuleWrap} wrap - The ModuleWrap of the SourceTextModule where `import.meta` is referenced.
*/
function initializeImportMetaObject(symbol, meta, wrap) {
if (symbol === source_text_module_default_hdo) {
defaultInitializeImportMetaForModule(meta, wrap);
return;
}
const data = moduleRegistries.get(symbol);
assert(data, `import.meta registry not found for ${wrap.url}`);
const { initializeImportMeta, callbackReferrer } = data;
if (initializeImportMeta !== undefined) {
meta = initializeImportMeta(meta, callbackReferrer);
}
}
/**
* Proxy the dynamic import handling to the default loader for source text modules.
* @param {string} specifier - The module specifier string.
* @param {number} phase - The module import phase.
* @param {Record<string, string>} attributes - The import attributes object.
* @param {string|null|undefined} referrerName - name of the referrer.
* @returns {Promise<import('internal/modules/esm/loader.js').ModuleExports>} - The imported module object.
*/
function defaultImportModuleDynamicallyForModule(specifier, phase, attributes, referrerName) {
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
return cascadedLoader.import(specifier, referrerName, attributes, phase);
}
/**
* Proxy the dynamic import to the default loader for classic scripts.
* @param {string} specifier - The module specifier string.
* @param {number} phase - The module import phase.
* @param {Record<string, string>} attributes - The import attributes object.
* @param {string|null|undefined} referrerName - name of the referrer.
* @returns {Promise<import('internal/modules/esm/loader.js').ModuleExports>} - The imported module object.
*/
function defaultImportModuleDynamicallyForScript(specifier, phase, attributes, referrerName) {
const parentURL = normalizeReferrerURL(referrerName);
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
return cascadedLoader.import(specifier, parentURL, attributes, phase);
}
/**
* Asynchronously imports a module dynamically using a callback function. The native callback.
* @param {symbol} referrerSymbol - Referrer symbol of the registered script, function, module, or contextified object.
* @param {string} specifier - The module specifier string.
* @param {number} phase - The module import phase.
* @param {Record<string, string>} attributes - The import attributes object.
* @param {string|null|undefined} referrerName - name of the referrer.
* @returns {Promise<import('internal/modules/esm/loader.js').ModuleExports>} - The imported module object.
* @throws {ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING} - If the callback function is missing.
*/
async function importModuleDynamicallyCallback(referrerSymbol, specifier, phase, attributes,
referrerName) {
// For user-provided vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER, emit the warning
// and fall back to the default loader.
if (referrerSymbol === vm_dynamic_import_main_context_default) {
emitExperimentalWarning('vm.USE_MAIN_CONTEXT_DEFAULT_LOADER');
return defaultImportModuleDynamicallyForScript(specifier, phase, attributes, referrerName);
}
// For script compiled internally that should use the default loader to handle dynamic
// import, proxy the request to the default loader without the warning.
if (referrerSymbol === vm_dynamic_import_default_internal) {
return defaultImportModuleDynamicallyForScript(specifier, phase, attributes, referrerName);
}
// For SourceTextModules compiled internally, proxy the request to the default loader.
if (referrerSymbol === source_text_module_default_hdo) {
return defaultImportModuleDynamicallyForModule(specifier, phase, attributes, referrerName);
}
if (moduleRegistries.has(referrerSymbol)) {
const { importModuleDynamically, callbackReferrer } = moduleRegistries.get(referrerSymbol);
if (importModuleDynamically !== undefined) {
return importModuleDynamically(specifier, callbackReferrer, attributes, phase);
}
}
if (referrerSymbol === vm_dynamic_import_missing_flag) {
throw new ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG();
}
throw new ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING();
}
let _forceDefaultLoader = false;
/**
* Initializes handling of ES modules.
* This is configured during pre-execution. Specifically it's set to true for
* the loader worker in internal/main/worker_thread.js.
* @param {boolean} [forceDefaultLoader=false] - A boolean indicating disabling custom loaders.
*/
function initializeESM(forceDefaultLoader = false) {
_forceDefaultLoader = forceDefaultLoader;
initializeDefaultConditions();
// Setup per-realm callbacks that locate data or callbacks that we keep
// track of for different ESM modules.
setInitializeImportMetaObjectCallback(initializeImportMetaObject);
setImportModuleDynamicallyCallback(importModuleDynamicallyCallback);
}
/**
* Determine whether custom loaders are disabled and it is forced to use the
* default loader.
* @returns {boolean}
*/
function forceDefaultLoader() {
return _forceDefaultLoader;
}
/**
* Register module customization hooks.
*/
async function initializeHooks() {
const customLoaderURLs = getOptionValue('--experimental-loader');
const { Hooks } = require('internal/modules/esm/hooks');
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
const hooks = new Hooks();
cascadedLoader.setCustomizations(hooks);
// We need the loader customizations to be set _before_ we start invoking
// `--require`, otherwise loops can happen because a `--require` script
// might call `register(...)` before we've installed ourselves. These
// global values are magically set in `setupUserModules` just for us and
// we call them in the correct order.
// N.B. This block appears here specifically in order to ensure that
// `--require` calls occur before `--loader` ones do.
loadPreloadModules();
initializeFrozenIntrinsics();
const parentURL = getCWDURL().href;
for (let i = 0; i < customLoaderURLs.length; i++) {
await hooks.register(
customLoaderURLs[i],
parentURL,
);
}
return hooks;
}
/**
* Compile a SourceTextModule for the built-in ESM loader. Register it for default
* source map and import.meta and dynamic import() handling if cascadedLoader is provided.
* @param {string} url URL of the module.
* @param {string} source Source code of the module.
* @param {typeof import('./loader.js').ModuleLoader|undefined} cascadedLoader If provided,
* register the module for default handling.
* @param {{ isMain?: boolean }|undefined} context - context object containing module metadata.
* @returns {ModuleWrap}
*/
function compileSourceTextModule(url, source, cascadedLoader, context = kEmptyObject) {
const hostDefinedOption = cascadedLoader ? source_text_module_default_hdo : undefined;
const wrap = new ModuleWrap(url, undefined, source, 0, 0, hostDefinedOption);
if (!cascadedLoader) {
return wrap;
}
const { isMain } = context;
if (isMain) {
wrap.isMain = true;
}
// Cache the source map for the module if present.
if (wrap.sourceMapURL) {
maybeCacheSourceMap(url, source, wrap, false, wrap.sourceURL, wrap.sourceMapURL);
}
return wrap;
}
module.exports = {
registerModule,
initializeESM,
initializeHooks,
getDefaultConditions,
getConditionsSet,
loaderWorkerId: 'internal/modules/esm/worker',
forceDefaultLoader,
compileSourceTextModule,
};

View File

@ -0,0 +1,266 @@
'use strict';
const {
AtomicsAdd,
AtomicsNotify,
DataViewPrototypeGetBuffer,
Int32Array,
PromisePrototypeThen,
ReflectApply,
SafeSet,
TypedArrayPrototypeGetBuffer,
} = primordials;
const assert = require('internal/assert');
const { clearImmediate, setImmediate } = require('timers');
const {
hasUncaughtExceptionCaptureCallback,
} = require('internal/process/execution');
const {
isArrayBuffer,
isDataView,
isTypedArray,
} = require('util/types');
const { receiveMessageOnPort } = require('internal/worker/io');
const {
WORKER_TO_MAIN_THREAD_NOTIFICATION,
} = require('internal/modules/esm/shared_constants');
const { initializeHooks } = require('internal/modules/esm/utils');
const { isMarkedAsUntransferable } = require('internal/buffer');
/**
* Transfers an ArrayBuffer, TypedArray, or DataView to a worker thread.
* @param {boolean} hasError - Whether an error occurred during transfer.
* @param {ArrayBuffer | TypedArray | DataView} source - The data to transfer.
*/
function transferArrayBuffer(hasError, source) {
if (hasError || source == null) { return; }
let arrayBuffer;
if (isArrayBuffer(source)) {
arrayBuffer = source;
} else if (isTypedArray(source)) {
arrayBuffer = TypedArrayPrototypeGetBuffer(source);
} else if (isDataView(source)) {
arrayBuffer = DataViewPrototypeGetBuffer(source);
}
if (arrayBuffer && !isMarkedAsUntransferable(arrayBuffer)) {
return [arrayBuffer];
}
}
/**
* Wraps a message with a status and body, and serializes the body if necessary.
* @param {string} status - The status of the message.
* @param {unknown} body - The body of the message.
*/
function wrapMessage(status, body) {
if (status === 'success' || body === null ||
(typeof body !== 'object' &&
typeof body !== 'function' &&
typeof body !== 'symbol')) {
return { status, body };
}
let serialized;
let serializationFailed;
try {
const { serializeError } = require('internal/error_serdes');
serialized = serializeError(body);
} catch {
serializationFailed = true;
}
return {
status,
body: {
serialized,
serializationFailed,
},
};
}
/**
* Initializes a worker thread for a customized module loader.
* @param {SharedArrayBuffer} lock - The lock used to synchronize communication between the worker and the main thread.
* @param {MessagePort} syncCommPort - The message port used for synchronous communication between the worker and the
* main thread.
* @param {(err: Error, origin?: string) => void} errorHandler - The function to use for uncaught exceptions.
* @returns {Promise<void>} A promise that resolves when the worker thread has been initialized.
*/
async function customizedModuleWorker(lock, syncCommPort, errorHandler) {
let hooks;
let initializationError;
let hasInitializationError = false;
{
// If a custom hook is calling `process.exit`, we should wake up the main thread
// so it can detect the exit event.
const { exit } = process;
process.exit = function(code) {
syncCommPort.postMessage(wrapMessage('exit', code ?? process.exitCode));
AtomicsAdd(lock, WORKER_TO_MAIN_THREAD_NOTIFICATION, 1);
AtomicsNotify(lock, WORKER_TO_MAIN_THREAD_NOTIFICATION);
return ReflectApply(exit, this, arguments);
};
}
try {
hooks = await initializeHooks();
} catch (exception) {
// If there was an error while parsing and executing a user loader, for example if because a
// loader contained a syntax error, then we need to send the error to the main thread so it can
// be thrown and printed.
hasInitializationError = true;
initializationError = exception;
}
syncCommPort.on('message', handleMessage);
if (hasInitializationError) {
syncCommPort.postMessage(wrapMessage('error', initializationError));
} else {
syncCommPort.postMessage(wrapMessage('success'));
}
// We're ready, so unlock the main thread.
AtomicsAdd(lock, WORKER_TO_MAIN_THREAD_NOTIFICATION, 1);
AtomicsNotify(lock, WORKER_TO_MAIN_THREAD_NOTIFICATION);
let immediate;
/**
* Checks for messages on the syncCommPort and handles them asynchronously.
*/
function checkForMessages() {
immediate = setImmediate(checkForMessages).unref();
// We need to let the event loop tick a few times to give the main thread a chance to send
// follow-up messages.
const response = receiveMessageOnPort(syncCommPort);
if (response !== undefined) {
PromisePrototypeThen(handleMessage(response.message), undefined, errorHandler);
}
}
const unsettledResponsePorts = new SafeSet();
process.on('beforeExit', () => {
for (const port of unsettledResponsePorts) {
port.postMessage(wrapMessage('never-settle'));
}
unsettledResponsePorts.clear();
AtomicsAdd(lock, WORKER_TO_MAIN_THREAD_NOTIFICATION, 1);
AtomicsNotify(lock, WORKER_TO_MAIN_THREAD_NOTIFICATION);
// Attach back the event handler.
syncCommPort.on('message', handleMessage);
// Also check synchronously for a message, in case it's already there.
clearImmediate(immediate);
checkForMessages();
// We don't need the sync check after this tick, as we already have added the event handler.
clearImmediate(immediate);
// Add some work for next tick so the worker cannot exit.
setImmediate(() => {});
});
/**
* Handles incoming messages from the main thread or other workers.
* @param {object} options - The options object.
* @param {string} options.method - The name of the hook.
* @param {Array} options.args - The arguments to pass to the method.
* @param {MessagePort} options.port - The message port to use for communication.
*/
async function handleMessage({ method, args, port }) {
// Each potential exception needs to be caught individually so that the correct error is sent to
// the main thread.
let hasError = false;
let shouldRemoveGlobalErrorHandler = false;
assert(typeof hooks[method] === 'function');
if (port == null && !hasUncaughtExceptionCaptureCallback()) {
// When receiving sync messages, we want to unlock the main thread when there's an exception.
process.on('uncaughtException', errorHandler);
shouldRemoveGlobalErrorHandler = true;
}
// We are about to yield the execution with `await ReflectApply` below. In case the code
// following the `await` never runs, we remove the message handler so the `beforeExit` event
// can be triggered.
syncCommPort.off('message', handleMessage);
// We keep checking for new messages to not miss any.
clearImmediate(immediate);
immediate = setImmediate(checkForMessages).unref();
unsettledResponsePorts.add(port ?? syncCommPort);
let response;
try {
response = await ReflectApply(hooks[method], hooks, args);
} catch (exception) {
hasError = true;
response = exception;
}
unsettledResponsePorts.delete(port ?? syncCommPort);
// Send the method response (or exception) to the main thread.
try {
(port ?? syncCommPort).postMessage(
wrapMessage(hasError ? 'error' : 'success', response),
transferArrayBuffer(hasError, response?.source),
);
} catch (exception) {
// Or send the exception thrown when trying to send the response.
(port ?? syncCommPort).postMessage(wrapMessage('error', exception));
}
if (shouldRemoveGlobalErrorHandler) {
process.off('uncaughtException', errorHandler);
}
syncCommPort.off('message', handleMessage);
// We keep checking for new messages to not miss any.
clearImmediate(immediate);
immediate = setImmediate(checkForMessages).unref();
// To prevent the main thread from terminating before this function completes after unlocking,
// the following process is executed at the end of the function.
AtomicsAdd(lock, WORKER_TO_MAIN_THREAD_NOTIFICATION, 1);
AtomicsNotify(lock, WORKER_TO_MAIN_THREAD_NOTIFICATION);
}
}
/**
* Initializes a worker thread for a module with customized hooks.
* ! Run everything possible within this function so errors get reported.
* @param {{lock: SharedArrayBuffer}} workerData - The lock used to synchronize with the main thread.
* @param {MessagePort} syncCommPort - The communication port used to communicate with the main thread.
*/
module.exports = function setupModuleWorker(workerData, syncCommPort) {
const lock = new Int32Array(workerData.lock);
/**
* Handles errors that occur in the worker thread.
* @param {Error} err - The error that occurred.
* @param {string} [origin='unhandledRejection'] - The origin of the error.
*/
function errorHandler(err, origin = 'unhandledRejection') {
AtomicsAdd(lock, WORKER_TO_MAIN_THREAD_NOTIFICATION, 1);
AtomicsNotify(lock, WORKER_TO_MAIN_THREAD_NOTIFICATION);
process.off('uncaughtException', errorHandler);
if (hasUncaughtExceptionCaptureCallback()) {
process._fatalException(err);
return;
}
internalBinding('errors').triggerUncaughtException(
err,
origin === 'unhandledRejection',
);
}
return PromisePrototypeThen(
customizedModuleWorker(lock, syncCommPort, errorHandler),
undefined,
errorHandler,
);
};

View File

@ -0,0 +1,432 @@
'use strict';
const {
ArrayPrototypeForEach,
ObjectDefineProperty,
ObjectFreeze,
ObjectPrototypeHasOwnProperty,
SafeMap,
SafeSet,
StringPrototypeCharCodeAt,
StringPrototypeIncludes,
StringPrototypeSlice,
StringPrototypeStartsWith,
} = primordials;
const {
ERR_INVALID_ARG_TYPE,
ERR_INVALID_RETURN_PROPERTY_VALUE,
} = require('internal/errors').codes;
const { BuiltinModule } = require('internal/bootstrap/realm');
const { validateString } = require('internal/validators');
const fs = require('fs'); // Import all of `fs` so that it can be monkey-patched.
const internalFS = require('internal/fs/utils');
const path = require('path');
const { pathToFileURL, fileURLToPath } = require('internal/url');
const assert = require('internal/assert');
const { getOptionValue } = require('internal/options');
const { setOwnProperty, getLazy } = require('internal/util');
const { inspect } = require('internal/util/inspect');
const lazyTmpdir = getLazy(() => require('os').tmpdir());
const { join } = path;
const { canParse: URLCanParse } = internalBinding('url');
const {
enableCompileCache: _enableCompileCache,
getCompileCacheDir: _getCompileCacheDir,
compileCacheStatus: _compileCacheStatus,
flushCompileCache,
} = internalBinding('modules');
let debug = require('internal/util/debuglog').debuglog('module', (fn) => {
debug = fn;
});
/** @typedef {import('internal/modules/cjs/loader.js').Module} Module */
/**
* Cache for storing resolved real paths of modules.
* In order to minimize unnecessary lstat() calls, this cache is a list of known-real paths.
* Set to an empty Map to reset.
* @type {Map<string, string>}
*/
const realpathCache = new SafeMap();
/**
* Resolves the path of a given `require` specifier, following symlinks.
* @param {string} requestPath The `require` specifier
*/
function toRealPath(requestPath) {
return fs.realpathSync(requestPath, {
[internalFS.realpathCacheKey]: realpathCache,
});
}
/** @type {Set<string>} */
let cjsConditions;
/**
* Define the conditions that apply to the CommonJS loader.
*/
function initializeCjsConditions() {
const userConditions = getOptionValue('--conditions');
const noAddons = getOptionValue('--no-addons');
const addonConditions = noAddons ? [] : ['node-addons'];
// TODO: Use this set when resolving pkg#exports conditions in loader.js.
cjsConditions = new SafeSet([
'require',
'node',
...addonConditions,
...userConditions,
]);
if (getOptionValue('--experimental-require-module')) {
cjsConditions.add('module-sync');
}
}
/**
* Get the conditions that apply to the CommonJS loader.
*/
function getCjsConditions() {
if (cjsConditions === undefined) {
initializeCjsConditions();
}
return cjsConditions;
}
/**
* Provide one of Node.js' public modules to user code.
* @param {string} id - The identifier/specifier of the builtin module to load
*/
function loadBuiltinModule(id) {
if (!BuiltinModule.canBeRequiredByUsers(id)) {
return;
}
/** @type {import('internal/bootstrap/realm.js').BuiltinModule} */
const mod = BuiltinModule.map.get(id);
debug('load built-in module %s', id);
// compileForPublicLoader() throws if canBeRequiredByUsers is false:
mod.compileForPublicLoader();
return mod;
}
/** @type {Module} */
let $Module = null;
/**
* Import the Module class on first use.
*/
function lazyModule() {
return $Module ??= require('internal/modules/cjs/loader').Module;
}
/**
* Create the module-scoped `require` function to pass into CommonJS modules.
* @param {Module} mod - The module to create the `require` function for.
* @typedef {(specifier: string) => unknown} RequireFunction
*/
function makeRequireFunction(mod) {
// lazy due to cycle
const Module = lazyModule();
if (mod instanceof Module !== true) {
throw new ERR_INVALID_ARG_TYPE('mod', 'Module', mod);
}
function require(path) {
return mod.require(path);
}
/**
* The `resolve` method that gets attached to module-scope `require`.
* @param {string} request
* @param {Parameters<Module['_resolveFilename']>[3]} options
*/
function resolve(request, options) {
validateString(request, 'request');
return Module._resolveFilename(request, mod, false, options);
}
require.resolve = resolve;
/**
* The `paths` method that gets attached to module-scope `require`.
* @param {string} request
*/
function paths(request) {
validateString(request, 'request');
return Module._resolveLookupPaths(request, mod);
}
resolve.paths = paths;
setOwnProperty(require, 'main', process.mainModule);
// Enable support to add extra extension types.
require.extensions = Module._extensions;
require.cache = Module._cache;
return require;
}
/**
* Remove byte order marker. This catches EF BB BF (the UTF-8 BOM)
* because the buffer-to-string conversion in `fs.readFileSync()`
* translates it to FEFF, the UTF-16 BOM.
* @param {string} content
*/
function stripBOM(content) {
if (StringPrototypeCharCodeAt(content) === 0xFEFF) {
content = StringPrototypeSlice(content, 1);
}
return content;
}
/**
* Add built-in modules to a global or REPL scope object.
* @param {Record<string, unknown>} object - The object such as `globalThis` to add the built-in modules to.
* @param {string} dummyModuleName - The label representing the set of built-in modules to add.
*/
function addBuiltinLibsToObject(object, dummyModuleName) {
// Make built-in modules available directly (loaded lazily).
const Module = require('internal/modules/cjs/loader').Module;
const { builtinModules } = Module;
// To require built-in modules in user-land and ignore modules whose
// `canBeRequiredByUsers` is false. So we create a dummy module object and not
// use `require()` directly.
const dummyModule = new Module(dummyModuleName);
ArrayPrototypeForEach(builtinModules, (name) => {
// Neither add underscored modules, nor ones that contain slashes (e.g.,
// 'fs/promises') or ones that are already defined.
if (name[0] === '_' ||
StringPrototypeIncludes(name, '/') ||
ObjectPrototypeHasOwnProperty(object, name)) {
return;
}
// Goals of this mechanism are:
// - Lazy loading of built-in modules
// - Having all built-in modules available as non-enumerable properties
// - Allowing the user to re-assign these variables as if there were no
// pre-existing globals with the same name.
const setReal = (val) => {
// Deleting the property before re-assigning it disables the
// getter/setter mechanism.
delete object[name];
object[name] = val;
};
ObjectDefineProperty(object, name, {
__proto__: null,
get: () => {
const lib = dummyModule.require(name);
try {
// Override the current getter/setter and set up a new
// non-enumerable property.
ObjectDefineProperty(object, name, {
__proto__: null,
get: () => lib,
set: setReal,
configurable: true,
enumerable: false,
});
} catch {
// If the property is no longer configurable, ignore the error.
}
return lib;
},
set: setReal,
configurable: true,
enumerable: false,
});
});
}
/**
* Normalize the referrer name as a URL.
* If it's a string containing an absolute path or a URL it's normalized as
* a URL string.
* Otherwise it's returned as undefined.
* @param {string | null | undefined} referrerName
* @returns {string | undefined}
*/
function normalizeReferrerURL(referrerName) {
if (referrerName === null || referrerName === undefined) {
return undefined;
}
if (typeof referrerName === 'string') {
if (path.isAbsolute(referrerName)) {
return pathToFileURL(referrerName).href;
}
if (StringPrototypeStartsWith(referrerName, 'file://') ||
URLCanParse(referrerName)) {
return referrerName;
}
return undefined;
}
assert.fail('Unreachable code reached by ' + inspect(referrerName));
}
/**
* @param {string|undefined} url URL to convert to filename
*/
function urlToFilename(url) {
if (url && StringPrototypeStartsWith(url, 'file://')) {
return fileURLToPath(url);
}
return url;
}
// Whether we have started executing any user-provided CJS code.
// This is set right before we call the wrapped CJS code (not after,
// in case we are half-way in the execution when internals check this).
// Used for internal assertions.
let _hasStartedUserCJSExecution = false;
// Similar to _hasStartedUserCJSExecution but for ESM. This is set
// right before ESM evaluation in the default ESM loader. We do not
// update this during vm SourceTextModule execution because at that point
// some user code must already have been run to execute code via vm
// there is little value checking whether any user JS code is run anyway.
let _hasStartedUserESMExecution = false;
/**
* Load a public built-in module. ID may or may not be prefixed by `node:` and
* will be normalized.
* @param {string} id ID of the built-in to be loaded.
* @returns {object|undefined} exports of the built-in. Undefined if the built-in
* does not exist.
*/
function getBuiltinModule(id) {
validateString(id, 'id');
const normalizedId = BuiltinModule.normalizeRequirableId(id);
return normalizedId ? require(normalizedId) : undefined;
}
/** @type {import('internal/util/types')} */
let _TYPES = null;
/**
* Lazily loads and returns the internal/util/types module.
*/
function lazyTypes() {
if (_TYPES !== null) { return _TYPES; }
return _TYPES = require('internal/util/types');
}
/**
* Asserts that the given body is a buffer source (either a string, array buffer, or typed array).
* Throws an error if the body is not a buffer source.
* @param {string | ArrayBufferView | ArrayBuffer} body - The body to check.
* @param {boolean} allowString - Whether or not to allow a string as a valid buffer source.
* @param {string} hookName - The name of the hook being called.
* @throws {ERR_INVALID_RETURN_PROPERTY_VALUE} If the body is not a buffer source.
*/
function assertBufferSource(body, allowString, hookName) {
if (allowString && typeof body === 'string') {
return;
}
const { isArrayBufferView, isAnyArrayBuffer } = lazyTypes();
if (isArrayBufferView(body) || isAnyArrayBuffer(body)) {
return;
}
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
`${allowString ? 'string, ' : ''}array buffer, or typed array`,
hookName,
'source',
body,
);
}
let DECODER = null;
/**
* Converts a buffer or buffer-like object to a string.
* @param {string | ArrayBuffer | ArrayBufferView} body - The buffer or buffer-like object to convert to a string.
* @returns {string} The resulting string.
*/
function stringify(body) {
if (typeof body === 'string') { return body; }
assertBufferSource(body, false, 'load');
const { TextDecoder } = require('internal/encoding');
DECODER = DECODER === null ? new TextDecoder() : DECODER;
return DECODER.decode(body);
}
/**
* Enable on-disk compiled cache for all user modules being complied in the current Node.js instance
* after this method is called.
* If cacheDir is undefined, defaults to the NODE_MODULE_CACHE environment variable.
* If NODE_MODULE_CACHE isn't set, default to path.join(os.tmpdir(), 'node-compile-cache').
* @param {string|undefined} cacheDir
* @returns {{status: number, message?: string, directory?: string}}
*/
function enableCompileCache(cacheDir) {
if (cacheDir === undefined) {
cacheDir = join(lazyTmpdir(), 'node-compile-cache');
}
const nativeResult = _enableCompileCache(cacheDir);
const result = { status: nativeResult[0] };
if (nativeResult[1]) {
result.message = nativeResult[1];
}
if (nativeResult[2]) {
result.directory = nativeResult[2];
}
return result;
}
const compileCacheStatus = { __proto__: null };
for (let i = 0; i < _compileCacheStatus.length; ++i) {
compileCacheStatus[_compileCacheStatus[i]] = i;
}
ObjectFreeze(compileCacheStatus);
const constants = { __proto__: null, compileCacheStatus };
ObjectFreeze(constants);
/**
* Get the compile cache directory if on-disk compile cache is enabled.
* @returns {string|undefined} Path to the module compile cache directory if it is enabled,
* or undefined otherwise.
*/
function getCompileCacheDir() {
return _getCompileCacheDir() || undefined;
}
module.exports = {
addBuiltinLibsToObject,
assertBufferSource,
constants,
enableCompileCache,
flushCompileCache,
getBuiltinModule,
getCjsConditions,
getCompileCacheDir,
initializeCjsConditions,
loadBuiltinModule,
makeRequireFunction,
normalizeReferrerURL,
stringify,
stripBOM,
toRealPath,
hasStartedUserCJSExecution() {
return _hasStartedUserCJSExecution;
},
setHasStartedUserCJSExecution() {
_hasStartedUserCJSExecution = true;
},
hasStartedUserESMExecution() {
return _hasStartedUserESMExecution;
},
setHasStartedUserESMExecution() {
_hasStartedUserESMExecution = true;
},
urlToFilename,
};

View File

@ -0,0 +1,316 @@
'use strict';
const {
ArrayIsArray,
JSONParse,
ObjectDefineProperty,
RegExpPrototypeExec,
StringPrototypeIndexOf,
StringPrototypeSlice,
} = primordials;
const {
fileURLToPath,
isURL,
pathToFileURL,
URL,
} = require('internal/url');
const { canParse: URLCanParse } = internalBinding('url');
const {
codes: {
ERR_INVALID_MODULE_SPECIFIER,
ERR_MISSING_ARGS,
ERR_MODULE_NOT_FOUND,
},
} = require('internal/errors');
const { kEmptyObject } = require('internal/util');
const modulesBinding = internalBinding('modules');
const path = require('path');
const { validateString } = require('internal/validators');
const internalFsBinding = internalBinding('fs');
/**
* @typedef {import('typings/internalBinding/modules').DeserializedPackageConfig} DeserializedPackageConfig
* @typedef {import('typings/internalBinding/modules').PackageConfig} PackageConfig
* @typedef {import('typings/internalBinding/modules').SerializedPackageConfig} SerializedPackageConfig
*/
/**
* @param {URL['pathname']} path
* @param {SerializedPackageConfig} contents
* @returns {DeserializedPackageConfig}
*/
function deserializePackageJSON(path, contents) {
if (contents === undefined) {
return {
data: {
__proto__: null,
type: 'none', // Ignore unknown types for forwards compatibility
},
exists: false,
path,
};
}
const {
0: name,
1: main,
2: type,
3: plainImports,
4: plainExports,
5: optionalFilePath,
} = contents;
const pjsonPath = optionalFilePath ?? path;
return {
data: {
__proto__: null,
...(name != null && { name }),
...(main != null && { main }),
...(type != null && { type }),
...(plainImports != null && {
// This getters are used to lazily parse the imports and exports fields.
get imports() {
const value = requiresJSONParse(plainImports) ? JSONParse(plainImports) : plainImports;
ObjectDefineProperty(this, 'imports', { __proto__: null, value });
return this.imports;
},
}),
...(plainExports != null && {
get exports() {
const value = requiresJSONParse(plainExports) ? JSONParse(plainExports) : plainExports;
ObjectDefineProperty(this, 'exports', { __proto__: null, value });
return this.exports;
},
}),
},
exists: true,
path: pjsonPath,
};
}
// The imports and exports fields can be either undefined or a string.
// - If it's a string, it's either plain string or a stringified JSON string.
// - If it's a stringified JSON string, it starts with either '[' or '{'.
const requiresJSONParse = (value) => (value !== undefined && (value[0] === '[' || value[0] === '{'));
/**
* Reads a package.json file and returns the parsed contents.
* @param {string} jsonPath
* @param {{
* base?: URL | string,
* specifier?: URL | string,
* isESM?: boolean,
* }} options
* @returns {PackageConfig}
*/
function read(jsonPath, { base, specifier, isESM } = kEmptyObject) {
// This function will be called by both CJS and ESM, so we need to make sure
// non-null attributes are converted to strings.
const parsed = modulesBinding.readPackageJSON(
jsonPath,
isESM,
base == null ? undefined : `${base}`,
specifier == null ? undefined : `${specifier}`,
);
const result = deserializePackageJSON(jsonPath, parsed);
return {
__proto__: null,
...result.data,
exists: result.exists,
pjsonPath: result.path,
};
}
/**
* Get the nearest parent package.json file from a given path.
* Return the package.json data and the path to the package.json file, or undefined.
* @param {string} checkPath The path to start searching from.
* @returns {undefined | DeserializedPackageConfig}
*/
function getNearestParentPackageJSON(checkPath) {
const result = modulesBinding.getNearestParentPackageJSON(checkPath);
if (result === undefined) {
return undefined;
}
return deserializePackageJSON(checkPath, result);
}
/**
* Returns the package configuration for the given resolved URL.
* @param {URL | string} resolved - The resolved URL.
* @returns {import('typings/internalBinding/modules').PackageConfig} - The package configuration.
*/
function getPackageScopeConfig(resolved) {
const result = modulesBinding.getPackageScopeConfig(`${resolved}`);
if (ArrayIsArray(result)) {
const { data, exists, path } = deserializePackageJSON(`${resolved}`, result);
return {
__proto__: null,
...data,
exists,
pjsonPath: path,
};
}
// This means that the response is a string
// and it is the path to the package.json file
return {
__proto__: null,
pjsonPath: result,
exists: false,
type: 'none',
};
}
/**
* Returns the package type for a given URL.
* @param {URL} url - The URL to get the package type for.
*/
function getPackageType(url) {
const type = modulesBinding.getPackageType(`${url}`);
return type ?? 'none';
}
const invalidPackageNameRegEx = /^\.|%|\\/;
/**
* Parse a package name from a specifier.
* @param {string} specifier - The import specifier.
* @param {string | URL | undefined} base - The parent URL.
*/
function parsePackageName(specifier, base) {
let separatorIndex = StringPrototypeIndexOf(specifier, '/');
let validPackageName = true;
let isScoped = false;
if (specifier[0] === '@') {
isScoped = true;
if (separatorIndex === -1 || specifier.length === 0) {
validPackageName = false;
} else {
separatorIndex = StringPrototypeIndexOf(
specifier, '/', separatorIndex + 1);
}
}
const packageName = separatorIndex === -1 ?
specifier : StringPrototypeSlice(specifier, 0, separatorIndex);
// Package name cannot have leading . and cannot have percent-encoding or
// \\ separators.
if (RegExpPrototypeExec(invalidPackageNameRegEx, packageName) !== null) {
validPackageName = false;
}
if (!validPackageName) {
throw new ERR_INVALID_MODULE_SPECIFIER(
specifier, 'is not a valid package name', fileURLToPath(base));
}
const packageSubpath = '.' + (separatorIndex === -1 ? '' :
StringPrototypeSlice(specifier, separatorIndex));
return { packageName, packageSubpath, isScoped };
}
function getPackageJSONURL(specifier, base) {
const { packageName, packageSubpath, isScoped } = parsePackageName(specifier, base);
// ResolveSelf
const packageConfig = getPackageScopeConfig(base);
if (packageConfig.exists) {
if (packageConfig.exports != null && packageConfig.name === packageName) {
const packageJSONPath = packageConfig.pjsonPath;
return { packageJSONUrl: pathToFileURL(packageJSONPath), packageJSONPath, packageSubpath };
}
}
let packageJSONUrl = new URL(`./node_modules/${packageName}/package.json`, base);
let packageJSONPath = fileURLToPath(packageJSONUrl);
let lastPath;
do {
const stat = internalFsBinding.internalModuleStat(
StringPrototypeSlice(packageJSONPath, 0, packageJSONPath.length - 13),
);
// Check for !stat.isDirectory()
if (stat !== 1) {
lastPath = packageJSONPath;
packageJSONUrl = new URL(
`${isScoped ? '../' : ''}../../../node_modules/${packageName}/package.json`,
packageJSONUrl,
);
packageJSONPath = fileURLToPath(packageJSONUrl);
continue;
}
// Package match.
return { packageJSONUrl, packageJSONPath, packageSubpath };
} while (packageJSONPath.length !== lastPath.length);
throw new ERR_MODULE_NOT_FOUND(packageName, fileURLToPath(base), null);
}
/** @type {import('./esm/resolve.js').defaultResolve} */
let defaultResolve;
/**
* @param {URL['href'] | string | URL} specifier The location for which to get the "root" package.json
* @param {URL['href'] | string | URL} [base] The location of the current module (ex file://tmp/foo.js).
*/
function findPackageJSON(specifier, base = 'data:') {
if (arguments.length === 0) {
throw new ERR_MISSING_ARGS('specifier');
}
try {
specifier = `${specifier}`;
} catch {
validateString(specifier, 'specifier');
}
let parentURL = base;
if (!isURL(base)) {
validateString(base, 'base');
parentURL = path.isAbsolute(base) ? pathToFileURL(base) : new URL(base);
}
if (specifier && specifier[0] !== '.' && specifier[0] !== '/' && !URLCanParse(specifier)) {
// If `specifier` is a bare specifier.
const { packageJSONPath } = getPackageJSONURL(specifier, parentURL);
return packageJSONPath;
}
let resolvedTarget;
defaultResolve ??= require('internal/modules/esm/resolve').defaultResolve;
try {
// TODO(@JakobJingleheimer): Detect whether findPackageJSON is being used within a loader
// (possibly piggyback on `allowImportMetaResolve`)
// - When inside, use the default resolve
// - (I think it's impossible to use the chain because of re-entry & a deadlock from atomics).
// - When outside, use cascadedLoader.resolveSync (not implemented yet, but the pieces exist).
resolvedTarget = defaultResolve(specifier, { parentURL: `${parentURL}` }).url;
} catch (err) {
if (err.code === 'ERR_UNSUPPORTED_DIR_IMPORT') {
resolvedTarget = err.url;
} else {
throw err;
}
}
const pkg = getNearestParentPackageJSON(fileURLToPath(resolvedTarget));
return pkg?.path;
}
module.exports = {
read,
getNearestParentPackageJSON,
getPackageScopeConfig,
getPackageType,
getPackageJSONURL,
findPackageJSON,
};

View File

@ -0,0 +1,168 @@
'use strict';
const {
StringPrototypeEndsWith,
globalThis,
} = primordials;
const { getNearestParentPackageJSONType } = internalBinding('modules');
const { getOptionValue } = require('internal/options');
const path = require('path');
const { pathToFileURL, URL } = require('internal/url');
const { kEmptyObject, getCWDURL } = require('internal/util');
const {
hasUncaughtExceptionCaptureCallback,
} = require('internal/process/execution');
const {
triggerUncaughtException,
} = internalBinding('errors');
const {
privateSymbols: {
entry_point_promise_private_symbol,
},
} = internalBinding('util');
/**
* Get the absolute path to the main entry point.
* @param {string} main - Entry point path
*/
function resolveMainPath(main) {
/** @type {string} */
let mainPath;
// Extension searching for the main entry point is supported for backward compatibility.
// Module._findPath is monkey-patchable here.
const { Module } = require('internal/modules/cjs/loader');
mainPath = Module._findPath(path.resolve(main), null, true);
if (!mainPath) { return; }
const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
if (!preserveSymlinksMain) {
const { toRealPath } = require('internal/modules/helpers');
mainPath = toRealPath(mainPath);
}
return mainPath;
}
/**
* Determine whether the main entry point should be loaded through the ESM Loader.
* @param {string} mainPath - Absolute path to the main entry point
*/
function shouldUseESMLoader(mainPath) {
/**
* @type {string[]} userLoaders A list of custom loaders registered by the user
* (or an empty list when none have been registered).
*/
const userLoaders = getOptionValue('--experimental-loader');
/**
* @type {string[]} userImports A list of preloaded modules registered by the user
* (or an empty list when none have been registered).
*/
const userImports = getOptionValue('--import');
if (userLoaders.length > 0 || userImports.length > 0) { return true; }
// Determine the module format of the entry point.
if (mainPath && StringPrototypeEndsWith(mainPath, '.mjs')) { return true; }
if (mainPath && StringPrototypeEndsWith(mainPath, '.wasm')) { return true; }
if (!mainPath || StringPrototypeEndsWith(mainPath, '.cjs')) { return false; }
if (getOptionValue('--experimental-strip-types')) {
if (!mainPath || StringPrototypeEndsWith(mainPath, '.cts')) { return false; }
// This will likely change in the future to start with commonjs loader by default
if (mainPath && StringPrototypeEndsWith(mainPath, '.mts')) { return true; }
}
const type = getNearestParentPackageJSONType(mainPath);
// No package.json or no `type` field.
if (type === undefined || type === 'none') {
return false;
}
return type === 'module';
}
/**
* @param {function(ModuleLoader):ModuleWrap|undefined} callback
*/
async function asyncRunEntryPointWithESMLoader(callback) {
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
try {
const userImports = getOptionValue('--import');
if (userImports.length > 0) {
const parentURL = getCWDURL().href;
for (let i = 0; i < userImports.length; i++) {
await cascadedLoader.import(userImports[i], parentURL, kEmptyObject);
}
} else {
cascadedLoader.forceLoadHooks();
}
await callback(cascadedLoader);
} catch (err) {
if (hasUncaughtExceptionCaptureCallback()) {
process._fatalException(err);
return;
}
triggerUncaughtException(
err,
true, /* fromPromise */
);
}
}
/**
* This initializes the ESM loader and runs --import (if any) before executing the
* callback to run the entry point.
* If the callback intends to evaluate a ESM module as entry point, it should return
* the corresponding ModuleWrap so that stalled TLA can be checked a process exit.
* @param {function(ModuleLoader):ModuleWrap|undefined} callback
* @returns {Promise}
*/
function runEntryPointWithESMLoader(callback) {
const promise = asyncRunEntryPointWithESMLoader(callback);
// Register the promise - if by the time the event loop finishes running, this is
// still unsettled, we'll search the graph from the entry point module and print
// the location of any unsettled top-level await found.
globalThis[entry_point_promise_private_symbol] = promise;
return promise;
}
/**
* Parse the CLI main entry point string and run it.
* For backwards compatibility, we have to run a bunch of monkey-patchable code that belongs to the CJS loader (exposed
* by `require('module')`) even when the entry point is ESM.
* Because of backwards compatibility, this function is exposed publicly via `import { runMain } from 'node:module'`.
* Because of module detection, this function will attempt to run ambiguous (no explicit extension, no
* `package.json` type field) entry points as CommonJS first; under certain conditions, it will retry running as ESM.
* @param {string} main - First positional CLI argument, such as `'entry.js'` from `node entry.js`
*/
function executeUserEntryPoint(main = process.argv[1]) {
let useESMLoader;
let resolvedMain;
if (getOptionValue('--entry-url')) {
useESMLoader = true;
} else {
resolvedMain = resolveMainPath(main);
useESMLoader = shouldUseESMLoader(resolvedMain);
}
// Unless we know we should use the ESM loader to handle the entry point per the checks in `shouldUseESMLoader`, first
// try to run the entry point via the CommonJS loader; and if that fails under certain conditions, retry as ESM.
if (!useESMLoader) {
const cjsLoader = require('internal/modules/cjs/loader');
const { wrapModuleLoad } = cjsLoader;
wrapModuleLoad(main, null, true);
} else {
const mainPath = resolvedMain || main;
const mainURL = getOptionValue('--entry-url') ? new URL(mainPath, getCWDURL()) : pathToFileURL(mainPath);
runEntryPointWithESMLoader((cascadedLoader) => {
// Note that if the graph contains unsettled TLA, this may never resolve
// even after the event loop stops running.
return cascadedLoader.import(mainURL, undefined, { __proto__: null }, undefined, true);
});
}
}
module.exports = {
executeUserEntryPoint,
runEntryPointWithESMLoader,
};

View File

@ -0,0 +1,241 @@
'use strict';
const {
ObjectPrototypeHasOwnProperty,
} = primordials;
const {
validateBoolean,
validateOneOf,
validateObject,
validateString,
} = require('internal/validators');
const { assertTypeScript,
emitExperimentalWarning,
getLazy,
isUnderNodeModules,
kEmptyObject } = require('internal/util');
const {
ERR_INVALID_TYPESCRIPT_SYNTAX,
ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING,
ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX,
} = require('internal/errors').codes;
const { getOptionValue } = require('internal/options');
const assert = require('internal/assert');
const { Buffer } = require('buffer');
const {
getCompileCacheEntry,
saveCompileCacheEntry,
cachedCodeTypes: { kStrippedTypeScript, kTransformedTypeScript, kTransformedTypeScriptWithSourceMaps },
} = internalBinding('modules');
/**
* The TypeScript parsing mode, either 'strip-only' or 'transform'.
* @type {string}
*/
const getTypeScriptParsingMode = getLazy(() =>
(getOptionValue('--experimental-transform-types') ? 'transform' : 'strip-only'),
);
/**
* Load the TypeScript parser.
* and returns an object with a `code` property.
* @returns {Function} The TypeScript parser function.
*/
const loadTypeScriptParser = getLazy(() => {
assertTypeScript();
const amaro = require('internal/deps/amaro/dist/index');
return amaro.transformSync;
});
/**
*
* @param {string} source the source code
* @param {object} options the options to pass to the parser
* @returns {TransformOutput} an object with a `code` property.
*/
function parseTypeScript(source, options) {
const parse = loadTypeScriptParser();
try {
return parse(source, options);
} catch (error) {
/**
* Amaro v0.3.0 (from SWC v1.10.7) throws an object with `message` and `code` properties.
* It allows us to distinguish between invalid syntax and unsupported syntax.
*/
switch (error?.code) {
case 'UnsupportedSyntax': {
const unsupportedSyntaxError = new ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX(error.message);
throw decorateErrorWithSnippet(unsupportedSyntaxError, error); /* node-do-not-add-exception-line */
}
case 'InvalidSyntax': {
const invalidSyntaxError = new ERR_INVALID_TYPESCRIPT_SYNTAX(error.message);
throw decorateErrorWithSnippet(invalidSyntaxError, error); /* node-do-not-add-exception-line */
}
default:
// SWC may throw strings when something goes wrong.
if (typeof error === 'string') { assert.fail(error); }
assert(error != null && ObjectPrototypeHasOwnProperty(error, 'message'));
assert.fail(error.message);
}
}
}
/**
*
* @param {Error} error the error to decorate: ERR_INVALID_TYPESCRIPT_SYNTAX, ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX
* @param {object} amaroError the error object from amaro
* @returns {Error} the decorated error
*/
function decorateErrorWithSnippet(error, amaroError) {
const errorHints = `${amaroError.filename}:${amaroError.startLine}\n${amaroError.snippet}`;
error.stack = `${errorHints}\n${error.stack}`;
return error;
}
/**
* Performs type-stripping to TypeScript source code.
* @param {string} code TypeScript code to parse.
* @param {TransformOptions} options The configuration for type stripping.
* @returns {string} The stripped TypeScript code.
*/
function stripTypeScriptTypes(code, options = kEmptyObject) {
emitExperimentalWarning('stripTypeScriptTypes');
validateString(code, 'code');
validateObject(options, 'options');
const {
sourceMap = false,
sourceUrl = '',
} = options;
let { mode = 'strip' } = options;
validateOneOf(mode, 'options.mode', ['strip', 'transform']);
validateBoolean(sourceMap, 'options.sourceMap');
validateString(sourceUrl, 'options.sourceUrl');
if (mode === 'strip') {
validateOneOf(sourceMap, 'options.sourceMap', [false, undefined]);
// Rename mode from 'strip' to 'strip-only'.
// The reason is to match `process.features.typescript` which returns `strip`,
// but the parser expects `strip-only`.
mode = 'strip-only';
}
return processTypeScriptCode(code, {
mode,
sourceMap,
filename: sourceUrl,
});
}
/**
* @typedef {'strip-only' | 'transform'} TypeScriptMode
* @typedef {object} TypeScriptOptions
* @property {TypeScriptMode} mode Mode.
* @property {boolean} sourceMap Whether to generate source maps.
* @property {string|undefined} filename Filename.
*/
/**
* Processes TypeScript code by stripping types or transforming.
* Handles source maps if needed.
* @param {string} code TypeScript code to process.
* @param {TypeScriptOptions} options The configuration object.
* @returns {string} The processed code.
*/
function processTypeScriptCode(code, options) {
const { code: transformedCode, map } = parseTypeScript(code, options);
if (map) {
return addSourceMap(transformedCode, map);
}
if (options.filename) {
return `${transformedCode}\n\n//# sourceURL=${options.filename}`;
}
return transformedCode;
}
/**
* Get the type enum used for compile cache.
* @param {TypeScriptMode} mode Mode of transpilation.
* @param {boolean} sourceMap Whether source maps are enabled.
* @returns {number}
*/
function getCachedCodeType(mode, sourceMap) {
if (mode === 'transform') {
if (sourceMap) { return kTransformedTypeScriptWithSourceMaps; }
return kTransformedTypeScript;
}
return kStrippedTypeScript;
}
/**
* Performs type-stripping to TypeScript source code internally.
* It is used by internal loaders.
* @param {string} source TypeScript code to parse.
* @param {string} filename The filename of the source code.
* @param {boolean} emitWarning Whether to emit a warning.
* @returns {TransformOutput} The stripped TypeScript code.
*/
function stripTypeScriptModuleTypes(source, filename, emitWarning = true) {
if (emitWarning) {
emitExperimentalWarning('Type Stripping');
}
assert(typeof source === 'string');
if (isUnderNodeModules(filename)) {
throw new ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING(filename);
}
const sourceMap = getOptionValue('--enable-source-maps');
const mode = getTypeScriptParsingMode();
// Instead of caching the compile cache status, just go into C++ to fetch it,
// as checking process.env equally involves calling into C++ anyway, and
// the compile cache can be enabled dynamically.
const type = getCachedCodeType(mode, sourceMap);
// Get a compile cache entry into the native compile cache store,
// keyed by the filename. If the cache can already be loaded on disk,
// cached.transpiled contains the cached string. Otherwise we should do
// the transpilation and save it in the native store later using
// saveCompileCacheEntry().
const cached = (filename ? getCompileCacheEntry(source, filename, type) : undefined);
if (cached?.transpiled) { // TODO(joyeecheung): return Buffer here.
return cached.transpiled;
}
const options = {
mode,
sourceMap,
filename,
};
const transpiled = processTypeScriptCode(source, options);
if (cached) {
// cached.external contains a pointer to the native cache entry.
// The cached object would be unreachable once it's out of scope,
// but the pointer inside cached.external would stay around for reuse until
// environment shutdown or when the cache is manually flushed
// to disk. Unwrap it in JS before passing into C++ since it's faster.
saveCompileCacheEntry(cached.external, transpiled);
}
return transpiled;
}
/**
*
* @param {string} code The compiled code.
* @param {string} sourceMap The source map.
* @returns {string} The code with the source map attached.
*/
function addSourceMap(code, sourceMap) {
// The base64 encoding should be https://datatracker.ietf.org/doc/html/rfc4648#section-4,
// not base64url https://datatracker.ietf.org/doc/html/rfc4648#section-5. See data url
// spec https://tools.ietf.org/html/rfc2397#section-2.
const base64SourceMap = Buffer.from(sourceMap).toString('base64');
return `${code}\n\n//# sourceMappingURL=data:application/json;base64,${base64SourceMap}`;
}
module.exports = {
stripTypeScriptModuleTypes,
stripTypeScriptTypes,
};