mirror of
https://github.com/zebrajr/node.git
synced 2025-12-06 12:20:27 +01:00
This relaxes the validation in sync hooks so that it accepts the quirky nullish source returned by the default step of the async loader when the module being loaded is CommonJS. When there are no customization hooks registered, a saner synchronous default load step is used to use a property instead of a reset nullish source to signify that the module should go through the CJS monkey patching routes and reduce excessive reloading from disk. PR-URL: https://github.com/nodejs/node/pull/59929 Fixes: https://github.com/nodejs/node/issues/59384 Fixes: https://github.com/nodejs/node/issues/57327 Refs: https://github.com/nodejs/node/issues/59666 Refs: https://github.com/dygabo/load_module_test Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com> Reviewed-By: Jacob Smith <jacob@frende.me>
663 lines
24 KiB
JavaScript
663 lines
24 KiB
JavaScript
'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,
|
|
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 } = 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.
|
|
* @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 } = 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)}`;
|
|
}
|
|
|
|
// FIXME(node:59666) Currently, the ESM loader re-invents require() here for imported CJS and this
|
|
// requires a separate cache to be populated as well as introducing several quirks. This is not ideal.
|
|
const job = cascadedLoader.getModuleJobForRequireInImportedCJS(specifier, url, importAttributes);
|
|
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 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 {{ format: ModuleFormat, source: ModuleSource }} 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.
|
|
* @param {typeof loadCJSModule} [loadCJS] - The function to load the CommonJS module.
|
|
* @returns {ModuleWrap} The ModuleWrap object for the CommonJS module.
|
|
*/
|
|
function createCJSModuleWrap(url, translateContext, parentURL, loadCJS = loadCJSModule) {
|
|
debug(`Translating CJSModule ${url}`, translateContext);
|
|
|
|
const { format: sourceFormat } = translateContext;
|
|
let { source } = translateContext;
|
|
const isMain = (parentURL === undefined);
|
|
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, 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) {
|
|
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 {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) {
|
|
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, translateContext, parentURL) {
|
|
initCJSParseSync();
|
|
|
|
return createCJSModuleWrap(url, translateContext, parentURL, loadCJSModuleWithModuleLoad);
|
|
});
|
|
|
|
// Handle CommonJS modules referenced by `require` calls.
|
|
// This translator function must be sync, as `require` is sync.
|
|
translators.set('require-commonjs', (url, translateContext, parentURL) => {
|
|
initCJSParseSync();
|
|
assert(cjsParse);
|
|
|
|
return createCJSModuleWrap(url, translateContext, parentURL);
|
|
});
|
|
|
|
// Handle CommonJS modules referenced by `require` calls.
|
|
// This translator function must be sync, as `require` is sync.
|
|
translators.set('require-commonjs-typescript', (url, translateContext, parentURL) => {
|
|
assert(cjsParse);
|
|
translateContext.source = stripTypeScriptModuleTypes(stringify(translateContext.source), url);
|
|
return createCJSModuleWrap(url, translateContext, parentURL);
|
|
});
|
|
|
|
// This goes through Module._load to accommodate monkey-patchers.
|
|
function loadCJSModuleWithModuleLoad(module, source, url, filename, isMain) {
|
|
assert(module === CJSModule._cache[filename]);
|
|
wrapModuleLoad(filename, undefined, isMain);
|
|
}
|
|
|
|
// 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) {
|
|
if (!cjsParse) {
|
|
initCJSParseSync();
|
|
}
|
|
|
|
// For backward-compatibility, it's possible 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
|
|
// monkey-patchable CJS loader.
|
|
// TODO(joyeecheung): just use wrapModuleLoad and let the CJS loader
|
|
// invoke the off-thread hooks. Use a special parent to avoid invoking in-thread
|
|
// hooks twice.
|
|
const shouldReloadByCJSLoader = (translateContext.shouldBeReloadedByCJSLoader || translateContext.source == null);
|
|
const cjsLoader = shouldReloadByCJSLoader ? loadCJSModuleWithModuleLoad : loadCJSModule;
|
|
|
|
try {
|
|
// We still need to read the FS to detect the exports.
|
|
translateContext.source ??= readFileSync(new URL(url), 'utf8');
|
|
} catch {
|
|
// Continue regardless of error.
|
|
}
|
|
return createCJSModuleWrap(url, translateContext, parentURL, cjsLoader);
|
|
});
|
|
|
|
/**
|
|
* 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}`);
|
|
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, 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;
|
|
assertBufferSource(source, false, 'load');
|
|
|
|
debug(`Translating WASMModule ${url}`, translateContext);
|
|
|
|
let compiled;
|
|
try {
|
|
compiled = new WebAssembly.Module(source, {
|
|
builtins: ['js-string'],
|
|
});
|
|
} 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);
|
|
});
|