1'use strict'; 2 3const { 4 ArrayPrototypeForEach, 5 ArrayPrototypeMap, 6 Boolean, 7 JSONParse, 8 ObjectGetPrototypeOf, 9 ObjectPrototypeHasOwnProperty, 10 ObjectKeys, 11 SafeArrayIterator, 12 SafeMap, 13 SafeSet, 14 StringPrototypeIncludes, 15 StringPrototypeReplaceAll, 16 StringPrototypeSlice, 17 StringPrototypeStartsWith, 18 SyntaxErrorPrototype, 19 globalThis: { WebAssembly }, 20} = primordials; 21 22/** @type {import('internal/util/types')} */ 23let _TYPES = null; 24/** 25 * Lazily loads and returns the internal/util/types module. 26 */ 27function lazyTypes() { 28 if (_TYPES !== null) { return _TYPES; } 29 return _TYPES = require('internal/util/types'); 30} 31 32const { readFileSync } = require('fs'); 33const { extname, isAbsolute } = require('path'); 34const { 35 hasEsmSyntax, 36 loadBuiltinModule, 37 stripBOM, 38} = require('internal/modules/helpers'); 39const { 40 Module: CJSModule, 41 cjsParseCache, 42} = require('internal/modules/cjs/loader'); 43const { fileURLToPath, URL } = require('internal/url'); 44let debug = require('internal/util/debuglog').debuglog('esm', (fn) => { 45 debug = fn; 46}); 47const { emitExperimentalWarning } = require('internal/util'); 48const { 49 ERR_UNKNOWN_BUILTIN_MODULE, 50 ERR_INVALID_RETURN_PROPERTY_VALUE, 51} = require('internal/errors').codes; 52const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache'); 53const moduleWrap = internalBinding('module_wrap'); 54const { ModuleWrap } = moduleWrap; 55const asyncESM = require('internal/process/esm_loader'); 56const { emitWarningSync } = require('internal/process/warning'); 57 58/** @type {import('deps/cjs-module-lexer/lexer.js').parse} */ 59let cjsParse; 60/** 61 * Initializes the CommonJS module lexer parser. 62 * If WebAssembly is available, it uses the optimized version from the dist folder. 63 * Otherwise, it falls back to the JavaScript version from the lexer folder. 64 */ 65async function initCJSParse() { 66 if (typeof WebAssembly === 'undefined') { 67 cjsParse = require('internal/deps/cjs-module-lexer/lexer').parse; 68 } else { 69 const { parse, init } = 70 require('internal/deps/cjs-module-lexer/dist/lexer'); 71 try { 72 await init(); 73 cjsParse = parse; 74 } catch { 75 cjsParse = require('internal/deps/cjs-module-lexer/lexer').parse; 76 } 77 } 78} 79 80const translators = new SafeMap(); 81exports.translators = translators; 82exports.enrichCJSError = enrichCJSError; 83 84let DECODER = null; 85/** 86 * Asserts that the given body is a buffer source (either a string, array buffer, or typed array). 87 * Throws an error if the body is not a buffer source. 88 * @param {string | ArrayBufferView | ArrayBuffer} body - The body to check. 89 * @param {boolean} allowString - Whether or not to allow a string as a valid buffer source. 90 * @param {string} hookName - The name of the hook being called. 91 * @throws {ERR_INVALID_RETURN_PROPERTY_VALUE} If the body is not a buffer source. 92 */ 93function assertBufferSource(body, allowString, hookName) { 94 if (allowString && typeof body === 'string') { 95 return; 96 } 97 const { isArrayBufferView, isAnyArrayBuffer } = lazyTypes(); 98 if (isArrayBufferView(body) || isAnyArrayBuffer(body)) { 99 return; 100 } 101 throw new ERR_INVALID_RETURN_PROPERTY_VALUE( 102 `${allowString ? 'string, ' : ''}array buffer, or typed array`, 103 hookName, 104 'source', 105 body, 106 ); 107} 108 109/** 110 * Converts a buffer or buffer-like object to a string. 111 * @param {string | ArrayBuffer | ArrayBufferView} body - The buffer or buffer-like object to convert to a string. 112 * @returns {string} The resulting string. 113 */ 114function stringify(body) { 115 if (typeof body === 'string') { return body; } 116 assertBufferSource(body, false, 'transformSource'); 117 const { TextDecoder } = require('internal/encoding'); 118 DECODER = DECODER === null ? new TextDecoder() : DECODER; 119 return DECODER.decode(body); 120} 121 122/** 123 * Converts a URL to a file path if the URL protocol is 'file:'. 124 * @param {string} url - The URL to convert. 125 */ 126function errPath(url) { 127 const parsed = new URL(url); 128 if (parsed.protocol === 'file:') { 129 return fileURLToPath(parsed); 130 } 131 return url; 132} 133 134/** 135 * Dynamically imports a module using the ESM loader. 136 * @param {string} specifier - The module specifier to import. 137 * @param {object} options - An object containing options for the import. 138 * @param {string} options.url - The URL of the module requesting the import. 139 * @param {Record<string, string>} [attributes] - An object containing attributes for the import. 140 * @returns {Promise<import('internal/modules/esm/loader.js').ModuleExports>} The imported module. 141 */ 142async function importModuleDynamically(specifier, { url }, attributes) { 143 return asyncESM.esmLoader.import(specifier, url, attributes); 144} 145 146// Strategy for loading a standard JavaScript module. 147translators.set('module', async function moduleStrategy(url, source, isMain) { 148 assertBufferSource(source, true, 'load'); 149 source = stringify(source); 150 maybeCacheSourceMap(url, source); 151 debug(`Translating StandardModule ${url}`); 152 const module = new ModuleWrap(url, undefined, source, 0, 0); 153 const { registerModule } = require('internal/modules/esm/utils'); 154 registerModule(module, { 155 __proto__: null, 156 initializeImportMeta: (meta, wrap) => this.importMetaInitialize(meta, { url }), 157 importModuleDynamically, 158 }); 159 return module; 160}); 161 162/** 163 * Provide a more informative error for CommonJS imports. 164 * @param {Error | any} err 165 * @param {string} [content] Content of the file, if known. 166 * @param {string} [filename] Useful only if `content` is unknown. 167 */ 168function enrichCJSError(err, content, filename) { 169 if (err != null && ObjectGetPrototypeOf(err) === SyntaxErrorPrototype && 170 hasEsmSyntax(content || readFileSync(filename, 'utf-8'))) { 171 // Emit the warning synchronously because we are in the middle of handling 172 // a SyntaxError that will throw and likely terminate the process before an 173 // asynchronous warning would be emitted. 174 emitWarningSync( 175 'To load an ES module, set "type": "module" in the package.json or use ' + 176 'the .mjs extension.', 177 ); 178 } 179} 180 181// Strategy for loading a node-style CommonJS module 182const isWindows = process.platform === 'win32'; 183translators.set('commonjs', async function commonjsStrategy(url, source, 184 isMain) { 185 debug(`Translating CJSModule ${url}`); 186 187 const filename = fileURLToPath(new URL(url)); 188 189 if (!cjsParse) { await initCJSParse(); } 190 const { module, exportNames } = cjsPreparseModuleExports(filename); 191 const namesWithDefault = exportNames.has('default') ? 192 [...exportNames] : ['default', ...exportNames]; 193 194 return new ModuleWrap(url, undefined, namesWithDefault, function() { 195 debug(`Loading CJSModule ${url}`); 196 197 let exports; 198 if (asyncESM.esmLoader.cjsCache.has(module)) { 199 exports = asyncESM.esmLoader.cjsCache.get(module); 200 asyncESM.esmLoader.cjsCache.delete(module); 201 } else { 202 try { 203 exports = CJSModule._load(filename, undefined, isMain); 204 } catch (err) { 205 enrichCJSError(err, undefined, filename); 206 throw err; 207 } 208 } 209 210 for (const exportName of exportNames) { 211 if (!ObjectPrototypeHasOwnProperty(exports, exportName) || 212 exportName === 'default') { 213 continue; 214 } 215 // We might trigger a getter -> dont fail. 216 let value; 217 try { 218 value = exports[exportName]; 219 } catch { 220 // Continue regardless of error. 221 } 222 this.setExport(exportName, value); 223 } 224 this.setExport('default', exports); 225 }); 226}); 227 228/** 229 * Pre-parses a CommonJS module's exports and re-exports. 230 * @param {string} filename - The filename of the module. 231 */ 232function cjsPreparseModuleExports(filename) { 233 let module = CJSModule._cache[filename]; 234 if (module) { 235 const cached = cjsParseCache.get(module); 236 if (cached) { 237 return { module, exportNames: cached.exportNames }; 238 } 239 } 240 const loaded = Boolean(module); 241 if (!loaded) { 242 module = new CJSModule(filename); 243 module.filename = filename; 244 module.paths = CJSModule._nodeModulePaths(module.path); 245 CJSModule._cache[filename] = module; 246 } 247 248 let source; 249 try { 250 source = readFileSync(filename, 'utf8'); 251 } catch { 252 // Continue regardless of error. 253 } 254 255 let exports, reexports; 256 try { 257 ({ exports, reexports } = cjsParse(source || '')); 258 } catch { 259 exports = []; 260 reexports = []; 261 } 262 263 const exportNames = new SafeSet(new SafeArrayIterator(exports)); 264 265 // Set first for cycles. 266 cjsParseCache.set(module, { source, exportNames, loaded }); 267 268 if (reexports.length) { 269 module.filename = filename; 270 module.paths = CJSModule._nodeModulePaths(module.path); 271 } 272 ArrayPrototypeForEach(reexports, (reexport) => { 273 let resolved; 274 try { 275 resolved = CJSModule._resolveFilename(reexport, module); 276 } catch { 277 return; 278 } 279 const ext = extname(resolved); 280 if ((ext === '.js' || ext === '.cjs' || !CJSModule._extensions[ext]) && 281 isAbsolute(resolved)) { 282 const { exportNames: reexportNames } = cjsPreparseModuleExports(resolved); 283 for (const name of reexportNames) { 284 exportNames.add(name); 285 } 286 } 287 }); 288 289 return { module, exportNames }; 290} 291 292// Strategy for loading a node builtin CommonJS module that isn't 293// through normal resolution 294translators.set('builtin', async function builtinStrategy(url) { 295 debug(`Translating BuiltinModule ${url}`); 296 // Slice 'node:' scheme 297 const id = StringPrototypeSlice(url, 5); 298 const module = loadBuiltinModule(id, url); 299 if (!StringPrototypeStartsWith(url, 'node:') || !module) { 300 throw new ERR_UNKNOWN_BUILTIN_MODULE(url); 301 } 302 debug(`Loading BuiltinModule ${url}`); 303 return module.getESMFacade(); 304}); 305 306// Strategy for loading a JSON file 307translators.set('json', async function jsonStrategy(url, source) { 308 emitExperimentalWarning('Importing JSON modules'); 309 assertBufferSource(source, true, 'load'); 310 debug(`Loading JSONModule ${url}`); 311 const pathname = StringPrototypeStartsWith(url, 'file:') ? 312 fileURLToPath(url) : null; 313 const shouldCheckAndPopulateCJSModuleCache = 314 // We want to involve the CJS loader cache only for `file:` URL with no search query and no hash. 315 pathname && !StringPrototypeIncludes(url, '?') && !StringPrototypeIncludes(url, '#'); 316 let modulePath; 317 let module; 318 if (shouldCheckAndPopulateCJSModuleCache) { 319 modulePath = isWindows ? 320 StringPrototypeReplaceAll(pathname, '/', '\\') : pathname; 321 module = CJSModule._cache[modulePath]; 322 if (module && module.loaded) { 323 const exports = module.exports; 324 return new ModuleWrap(url, undefined, ['default'], function() { 325 this.setExport('default', exports); 326 }); 327 } 328 } 329 source = stringify(source); 330 if (shouldCheckAndPopulateCJSModuleCache) { 331 // A require call could have been called on the same file during loading and 332 // that resolves synchronously. To make sure we always return the identical 333 // export, we have to check again if the module already exists or not. 334 module = CJSModule._cache[modulePath]; 335 if (module && module.loaded) { 336 const exports = module.exports; 337 return new ModuleWrap(url, undefined, ['default'], function() { 338 this.setExport('default', exports); 339 }); 340 } 341 } 342 try { 343 const exports = JSONParse(stripBOM(source)); 344 module = { 345 exports, 346 loaded: true, 347 }; 348 } catch (err) { 349 // TODO (BridgeAR): We could add a NodeCore error that wraps the JSON 350 // parse error instead of just manipulating the original error message. 351 // That would allow to add further properties and maybe additional 352 // debugging information. 353 err.message = errPath(url) + ': ' + err.message; 354 throw err; 355 } 356 if (shouldCheckAndPopulateCJSModuleCache) { 357 CJSModule._cache[modulePath] = module; 358 } 359 return new ModuleWrap(url, undefined, ['default'], function() { 360 debug(`Parsing JSONModule ${url}`); 361 this.setExport('default', module.exports); 362 }); 363}); 364 365// Strategy for loading a wasm module 366translators.set('wasm', async function(url, source) { 367 emitExperimentalWarning('Importing WebAssembly modules'); 368 369 assertBufferSource(source, false, 'load'); 370 371 debug(`Translating WASMModule ${url}`); 372 373 let compiled; 374 try { 375 compiled = await WebAssembly.compile(source); 376 } catch (err) { 377 err.message = errPath(url) + ': ' + err.message; 378 throw err; 379 } 380 381 const imports = 382 ArrayPrototypeMap(WebAssembly.Module.imports(compiled), 383 ({ module }) => module); 384 const exports = 385 ArrayPrototypeMap(WebAssembly.Module.exports(compiled), 386 ({ name }) => name); 387 388 const createDynamicModule = require( 389 'internal/modules/esm/create_dynamic_module'); 390 return createDynamicModule(imports, exports, url, (reflect) => { 391 const { exports } = new WebAssembly.Instance(compiled, reflect.imports); 392 for (const expt of ObjectKeys(exports)) { 393 reflect.exports[expt].set(exports[expt]); 394 } 395 }).module; 396}); 397