- For imported CJS, if it's not customized by asynchronous hooks, make sure it won't use the quirky re-invented require in all cases. - When the imported CJS module is customized by synchronous hooks, in the synthetic module evalutation step, avoid calling the respective default step again. - Make the branching of loadCJSModuleWithModuleLoad() and loadCJSModuleWithSpecialRequire() more explicit, and fold the tentative fs read in the 'commonjs' translator into the share createCJSModuleWrap() helper instead of checking it twice in the same path. Signed-off-by: Joyee Cheung <joyeec9h3@gmail.com> PR-URL: https://github.com/nodejs/node/pull/62920 Fixes: https://github.com/nodejs/node/issues/63060 Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
683 lines
26 KiB
JavaScript
683 lines
26 KiB
JavaScript
'use strict';
|
|
|
|
const {
|
|
ArrayPrototypePush,
|
|
FunctionPrototypeCall,
|
|
JSONParse,
|
|
ObjectAssign,
|
|
ObjectPrototypeHasOwnProperty,
|
|
SafeMap,
|
|
SafeSet,
|
|
SafeWeakMap,
|
|
StringPrototypeIncludes,
|
|
StringPrototypeReplaceAll,
|
|
StringPrototypeSlice,
|
|
StringPrototypeStartsWith,
|
|
globalThis,
|
|
} = primordials;
|
|
|
|
const {
|
|
compileFunctionForCJSLoader,
|
|
} = internalBinding('contextify');
|
|
|
|
const { BuiltinModule } = require('internal/bootstrap/realm');
|
|
const assert = require('internal/assert');
|
|
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,
|
|
populateCJSExportsFromESM,
|
|
} = 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, kEvaluationPhase } = moduleWrap;
|
|
|
|
const { getSourceSync } = require('internal/modules/esm/load');
|
|
|
|
const { parse: cjsParse } = internalBinding('cjs_lexer');
|
|
|
|
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.
|
|
* @returns {string|URL}
|
|
*/
|
|
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, translateContext, parentURL) {
|
|
let { source } = translateContext;
|
|
const isMain = (parentURL === undefined);
|
|
assertBufferSource(source, true, 'load');
|
|
source = stringify(source);
|
|
debug(`Translating StandardModule ${url}`, translateContext);
|
|
const {
|
|
compileSourceTextModule, SourceTextModuleTypes: { kUser },
|
|
} = require('internal/modules/esm/utils');
|
|
const context = isMain ? { isMain } : undefined;
|
|
const module = compileSourceTextModule(url, source, kUser, context);
|
|
return module;
|
|
});
|
|
|
|
const { requestTypes: { kRequireInImportedCJS } } = require('internal/modules/esm/utils');
|
|
const kShouldSkipModuleHooks = { __proto__: null, shouldSkipModuleHooks: true };
|
|
const kShouldNotSkipModuleHooks = { __proto__: null, shouldSkipModuleHooks: false };
|
|
|
|
/**
|
|
* This may be eventually removed when module.register() reaches end-of-life.
|
|
*
|
|
* 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
|
|
* @param {TranslateContext} translateContext Context for the translator
|
|
*/
|
|
function loadCJSModuleWithSpecialRequire(module, source, url, filename, isMain, translateContext) {
|
|
// Use the full URL as the V8 resource name so that any search params
|
|
// (e.g. ?node-test-mock) are preserved in coverage reports.
|
|
const compileResult = compileFunctionForCJSLoader(source, url, 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':
|
|
// If it gets here in the translators, the hooks must have already been invoked
|
|
// in the loader. Skip them in the synthetic module evaluation step.
|
|
return wrapModuleLoad(specifier, module, false, kShouldSkipModuleHooks);
|
|
default:
|
|
// fall through
|
|
}
|
|
specifier = `${pathToFileURL(path)}`;
|
|
}
|
|
|
|
// NOTE: This re-invented require() is only used on the loader-hook worker thread.
|
|
// On the main thread, the authentic require() is used instead (fixed by #60380).
|
|
const request = { specifier, attributes: importAttributes, phase: kEvaluationPhase, __proto__: null };
|
|
const job = cascadedLoader.getOrCreateModuleJob(url, request, kRequireInImportedCJS);
|
|
job.runSync();
|
|
let mod = cjsCache.get(job.url);
|
|
assert(job.module, `Imported CJS module ${url} failed to load module ${job.url} using require() due to race condition`);
|
|
|
|
if (job.module.synthetic) {
|
|
assert(mod, `Imported CJS module ${url} failed to load module ${job.url} using require() due to missed cache`);
|
|
return mod.exports;
|
|
}
|
|
|
|
// The module being required is a source text module.
|
|
if (!mod) {
|
|
mod = cjsEmplaceModuleCacheEntry(job.url);
|
|
cjsCache.set(job.url, mod);
|
|
}
|
|
// The module has been cached by the re-invented require. Update the exports object
|
|
// from the namespace object and return the evaluated exports.
|
|
if (!mod.loaded) {
|
|
debug('populateCJSExportsFromESM from require(esm) in imported CJS', url, mod, job.module);
|
|
populateCJSExportsFromESM(mod, job.module, job.module.getNamespace());
|
|
mod.loaded = true;
|
|
}
|
|
return mod.exports;
|
|
};
|
|
setOwnProperty(requireFn, 'resolve', function resolve(specifier) {
|
|
if (!StringPrototypeStartsWith(specifier, 'node:')) {
|
|
const {
|
|
filename, url: resolvedURL,
|
|
} = resolveForCJSWithHooks(specifier, module, false, kShouldNotSkipModuleHooks);
|
|
if (specifier !== filename) {
|
|
specifier = resolvedURL ?? `${pathToFileURL(filename)}`;
|
|
}
|
|
}
|
|
|
|
const request = { specifier, __proto__: null, attributes: kEmptyObject };
|
|
// Skip sync hooks in resolveSync since resolveForCJSWithHooks already ran them above.
|
|
const { url: resolvedURL } = cascadedLoader.resolveSync(url, request, /* shouldSkipSyncHooks */ true);
|
|
return urlToFilename(resolvedURL);
|
|
});
|
|
setOwnProperty(requireFn, 'main', process.mainModule);
|
|
|
|
FunctionPrototypeCall(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 {import('./loader').TranslateContext} translateContext Context for the translator
|
|
* @param {string|undefined} parentURL URL of the module initiating the module loading for the first time.
|
|
* Undefined if it's the entry point.
|
|
* @returns {ModuleWrap} The ModuleWrap object for the CommonJS module.
|
|
*/
|
|
function createCJSModuleWrap(url, translateContext, parentURL) {
|
|
debug(`Translating CJSModule ${url}`, translateContext);
|
|
|
|
const { format: sourceFormat } = translateContext;
|
|
let { source } = translateContext;
|
|
const isMain = (parentURL === undefined);
|
|
const filename = urlToFilename(url);
|
|
try {
|
|
// In case the source was not provided by the `load` step, we need fetch it now.
|
|
source = stringify(source ?? getSourceSync(new URL(url)).source);
|
|
} catch {
|
|
// Continue regardless of error.
|
|
}
|
|
|
|
const { exportNames, module } = cjsPreparseModuleExports(filename, source, sourceFormat);
|
|
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) {
|
|
// For backward-compatibility, it's possible for async hooks to return a nullish value for
|
|
// CJS source associated with a `file:` URL - that usually means the source is not
|
|
// customized (is loaded by default load) or the hook author wants it to be reloaded
|
|
// through CJS routine. In this case, the source is obtained by calling the
|
|
// Module._load().
|
|
if (translateContext.translatorKey === 'commonjs-sync' ||
|
|
translateContext.isSourceLoadedSynchronously ||
|
|
translateContext.source == null) {
|
|
loadCJSModuleWithModuleLoad(module, source, url, filename, !!isMain, translateContext);
|
|
} else { // CommonJS with source customized by async hooks
|
|
// This may be eventually removed when module.register() reaches end-of-life.
|
|
loadCJSModuleWithSpecialRequire(module, source, url, filename, !!isMain, translateContext);
|
|
}
|
|
}
|
|
|
|
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 {string|undefined} parentURL - URL of the parent module, if any.
|
|
* @returns {ModuleWrap} The ModuleWrap object for the CommonJS module.
|
|
*/
|
|
function createCJSNoSourceModuleWrap(url, parentURL) {
|
|
debug(`Translating CJSModule without source ${url}`);
|
|
const isMain = (parentURL === undefined);
|
|
|
|
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) {
|
|
// If it gets here in the translators, the hooks must have already been invoked
|
|
// in the loader. Skip them in the synthetic module evaluation step.
|
|
wrapModuleLoad(filename, null, isMain, kShouldSkipModuleHooks);
|
|
}
|
|
|
|
/** @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, translateContext, parentURL) {
|
|
return createCJSModuleWrap(url, translateContext, parentURL);
|
|
});
|
|
|
|
// Handle CommonJS modules referenced by `require` calls using re-invented require.
|
|
// This path is only used by require() from imported CJS customized by the *async*
|
|
// loader hooks.
|
|
translators.set('require-commonjs', (url, translateContext, parentURL) => {
|
|
return createCJSModuleWrap(url, translateContext, parentURL);
|
|
});
|
|
|
|
// Handle TypeScript CommonJS modules referenced by `require` calls using re-invented require.
|
|
// This path is only used by require() from imported CJS customized by the *async*
|
|
// loader hooks.
|
|
translators.set('require-commonjs-typescript', (url, translateContext, parentURL) => {
|
|
translateContext.source = stripTypeScriptModuleTypes(stringify(translateContext.source), url);
|
|
return createCJSModuleWrap(url, translateContext, parentURL);
|
|
});
|
|
|
|
// This goes through Module._load to accommodate monkey-patchers.
|
|
/**
|
|
* Loads a CommonJS module through Module._load to accommodate monkey-patchers.
|
|
* If the module was resolved by synchronous hooks (i.e. not by the default resolver),
|
|
* passes the pre-resolved information and source to Module._load to avoid
|
|
* re-resolving and re-loading.
|
|
* @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
|
|
* @param {import('./loader').TranslateContext} translateContext Context for the translator
|
|
*/
|
|
function loadCJSModuleWithModuleLoad(module, source, url, filename, isMain, translateContext) {
|
|
assert(module === CJSModule._cache[filename]);
|
|
debug(`loadCJSModuleWithModuleLoad ${url}`);
|
|
let exports;
|
|
if (translateContext.isResolvedBySyncHooks) {
|
|
exports = wrapModuleLoad(filename, undefined, isMain, {
|
|
__proto__: null,
|
|
resolved: {
|
|
__proto__: null,
|
|
filename,
|
|
format: translateContext.format,
|
|
url,
|
|
},
|
|
shouldSkipModuleHooks: true,
|
|
source,
|
|
});
|
|
} else {
|
|
// If it gets here in the translators, the hooks must have already been invoked
|
|
// in the loader. Skip them in the synthetic module evaluation step.
|
|
exports = wrapModuleLoad(filename, undefined, isMain, kShouldSkipModuleHooks);
|
|
}
|
|
|
|
// Patched Module._load implementations may return exports without updating the
|
|
// ESM-created cache entry. Mirror the returned value into the translator-owned
|
|
// module so the synthetic module namespace observes the loaded exports.
|
|
if (!module.loaded) {
|
|
module.exports = exports;
|
|
module.loaded = true;
|
|
}
|
|
module[kModuleExport] = exports;
|
|
}
|
|
|
|
// 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, translateContext, parentURL) {
|
|
return createCJSModuleWrap(url, translateContext, parentURL);
|
|
});
|
|
|
|
/**
|
|
* Get or create an entry in the CJS module cache for the given filename.
|
|
* @param {string} filename CJS module filename
|
|
* @param {CJSModule} parent The parent CJS module
|
|
* @returns {CJSModule} the cached CJS module entry
|
|
*/
|
|
function cjsEmplaceModuleCacheEntry(filename, parent) {
|
|
// 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, parent);
|
|
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]
|
|
* @returns {{module: CJSModule, exportNames: string[]}}
|
|
*/
|
|
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}`);
|
|
const { 0: exportNames, 1: reexports } = cjsParse(source);
|
|
|
|
// 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, kShouldNotSkipModuleHooks));
|
|
} 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, translateContext) {
|
|
debug(`Translating BuiltinModule ${url}`, translateContext);
|
|
// 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, translateContext) {
|
|
let { source } = translateContext;
|
|
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', function(url, translateContext) {
|
|
const { source } = translateContext;
|
|
// WebAssembly global is not available during snapshot building, so we need to get it lazily.
|
|
const { WebAssembly } = globalThis;
|
|
assertBufferSource(source, false, 'load');
|
|
|
|
debug(`Translating WASMModule ${url}`, translateContext);
|
|
|
|
let compiled;
|
|
try {
|
|
compiled = new WebAssembly.Module(source, {
|
|
builtins: ['js-string'],
|
|
importedStringConstants: 'wasm:js/string-constants',
|
|
});
|
|
} 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);
|
|
}
|
|
// Prefix reservations per https://webassembly.github.io/esm-integration/js-api/index.html#parse-a-webassembly-module.
|
|
if (impt.module.startsWith('wasm-js:')) {
|
|
throw new WebAssembly.LinkError(`Invalid Wasm import "${impt.module}" in ${url}`);
|
|
}
|
|
if (impt.name.startsWith('wasm:') || impt.name.startsWith('wasm-js:')) {
|
|
throw new WebAssembly.LinkError(`Invalid Wasm import name "${impt.module}" in ${url}`);
|
|
}
|
|
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);
|
|
}
|
|
if (expt.name.startsWith('wasm:') || expt.name.startsWith('wasm-js:')) {
|
|
throw new WebAssembly.LinkError(`Invalid Wasm export name "${expt.name}" in ${url}`);
|
|
}
|
|
exportsList.add(expt.name);
|
|
}
|
|
|
|
const createDynamicModule = require('internal/modules/esm/create_dynamic_module');
|
|
|
|
const { module } = createDynamicModule([...importsList], [...exportsList], url, (reflect) => {
|
|
emitExperimentalWarning('Importing WebAssembly module instances');
|
|
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, translateContext, parentURL) {
|
|
emitExperimentalWarning('Importing addons');
|
|
|
|
const { source } = translateContext;
|
|
// 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}`, translateContext);
|
|
|
|
return createCJSNoSourceModuleWrap(url, parentURL);
|
|
});
|
|
|
|
// Strategy for loading a commonjs TypeScript module
|
|
translators.set('commonjs-typescript', function(url, translateContext, parentURL) {
|
|
const { source } = translateContext;
|
|
assertBufferSource(source, true, 'load');
|
|
debug(`Translating TypeScript ${url}`, translateContext);
|
|
translateContext.source = stripTypeScriptModuleTypes(stringify(source), url);
|
|
return FunctionPrototypeCall(translators.get('commonjs'), this, url, translateContext, parentURL);
|
|
});
|
|
|
|
// Strategy for loading an esm TypeScript module
|
|
translators.set('module-typescript', function(url, translateContext, parentURL) {
|
|
const { source } = translateContext;
|
|
assertBufferSource(source, true, 'load');
|
|
debug(`Translating TypeScript ${url}`, translateContext);
|
|
translateContext.source = stripTypeScriptModuleTypes(stringify(source), url);
|
|
return FunctionPrototypeCall(translators.get('module'), this, url, translateContext, parentURL);
|
|
});
|