1'use strict'; 2 3// This is needed to avoid cycles in esm/resolve <-> cjs/loader 4require('internal/modules/cjs/loader'); 5 6const { 7 FunctionPrototypeBind, 8 ObjectSetPrototypeOf, 9 SafeWeakMap, 10} = primordials; 11 12const { 13 ERR_INVALID_ARG_VALUE, 14 ERR_INVALID_RETURN_PROPERTY, 15 ERR_INVALID_RETURN_PROPERTY_VALUE, 16 ERR_INVALID_RETURN_VALUE, 17 ERR_MISSING_DYNAMIC_INSTANTIATE_HOOK, 18 ERR_UNKNOWN_MODULE_FORMAT 19} = require('internal/errors').codes; 20const { URL, pathToFileURL } = require('internal/url'); 21const { validateString } = require('internal/validators'); 22const ModuleMap = require('internal/modules/esm/module_map'); 23const ModuleJob = require('internal/modules/esm/module_job'); 24 25const { 26 defaultResolve, 27 DEFAULT_CONDITIONS, 28} = require('internal/modules/esm/resolve'); 29const { defaultGetFormat } = require('internal/modules/esm/get_format'); 30const { defaultGetSource } = require( 31 'internal/modules/esm/get_source'); 32const { defaultTransformSource } = require( 33 'internal/modules/esm/transform_source'); 34const createDynamicModule = require( 35 'internal/modules/esm/create_dynamic_module'); 36const { translators } = require( 37 'internal/modules/esm/translators'); 38const { getOptionValue } = require('internal/options'); 39 40const debug = require('internal/util/debuglog').debuglog('esm'); 41 42/* A Loader instance is used as the main entry point for loading ES modules. 43 * Currently, this is a singleton -- there is only one used for loading 44 * the main module and everything in its dependency graph. */ 45class Loader { 46 constructor() { 47 // Methods which translate input code or other information 48 // into es modules 49 this.translators = translators; 50 51 // Registry of loaded modules, akin to `require.cache` 52 this.moduleMap = new ModuleMap(); 53 54 // Map of already-loaded CJS modules to use 55 this.cjsCache = new SafeWeakMap(); 56 57 // This hook is called before the first root module is imported. It's a 58 // function that returns a piece of code that runs as a sloppy-mode script. 59 // The script may evaluate to a function that can be called with a 60 // `getBuiltin` helper that can be used to retrieve builtins. 61 // If the hook returns `null` instead of a source string, it opts out of 62 // running any preload code. 63 // The preload code runs as soon as the hook module has finished evaluating. 64 this._getGlobalPreloadCode = null; 65 // The resolver has the signature 66 // (specifier : string, parentURL : string, defaultResolve) 67 // -> Promise<{ url : string }> 68 // where defaultResolve is ModuleRequest.resolve (having the same 69 // signature itself). 70 this._resolve = defaultResolve; 71 // This hook is called after the module is resolved but before a translator 72 // is chosen to load it; the format returned by this function is the name 73 // of a translator. 74 // If `.format` on the returned value is 'dynamic', .dynamicInstantiate 75 // will be used as described below. 76 this._getFormat = defaultGetFormat; 77 // This hook is called just before the source code of an ES module file 78 // is loaded. 79 this._getSource = defaultGetSource; 80 // This hook is called just after the source code of an ES module file 81 // is loaded, but before anything is done with the string. 82 this._transformSource = defaultTransformSource; 83 // This hook is only called when getFormat is 'dynamic' and 84 // has the signature 85 // (url : string) -> Promise<{ exports: { ... }, execute: function }> 86 // Where `exports` is an object whose property names define the exported 87 // names of the generated module. `execute` is a function that receives 88 // an object with the same keys as `exports`, whose values are get/set 89 // functions for the actual exported values. 90 this._dynamicInstantiate = undefined; 91 // The index for assigning unique URLs to anonymous module evaluation 92 this.evalIndex = 0; 93 } 94 95 async resolve(specifier, parentURL) { 96 const isMain = parentURL === undefined; 97 if (!isMain) 98 validateString(parentURL, 'parentURL'); 99 100 const resolveResponse = await this._resolve( 101 specifier, { parentURL, conditions: DEFAULT_CONDITIONS }, defaultResolve); 102 if (typeof resolveResponse !== 'object') { 103 throw new ERR_INVALID_RETURN_VALUE( 104 'object', 'loader resolve', resolveResponse); 105 } 106 107 const { url } = resolveResponse; 108 if (typeof url !== 'string') { 109 throw new ERR_INVALID_RETURN_PROPERTY_VALUE( 110 'string', 'loader resolve', 'url', url); 111 } 112 return url; 113 } 114 115 async getFormat(url) { 116 const getFormatResponse = await this._getFormat( 117 url, {}, defaultGetFormat); 118 if (typeof getFormatResponse !== 'object') { 119 throw new ERR_INVALID_RETURN_VALUE( 120 'object', 'loader getFormat', getFormatResponse); 121 } 122 123 const { format } = getFormatResponse; 124 if (typeof format !== 'string') { 125 throw new ERR_INVALID_RETURN_PROPERTY_VALUE( 126 'string', 'loader getFormat', 'format', format); 127 } 128 129 if (format === 'builtin') { 130 return format; 131 } 132 133 if (this._resolve !== defaultResolve) { 134 try { 135 new URL(url); 136 } catch { 137 throw new ERR_INVALID_RETURN_PROPERTY( 138 'url', 'loader resolve', 'url', url 139 ); 140 } 141 } 142 143 if (this._resolve === defaultResolve && 144 format !== 'dynamic' && 145 !url.startsWith('file:') && 146 !url.startsWith('data:') 147 ) { 148 throw new ERR_INVALID_RETURN_PROPERTY( 149 'file: or data: url', 'loader resolve', 'url', url 150 ); 151 } 152 153 return format; 154 } 155 156 async eval( 157 source, 158 url = pathToFileURL(`${process.cwd()}/[eval${++this.evalIndex}]`).href 159 ) { 160 const evalInstance = (url) => { 161 const { ModuleWrap, callbackMap } = internalBinding('module_wrap'); 162 const module = new ModuleWrap(url, undefined, source, 0, 0); 163 callbackMap.set(module, { 164 importModuleDynamically: (specifier, { url }) => { 165 return this.import(specifier, url); 166 } 167 }); 168 169 return module; 170 }; 171 const job = new ModuleJob(this, url, evalInstance, false, false); 172 this.moduleMap.set(url, job); 173 const { module, result } = await job.run(); 174 return { 175 namespace: module.getNamespace(), 176 result 177 }; 178 } 179 180 async import(specifier, parent) { 181 const job = await this.getModuleJob(specifier, parent); 182 const { module } = await job.run(); 183 return module.getNamespace(); 184 } 185 186 hook(hooks) { 187 const { 188 resolve, 189 dynamicInstantiate, 190 getFormat, 191 getSource, 192 transformSource, 193 getGlobalPreloadCode, 194 } = hooks; 195 196 // Use .bind() to avoid giving access to the Loader instance when called. 197 if (resolve !== undefined) 198 this._resolve = FunctionPrototypeBind(resolve, null); 199 if (dynamicInstantiate !== undefined) { 200 this._dynamicInstantiate = 201 FunctionPrototypeBind(dynamicInstantiate, null); 202 } 203 if (getFormat !== undefined) { 204 this._getFormat = FunctionPrototypeBind(getFormat, null); 205 } 206 if (getSource !== undefined) { 207 this._getSource = FunctionPrototypeBind(getSource, null); 208 } 209 if (transformSource !== undefined) { 210 this._transformSource = FunctionPrototypeBind(transformSource, null); 211 } 212 if (getGlobalPreloadCode !== undefined) { 213 this._getGlobalPreloadCode = 214 FunctionPrototypeBind(getGlobalPreloadCode, null); 215 } 216 } 217 218 runGlobalPreloadCode() { 219 if (!this._getGlobalPreloadCode) { 220 return; 221 } 222 const preloadCode = this._getGlobalPreloadCode(); 223 if (preloadCode === null) { 224 return; 225 } 226 227 if (typeof preloadCode !== 'string') { 228 throw new ERR_INVALID_RETURN_VALUE( 229 'string', 'loader getGlobalPreloadCode', preloadCode); 230 } 231 const { compileFunction } = require('vm'); 232 const preloadInit = compileFunction(preloadCode, ['getBuiltin'], { 233 filename: '<preload>', 234 }); 235 const { NativeModule } = require('internal/bootstrap/loaders'); 236 237 preloadInit.call(globalThis, (builtinName) => { 238 if (NativeModule.canBeRequiredByUsers(builtinName)) { 239 return require(builtinName); 240 } 241 throw new ERR_INVALID_ARG_VALUE('builtinName', builtinName); 242 }); 243 } 244 245 async getModuleJob(specifier, parentURL) { 246 const url = await this.resolve(specifier, parentURL); 247 const format = await this.getFormat(url); 248 let job = this.moduleMap.get(url); 249 // CommonJS will set functions for lazy job evaluation. 250 if (typeof job === 'function') 251 this.moduleMap.set(url, job = job()); 252 if (job !== undefined) 253 return job; 254 255 let loaderInstance; 256 if (format === 'dynamic') { 257 if (typeof this._dynamicInstantiate !== 'function') 258 throw new ERR_MISSING_DYNAMIC_INSTANTIATE_HOOK(); 259 260 loaderInstance = async (url) => { 261 debug(`Translating dynamic ${url}`); 262 const { exports, execute } = await this._dynamicInstantiate(url); 263 return createDynamicModule([], exports, url, (reflect) => { 264 debug(`Loading dynamic ${url}`); 265 execute(reflect.exports); 266 }).module; 267 }; 268 } else { 269 if (!translators.has(format)) 270 throw new ERR_UNKNOWN_MODULE_FORMAT(format); 271 272 loaderInstance = translators.get(format); 273 } 274 275 const inspectBrk = parentURL === undefined && 276 format === 'module' && getOptionValue('--inspect-brk'); 277 job = new ModuleJob(this, url, loaderInstance, parentURL === undefined, 278 inspectBrk); 279 this.moduleMap.set(url, job); 280 return job; 281 } 282} 283 284ObjectSetPrototypeOf(Loader.prototype, null); 285 286exports.Loader = Loader; 287