1'use strict'; 2 3// This is needed to avoid cycles in esm/resolve <-> cjs/loader 4require('internal/modules/cjs/loader'); 5 6const { 7 ArrayPrototypeJoin, 8 ArrayPrototypeMap, 9 ArrayPrototypeReduce, 10 FunctionPrototypeCall, 11 JSONStringify, 12 ObjectSetPrototypeOf, 13 RegExpPrototypeSymbolReplace, 14 SafeWeakMap, 15 encodeURIComponent, 16 hardenRegExp, 17} = primordials; 18 19const { 20 ERR_UNKNOWN_MODULE_FORMAT, 21} = require('internal/errors').codes; 22const { getOptionValue } = require('internal/options'); 23const { pathToFileURL, isURL } = require('internal/url'); 24const { emitExperimentalWarning } = require('internal/util'); 25const { 26 getDefaultConditions, 27} = require('internal/modules/esm/utils'); 28let defaultResolve, defaultLoad, importMetaInitializer; 29 30/** 31 * Lazy loads the module_map module and returns a new instance of ResolveCache. 32 * @returns {import('./module_map.js').ResolveCache')} 33 */ 34function newResolveCache() { 35 const { ResolveCache } = require('internal/modules/esm/module_map'); 36 return new ResolveCache(); 37} 38 39/** 40 * Generate a load cache (to store the final result of a load-chain for a particular module). 41 * @returns {import('./module_map.js').LoadCache')} 42 */ 43function newLoadCache() { 44 const { LoadCache } = require('internal/modules/esm/module_map'); 45 return new LoadCache(); 46} 47 48/** 49 * Lazy-load translators to avoid potentially unnecessary work at startup (ex if ESM is not used). 50 * @returns {import('./translators.js').Translators} 51 */ 52function getTranslators() { 53 const { translators } = require('internal/modules/esm/translators'); 54 return translators; 55} 56 57/** 58 * @type {HooksProxy} 59 * Multiple loader instances exist for various, specific reasons (see code comments at site). 60 * In order to maintain consistency, we use a single worker (sandbox), which must sit apart of an 61 * individual loader instance. 62 */ 63let hooksProxy; 64 65/** 66 * @typedef {Record<string, any>} ModuleExports 67 */ 68 69/** 70 * @typedef {'builtin'|'commonjs'|'json'|'module'|'wasm'} ModuleFormat 71 */ 72 73/** 74 * @typedef {ArrayBuffer|TypedArray|string} ModuleSource 75 */ 76 77let emittedSpecifierResolutionWarning = false; 78 79/** 80 * This class covers the base machinery of module loading. To add custom 81 * behavior you can pass a customizations object and this object will be 82 * used to do the loading/resolving/registration process. 83 */ 84class ModuleLoader { 85 /** 86 * The conditions for resolving packages if `--conditions` is not used. 87 */ 88 #defaultConditions = getDefaultConditions(); 89 90 /** 91 * Map of already-loaded CJS modules to use 92 */ 93 cjsCache = new SafeWeakMap(); 94 95 /** 96 * The index for assigning unique URLs to anonymous module evaluation 97 */ 98 evalIndex = 0; 99 100 /** 101 * Registry of resolved specifiers 102 */ 103 #resolveCache = newResolveCache(); 104 105 /** 106 * Registry of loaded modules, akin to `require.cache` 107 */ 108 loadCache = newLoadCache(); 109 110 /** 111 * Methods which translate input code or other information into ES modules 112 */ 113 translators = getTranslators(); 114 115 /** 116 * Truthy to allow the use of `import.meta.resolve`. This is needed 117 * currently because the `Hooks` class does not have `resolveSync` 118 * implemented and `import.meta.resolve` requires it. 119 */ 120 allowImportMetaResolve; 121 122 /** 123 * Customizations to pass requests to. 124 * 125 * Note that this value _MUST_ be set with `setCustomizations` 126 * because it needs to copy `customizations.allowImportMetaResolve` 127 * to this property and failure to do so will cause undefined 128 * behavior when invoking `import.meta.resolve`. 129 * @see {ModuleLoader.setCustomizations} 130 */ 131 #customizations; 132 133 constructor(customizations) { 134 if (getOptionValue('--experimental-network-imports')) { 135 emitExperimentalWarning('Network Imports'); 136 } 137 if ( 138 !emittedSpecifierResolutionWarning && 139 getOptionValue('--experimental-specifier-resolution') === 'node' 140 ) { 141 process.emitWarning( 142 'The Node.js specifier resolution flag is experimental. It could change or be removed at any time.', 143 'ExperimentalWarning', 144 ); 145 emittedSpecifierResolutionWarning = true; 146 } 147 this.setCustomizations(customizations); 148 } 149 150 /** 151 * Change the currently activate customizations for this module 152 * loader to be the provided `customizations`. 153 * 154 * If present, this class customizes its core functionality to the 155 * `customizations` object, including registration, loading, and resolving. 156 * There are some responsibilities that this class _always_ takes 157 * care of, like validating outputs, so that the customizations object 158 * does not have to do so. 159 * 160 * The customizations object has the shape: 161 * 162 * ```ts 163 * interface LoadResult { 164 * format: ModuleFormat; 165 * source: ModuleSource; 166 * } 167 * 168 * interface ResolveResult { 169 * format: string; 170 * url: URL['href']; 171 * } 172 * 173 * interface Customizations { 174 * allowImportMetaResolve: boolean; 175 * load(url: string, context: object): Promise<LoadResult> 176 * resolve( 177 * originalSpecifier: 178 * string, parentURL: string, 179 * importAttributes: Record<string, string> 180 * ): Promise<ResolveResult> 181 * resolveSync( 182 * originalSpecifier: 183 * string, parentURL: string, 184 * importAttributes: Record<string, string> 185 * ) ResolveResult; 186 * register(specifier: string, parentURL: string): any; 187 * forceLoadHooks(): void; 188 * } 189 * ``` 190 * 191 * Note that this class _also_ implements the `Customizations` 192 * interface, as does `CustomizedModuleLoader` and `Hooks`. 193 * 194 * Calling this function alters how modules are loaded and should be 195 * invoked with care. 196 * @param {object} customizations 197 */ 198 setCustomizations(customizations) { 199 this.#customizations = customizations; 200 if (customizations) { 201 this.allowImportMetaResolve = customizations.allowImportMetaResolve; 202 } else { 203 this.allowImportMetaResolve = true; 204 } 205 } 206 207 async eval( 208 source, 209 url = pathToFileURL(`${process.cwd()}/[eval${++this.evalIndex}]`).href, 210 ) { 211 const evalInstance = (url) => { 212 const { ModuleWrap } = internalBinding('module_wrap'); 213 const { registerModule } = require('internal/modules/esm/utils'); 214 const module = new ModuleWrap(url, undefined, source, 0, 0); 215 registerModule(module, { 216 __proto__: null, 217 initializeImportMeta: (meta, wrap) => this.importMetaInitialize(meta, { url }), 218 importModuleDynamically: (specifier, { url }, importAttributes) => { 219 return this.import(specifier, url, importAttributes); 220 }, 221 }); 222 223 return module; 224 }; 225 const ModuleJob = require('internal/modules/esm/module_job'); 226 const job = new ModuleJob( 227 this, url, undefined, evalInstance, false, false); 228 this.loadCache.set(url, undefined, job); 229 const { module } = await job.run(); 230 231 return { 232 namespace: module.getNamespace(), 233 }; 234 } 235 236 /** 237 * Get a (possibly still pending) module job from the cache, 238 * or create one and return its Promise. 239 * @param {string} specifier The string after `from` in an `import` statement, 240 * or the first parameter of an `import()` 241 * expression 242 * @param {string | undefined} parentURL The URL of the module importing this 243 * one, unless this is the Node.js entry 244 * point. 245 * @param {Record<string, string>} importAttributes Validations for the 246 * module import. 247 * @returns {Promise<ModuleJob>} The (possibly pending) module job 248 */ 249 async getModuleJob(specifier, parentURL, importAttributes) { 250 const resolveResult = await this.resolve(specifier, parentURL, importAttributes); 251 return this.getJobFromResolveResult(resolveResult, parentURL, importAttributes); 252 } 253 254 getJobFromResolveResult(resolveResult, parentURL, importAttributes) { 255 const { url, format } = resolveResult; 256 const resolvedImportAttributes = resolveResult.importAttributes ?? importAttributes; 257 let job = this.loadCache.get(url, resolvedImportAttributes.type); 258 259 // CommonJS will set functions for lazy job evaluation. 260 if (typeof job === 'function') { 261 this.loadCache.set(url, undefined, job = job()); 262 } 263 264 if (job === undefined) { 265 job = this.#createModuleJob(url, resolvedImportAttributes, parentURL, format); 266 } 267 268 return job; 269 } 270 271 /** 272 * Create and cache an object representing a loaded module. 273 * @param {string} url The absolute URL that was resolved for this module 274 * @param {Record<string, string>} importAttributes Validations for the 275 * module import. 276 * @param {string} [parentURL] The absolute URL of the module importing this 277 * one, unless this is the Node.js entry point 278 * @param {string} [format] The format hint possibly returned by the 279 * `resolve` hook 280 * @returns {Promise<ModuleJob>} The (possibly pending) module job 281 */ 282 #createModuleJob(url, importAttributes, parentURL, format) { 283 const moduleProvider = async (url, isMain) => { 284 const { 285 format: finalFormat, 286 responseURL, 287 source, 288 } = await this.load(url, { 289 format, 290 importAttributes, 291 }); 292 293 const translator = getTranslators().get(finalFormat); 294 295 if (!translator) { 296 throw new ERR_UNKNOWN_MODULE_FORMAT(finalFormat, responseURL); 297 } 298 299 return FunctionPrototypeCall(translator, this, responseURL, source, isMain); 300 }; 301 302 const inspectBrk = ( 303 parentURL === undefined && 304 getOptionValue('--inspect-brk') 305 ); 306 307 if (process.env.WATCH_REPORT_DEPENDENCIES && process.send) { 308 process.send({ 'watch:import': [url] }); 309 } 310 311 const ModuleJob = require('internal/modules/esm/module_job'); 312 const job = new ModuleJob( 313 this, 314 url, 315 importAttributes, 316 moduleProvider, 317 parentURL === undefined, 318 inspectBrk, 319 ); 320 321 this.loadCache.set(url, importAttributes.type, job); 322 323 return job; 324 } 325 326 /** 327 * This method is usually called indirectly as part of the loading processes. 328 * Use directly with caution. 329 * @param {string} specifier The first parameter of an `import()` expression. 330 * @param {string} parentURL Path of the parent importing the module. 331 * @param {Record<string, string>} importAttributes Validations for the 332 * module import. 333 * @returns {Promise<ModuleExports>} 334 */ 335 async import(specifier, parentURL, importAttributes) { 336 const moduleJob = await this.getModuleJob(specifier, parentURL, importAttributes); 337 const { module } = await moduleJob.run(); 338 return module.getNamespace(); 339 } 340 341 /** 342 * @see {@link CustomizedModuleLoader.register} 343 */ 344 register(specifier, parentURL, data, transferList) { 345 if (!this.#customizations) { 346 // `CustomizedModuleLoader` is defined at the bottom of this file and 347 // available well before this line is ever invoked. This is here in 348 // order to preserve the git diff instead of moving the class. 349 // eslint-disable-next-line no-use-before-define 350 this.setCustomizations(new CustomizedModuleLoader()); 351 } 352 return this.#customizations.register(`${specifier}`, `${parentURL}`, data, transferList); 353 } 354 355 /** 356 * Resolve the location of the module. 357 * @param {string} originalSpecifier The specified URL path of the module to 358 * be resolved. 359 * @param {string} [parentURL] The URL path of the module's parent. 360 * @param {ImportAttributes} importAttributes Attributes from the import 361 * statement or expression. 362 * @returns {{ format: string, url: URL['href'] }} 363 */ 364 resolve(originalSpecifier, parentURL, importAttributes) { 365 if (this.#customizations) { 366 return this.#customizations.resolve(originalSpecifier, parentURL, importAttributes); 367 } 368 const requestKey = this.#resolveCache.serializeKey(originalSpecifier, importAttributes); 369 const cachedResult = this.#resolveCache.get(requestKey, parentURL); 370 if (cachedResult != null) { 371 return cachedResult; 372 } 373 const result = this.defaultResolve(originalSpecifier, parentURL, importAttributes); 374 this.#resolveCache.set(requestKey, parentURL, result); 375 return result; 376 } 377 378 /** 379 * Just like `resolve` except synchronous. This is here specifically to support 380 * `import.meta.resolve` which must happen synchronously. 381 */ 382 resolveSync(originalSpecifier, parentURL, importAttributes) { 383 if (this.#customizations) { 384 return this.#customizations.resolveSync(originalSpecifier, parentURL, importAttributes); 385 } 386 return this.defaultResolve(originalSpecifier, parentURL, importAttributes); 387 } 388 389 /** 390 * Our `defaultResolve` is synchronous and can be used in both 391 * `resolve` and `resolveSync`. This function is here just to avoid 392 * repeating the same code block twice in those functions. 393 */ 394 defaultResolve(originalSpecifier, parentURL, importAttributes) { 395 defaultResolve ??= require('internal/modules/esm/resolve').defaultResolve; 396 397 const context = { 398 __proto__: null, 399 conditions: this.#defaultConditions, 400 importAttributes, 401 parentURL, 402 }; 403 404 return defaultResolve(originalSpecifier, context); 405 } 406 407 /** 408 * Provide source that is understood by one of Node's translators. 409 * @param {URL['href']} url The URL/path of the module to be loaded 410 * @param {object} [context] Metadata about the module 411 * @returns {Promise<{ format: ModuleFormat, source: ModuleSource }>} 412 */ 413 async load(url, context) { 414 defaultLoad ??= require('internal/modules/esm/load').defaultLoad; 415 const result = this.#customizations ? 416 await this.#customizations.load(url, context) : 417 await defaultLoad(url, context); 418 this.validateLoadResult(url, result?.format); 419 return result; 420 } 421 422 validateLoadResult(url, format) { 423 if (format == null) { 424 require('internal/modules/esm/load').throwUnknownModuleFormat(url, format); 425 } 426 } 427 428 importMetaInitialize(meta, context) { 429 if (this.#customizations) { 430 return this.#customizations.importMetaInitialize(meta, context, this); 431 } 432 importMetaInitializer ??= require('internal/modules/esm/initialize_import_meta').initializeImportMeta; 433 meta = importMetaInitializer(meta, context, this); 434 return meta; 435 } 436 437 /** 438 * No-op when no hooks have been supplied. 439 */ 440 forceLoadHooks() { 441 this.#customizations?.forceLoadHooks(); 442 } 443} 444ObjectSetPrototypeOf(ModuleLoader.prototype, null); 445 446class CustomizedModuleLoader { 447 448 allowImportMetaResolve = true; 449 450 /** 451 * Instantiate a module loader that uses user-provided custom loader hooks. 452 */ 453 constructor() { 454 getHooksProxy(); 455 } 456 457 /** 458 * Register some loader specifier. 459 * @param {string} originalSpecifier The specified URL path of the loader to 460 * be registered. 461 * @param {string} parentURL The parent URL from where the loader will be 462 * registered if using it package name as specifier 463 * @param {any} [data] Arbitrary data to be passed from the custom loader 464 * (user-land) to the worker. 465 * @param {any[]} [transferList] Objects in `data` that are changing ownership 466 * @returns {{ format: string, url: URL['href'] }} 467 */ 468 register(originalSpecifier, parentURL, data, transferList) { 469 return hooksProxy.makeSyncRequest('register', transferList, originalSpecifier, parentURL, data); 470 } 471 472 /** 473 * Resolve the location of the module. 474 * @param {string} originalSpecifier The specified URL path of the module to 475 * be resolved. 476 * @param {string} [parentURL] The URL path of the module's parent. 477 * @param {ImportAttributes} importAttributes Attributes from the import 478 * statement or expression. 479 * @returns {{ format: string, url: URL['href'] }} 480 */ 481 resolve(originalSpecifier, parentURL, importAttributes) { 482 return hooksProxy.makeAsyncRequest('resolve', undefined, originalSpecifier, parentURL, importAttributes); 483 } 484 485 resolveSync(originalSpecifier, parentURL, importAttributes) { 486 // This happens only as a result of `import.meta.resolve` calls, which must be sync per spec. 487 return hooksProxy.makeSyncRequest('resolve', undefined, originalSpecifier, parentURL, importAttributes); 488 } 489 490 /** 491 * Provide source that is understood by one of Node's translators. 492 * @param {URL['href']} url The URL/path of the module to be loaded 493 * @param {object} [context] Metadata about the module 494 * @returns {Promise<{ format: ModuleFormat, source: ModuleSource }>} 495 */ 496 load(url, context) { 497 return hooksProxy.makeAsyncRequest('load', undefined, url, context); 498 } 499 500 importMetaInitialize(meta, context, loader) { 501 hooksProxy.importMetaInitialize(meta, context, loader); 502 } 503 504 forceLoadHooks() { 505 hooksProxy.waitForWorker(); 506 } 507} 508 509let emittedLoaderFlagWarning = false; 510/** 511 * A loader instance is used as the main entry point for loading ES modules. Currently, this is a singleton; there is 512 * only one used for loading the main module and everything in its dependency graph, though separate instances of this 513 * class might be instantiated as part of bootstrap for other purposes. 514 * @param {boolean} useCustomLoadersIfPresent If the user has provided loaders via the --loader flag, use them. 515 * @returns {ModuleLoader} 516 */ 517function createModuleLoader(useCustomLoadersIfPresent = true) { 518 let customizations = null; 519 if (useCustomLoadersIfPresent && 520 // Don't spawn a new worker if we're already in a worker thread created by instantiating CustomizedModuleLoader; 521 // doing so would cause an infinite loop. 522 !require('internal/modules/esm/utils').isLoaderWorker()) { 523 const userLoaderPaths = getOptionValue('--experimental-loader'); 524 if (userLoaderPaths.length > 0) { 525 if (!emittedLoaderFlagWarning) { 526 const readableURIEncode = (string) => ArrayPrototypeReduce( 527 [ 528 [/'/g, '%27'], // We need to URL-encode the single quote as it's the delimiter for the --import flag. 529 [/%22/g, '"'], // We can decode the double quotes to improve readability. 530 [/%2F/ig, '/'], // We can decode the slashes to improve readability. 531 ], 532 (str, { 0: regex, 1: replacement }) => RegExpPrototypeSymbolReplace(hardenRegExp(regex), str, replacement), 533 encodeURIComponent(string)); 534 process.emitWarning( 535 '`--experimental-loader` may be removed in the future; instead use `register()`:\n' + 536 `--import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; ${ArrayPrototypeJoin( 537 ArrayPrototypeMap(userLoaderPaths, (loader) => `register(${readableURIEncode(JSONStringify(loader))}, pathToFileURL("./"))`), 538 '; ', 539 )};'`, 540 'ExperimentalWarning', 541 ); 542 emittedLoaderFlagWarning = true; 543 } 544 customizations = new CustomizedModuleLoader(); 545 } 546 } 547 548 return new ModuleLoader(customizations); 549} 550 551 552/** 553 * Get the HooksProxy instance. If it is not defined, then create a new one. 554 * @returns {HooksProxy} 555 */ 556function getHooksProxy() { 557 if (!hooksProxy) { 558 const { HooksProxy } = require('internal/modules/esm/hooks'); 559 hooksProxy = new HooksProxy(); 560 } 561 562 return hooksProxy; 563} 564 565/** 566 * Register a single loader programmatically. 567 * @param {string|import('url').URL} specifier 568 * @param {string|import('url').URL} [parentURL] Base to use when resolving `specifier`; optional if 569 * `specifier` is absolute. Same as `options.parentUrl`, just inline 570 * @param {object} [options] Additional options to apply, described below. 571 * @param {string|import('url').URL} [options.parentURL] Base to use when resolving `specifier` 572 * @param {any} [options.data] Arbitrary data passed to the loader's `initialize` hook 573 * @param {any[]} [options.transferList] Objects in `data` that are changing ownership 574 * @returns {void} We want to reserve the return value for potential future extension of the API. 575 * @example 576 * ```js 577 * register('./myLoader.js'); 578 * register('ts-node/esm', { parentURL: import.meta.url }); 579 * register('./myLoader.js', { parentURL: import.meta.url }); 580 * register('ts-node/esm', import.meta.url); 581 * register('./myLoader.js', import.meta.url); 582 * register(new URL('./myLoader.js', import.meta.url)); 583 * register('./myLoader.js', { 584 * parentURL: import.meta.url, 585 * data: { banana: 'tasty' }, 586 * }); 587 * register('./myLoader.js', { 588 * parentURL: import.meta.url, 589 * data: someArrayBuffer, 590 * transferList: [someArrayBuffer], 591 * }); 592 * ``` 593 */ 594function register(specifier, parentURL = undefined, options) { 595 const moduleLoader = require('internal/process/esm_loader').esmLoader; 596 if (parentURL != null && typeof parentURL === 'object' && !isURL(parentURL)) { 597 options = parentURL; 598 parentURL = options.parentURL; 599 } 600 moduleLoader.register( 601 specifier, 602 parentURL ?? 'data:', 603 options?.data, 604 options?.transferList, 605 ); 606} 607 608module.exports = { 609 createModuleLoader, 610 getHooksProxy, 611 register, 612}; 613