'use strict'; const { FunctionPrototypeBind, ObjectSetPrototypeOf, SafeMap, } = primordials; const { ERR_INVALID_RETURN_PROPERTY, ERR_INVALID_RETURN_PROPERTY_VALUE, ERR_INVALID_RETURN_VALUE, ERR_MISSING_DYNAMIC_INSTANTIATE_HOOK, ERR_UNKNOWN_MODULE_FORMAT } = require('internal/errors').codes; const { URL, pathToFileURL } = require('internal/url'); const { validateString } = require('internal/validators'); const ModuleMap = require('internal/modules/esm/module_map'); const ModuleJob = require('internal/modules/esm/module_job'); const { defaultResolve } = require('internal/modules/esm/resolve'); const { defaultGetFormat } = require('internal/modules/esm/get_format'); const { defaultGetSource } = require( 'internal/modules/esm/get_source'); const { defaultTransformSource } = require( 'internal/modules/esm/transform_source'); const createDynamicModule = require( 'internal/modules/esm/create_dynamic_module'); const { translators } = require( 'internal/modules/esm/translators'); const { getOptionValue } = require('internal/options'); const debug = require('internal/util/debuglog').debuglog('esm'); /* A Loader instance is used as the main entry point for loading ES modules. * Currently, this is a singleton -- there is only one used for loading * the main module and everything in its dependency graph. */ class Loader { constructor() { // Methods which translate input code or other information // into es modules this.translators = translators; // Registry of loaded modules, akin to `require.cache` this.moduleMap = new ModuleMap(); // Map of already-loaded CJS modules to use this.cjsCache = new SafeMap(); // The resolver has the signature // (specifier : string, parentURL : string, defaultResolve) // -> Promise<{ url : string }> // where defaultResolve is ModuleRequest.resolve (having the same // signature itself). this._resolve = defaultResolve; // This hook is called after the module is resolved but before a translator // is chosen to load it; the format returned by this function is the name // of a translator. // If `.format` on the returned value is 'dynamic', .dynamicInstantiate // will be used as described below. this._getFormat = defaultGetFormat; // This hook is called just before the source code of an ES module file // is loaded. this._getSource = defaultGetSource; // This hook is called just after the source code of an ES module file // is loaded, but before anything is done with the string. this._transformSource = defaultTransformSource; // This hook is only called when getFormat is 'dynamic' and // has the signature // (url : string) -> Promise<{ exports: { ... }, execute: function }> // Where `exports` is an object whose property names define the exported // names of the generated module. `execute` is a function that receives // an object with the same keys as `exports`, whose values are get/set // functions for the actual exported values. this._dynamicInstantiate = undefined; // The index for assigning unique URLs to anonymous module evaluation this.evalIndex = 0; } async resolve(specifier, parentURL) { const isMain = parentURL === undefined; if (!isMain) validateString(parentURL, 'parentURL'); const resolveResponse = await this._resolve( specifier, { parentURL }, defaultResolve); if (typeof resolveResponse !== 'object') { throw new ERR_INVALID_RETURN_VALUE( 'object', 'loader resolve', resolveResponse); } const { url } = resolveResponse; if (typeof url !== 'string') { throw new ERR_INVALID_RETURN_PROPERTY_VALUE( 'string', 'loader resolve', 'url', url); } const getFormatResponse = await this._getFormat( url, {}, defaultGetFormat); if (typeof getFormatResponse !== 'object') { throw new ERR_INVALID_RETURN_VALUE( 'object', 'loader getFormat', getFormatResponse); } const { format } = getFormatResponse; if (typeof format !== 'string') { throw new ERR_INVALID_RETURN_PROPERTY_VALUE( 'string', 'loader getFormat', 'format', format); } if (format === 'builtin') { return { url: `node:${url}`, format }; } if (this._resolve !== defaultResolve) { try { new URL(url); } catch { throw new ERR_INVALID_RETURN_PROPERTY( 'url', 'loader resolve', 'url', url ); } } if (this._resolve === defaultResolve && format !== 'dynamic' && !url.startsWith('file:') && !url.startsWith('data:') ) { throw new ERR_INVALID_RETURN_PROPERTY( 'file: or data: url', 'loader resolve', 'url', url ); } return { url, format }; } async eval( source, url = pathToFileURL(`${process.cwd()}/[eval${++this.evalIndex}]`).href ) { const evalInstance = (url) => { const { ModuleWrap, callbackMap } = internalBinding('module_wrap'); const module = new ModuleWrap(url, undefined, source, 0, 0); callbackMap.set(module, { importModuleDynamically: (specifier, { url }) => { return this.import(specifier, url); } }); return module; }; const job = new ModuleJob(this, url, evalInstance, false, false); this.moduleMap.set(url, job); const { module, result } = await job.run(); return { namespace: module.getNamespace(), result }; } async import(specifier, parent) { const job = await this.getModuleJob(specifier, parent); const { module } = await job.run(); return module.getNamespace(); } hook({ resolve, dynamicInstantiate, getFormat, getSource, transformSource }) { // Use .bind() to avoid giving access to the Loader instance when called. if (resolve !== undefined) this._resolve = FunctionPrototypeBind(resolve, null); if (dynamicInstantiate !== undefined) { this._dynamicInstantiate = FunctionPrototypeBind(dynamicInstantiate, null); } if (getFormat !== undefined) { this._getFormat = FunctionPrototypeBind(getFormat, null); } if (getSource !== undefined) { this._getSource = FunctionPrototypeBind(getSource, null); } if (transformSource !== undefined) { this._transformSource = FunctionPrototypeBind(transformSource, null); } } async getModuleJob(specifier, parentURL) { const { url, format } = await this.resolve(specifier, parentURL); let job = this.moduleMap.get(url); // CommonJS will set functions for lazy job evaluation. if (typeof job === 'function') this.moduleMap.set(url, job = job()); if (job !== undefined) return job; let loaderInstance; if (format === 'dynamic') { if (typeof this._dynamicInstantiate !== 'function') throw new ERR_MISSING_DYNAMIC_INSTANTIATE_HOOK(); loaderInstance = async (url) => { debug(`Translating dynamic ${url}`); const { exports, execute } = await this._dynamicInstantiate(url); return createDynamicModule([], exports, url, (reflect) => { debug(`Loading dynamic ${url}`); execute(reflect.exports); }).module; }; } else { if (!translators.has(format)) throw new ERR_UNKNOWN_MODULE_FORMAT(format); loaderInstance = translators.get(format); } const inspectBrk = parentURL === undefined && format === 'module' && getOptionValue('--inspect-brk'); job = new ModuleJob(this, url, loaderInstance, parentURL === undefined, inspectBrk); this.moduleMap.set(url, job); return job; } } ObjectSetPrototypeOf(Loader.prototype, null); exports.Loader = Loader;