GeezerVison is showing github.com — module: support ESM detection in the CJS loader · nodejs/node@a03faf2
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: .eslintrc.js +2Lines changed: 2 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -58,6 +58,8 @@ module.exports = { | 58 | 58 | 'test/es-module/test-esm-example-loader.js', | 59 | 59 | 'test/es-module/test-esm-type-flag.js', | 60 | 60 | 'test/es-module/test-esm-type-flag-alias.js', | | 61 | + 'test/es-module/test-require-module-detect-entry-point.js', | | 62 | + 'test/es-module/test-require-module-detect-entry-point-aou.js', | 61 | 63 | ], | 62 | 64 | parserOptions: { sourceType: 'module' }, | 63 | 65 | }, | |
Collapse file Copy file name to clipboardExpand all lines: doc/api/modules.md +19-7Lines changed: 19 additions & 7 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -187,9 +187,12 @@ regarding which files are parsed as ECMAScript modules. | 187 | 187 | If `--experimental-require-module` is enabled, and the ECMAScript module being | 188 | 188 | loaded by `require()` meets the following requirements: | 189 | 189 |
| 190 | | -* Explicitly marked as an ES module with a `"type": "module"` field in | 191 | | - the closest package.json or a `.mjs` extension. | 192 | | -* Fully synchronous (contains no top-level `await`). | | 190 | +* The module is fully synchronous (contains no top-level `await`); and | | 191 | +* One of these conditions are met: | | 192 | + 1. The file has a `.mjs` extension. | | 193 | + 2. The file has a `.js` extension, and the closest `package.json` contains `"type": "module"` | | 194 | + 3. The file has a `.js` extension, the closest `package.json` does not contain | | 195 | + `"type": "commonjs"`, and `--experimental-detect-module` is enabled. | 193 | 196 |
| 194 | 197 | `require()` will load the requested module as an ES Module, and return | 195 | 198 | the module name space object. In this case it is similar to dynamic | @@ -256,18 +259,27 @@ require(X) from module at path Y | 256 | 259 | 6. LOAD_NODE_MODULES(X, dirname(Y)) | 257 | 260 | 7. THROW "not found" | 258 | 261 |
| | 262 | +MAYBE_DETECT_AND_LOAD(X) | | 263 | +1. If X parses as a CommonJS module, load X as a CommonJS module. STOP. | | 264 | +2. Else, if `--experimental-require-module` and `--experimental-detect-module` are | | 265 | + enabled, and the source code of X can be parsed as ECMAScript module using | | 266 | + <a href="esm.md#resolver-algorithm-specification">DETECT_MODULE_SYNTAX defined in | | 267 | + the ESM resolver</a>, | | 268 | + a. Load X as an ECMAScript module. STOP. | | 269 | +3. THROW the SyntaxError from attempting to parse X as CommonJS in 1. STOP. | | 270 | +
| 259 | 271 | LOAD_AS_FILE(X) | 260 | 272 | 1. If X is a file, load X as its file extension format. STOP | 261 | 273 | 2. If X.js is a file, | 262 | 274 | a. Find the closest package scope SCOPE to X. | 263 | | - b. If no scope was found, load X.js as a CommonJS module. STOP. | | 275 | + b. If no scope was found | | 276 | + 1. MAYBE_DETECT_AND_LOAD(X.js) | 264 | 277 | c. If the SCOPE/package.json contains "type" field, | 265 | 278 | 1. If the "type" field is "module", load X.js as an ECMAScript module. STOP. | 266 | | - 2. Else, load X.js as an CommonJS module. STOP. | | 279 | + 2. If the "type" field is "commonjs", load X.js as an CommonJS module. STOP. | | 280 | + d. MAYBE_DETECT_AND_LOAD(X.js) | 267 | 281 | 3. If X.json is a file, load X.json to a JavaScript Object. STOP | 268 | 282 | 4. If X.node is a file, load X.node as binary addon. STOP | 269 | | -5. If X.mjs is a file, and `--experimental-require-module` is enabled, | 270 | | - load X.mjs as an ECMAScript module. STOP | 271 | 283 |
| 272 | 284 | LOAD_INDEX(X) | 273 | 285 | 1. If X/index.js is a file | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/cjs/loader.js +48-39Lines changed: 48 additions & 39 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -106,7 +106,6 @@ module.exports = { | 106 | 106 | kModuleExportNames, | 107 | 107 | kModuleCircularVisited, | 108 | 108 | initializeCJS, | 109 | | - entryPointSource: undefined, // Set below. | 110 | 109 | Module, | 111 | 110 | wrapSafe, | 112 | 111 | kIsMainSymbol, | @@ -1333,9 +1332,18 @@ function loadESMFromCJS(mod, filename) { | 1333 | 1332 | const source = getMaybeCachedSource(mod, filename); | 1334 | 1333 | const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader(); | 1335 | 1334 | const isMain = mod[kIsMainSymbol]; | 1336 | | - // TODO(joyeecheung): we may want to invent optional special handling for default exports here. | 1337 | | - // For now, it's good enough to be identical to what `import()` returns. | 1338 | | - mod.exports = cascadedLoader.importSyncForRequire(mod, filename, source, isMain, mod[kModuleParent]); | | 1335 | + if (isMain) { | | 1336 | + require('internal/modules/run_main').runEntryPointWithESMLoader((cascadedLoader) => { | | 1337 | + const mainURL = pathToFileURL(filename).href; | | 1338 | + cascadedLoader.import(mainURL, undefined, { __proto__: null }, true); | | 1339 | + }); | | 1340 | + // ESM won't be accessible via process.mainModule. | | 1341 | + setOwnProperty(process, 'mainModule', undefined); | | 1342 | + } else { | | 1343 | + // TODO(joyeecheung): we may want to invent optional special handling for default exports here. | | 1344 | + // For now, it's good enough to be identical to what `import()` returns. | | 1345 | + mod.exports = cascadedLoader.importSyncForRequire(mod, filename, source, isMain, mod[kModuleParent]); | | 1346 | + } | 1339 | 1347 | } | 1340 | 1348 |
| 1341 | 1349 | /** | @@ -1344,8 +1352,10 @@ function loadESMFromCJS(mod, filename) { | 1344 | 1352 | * @param {string} content The content of the file being loaded | 1345 | 1353 | * @param {Module} cjsModuleInstance The CommonJS loader instance | 1346 | 1354 | * @param {object} codeCache The SEA code cache | | 1355 | + * @param {'commonjs'|undefined} format Intended format of the module. | 1347 | 1356 | */ | 1348 | | -function wrapSafe(filename, content, cjsModuleInstance, codeCache) { | | 1357 | +function wrapSafe(filename, content, cjsModuleInstance, codeCache, format) { | | 1358 | + assert(format !== 'module'); // ESM should be handled in loadESMFromCJS(). | 1349 | 1359 | const hostDefinedOptionId = vm_dynamic_import_default_internal; | 1350 | 1360 | const importModuleDynamically = vm_dynamic_import_default_internal; | 1351 | 1361 | if (patched) { | @@ -1375,46 +1385,33 @@ function wrapSafe(filename, content, cjsModuleInstance, codeCache) { | 1375 | 1385 | }; | 1376 | 1386 | } | 1377 | 1387 |
| 1378 | | - try { | 1379 | | - const result = compileFunctionForCJSLoader(content, filename); | 1380 | | - | 1381 | | - // cachedDataRejected is only set for cache coming from SEA. | 1382 | | - if (codeCache && | 1383 | | - result.cachedDataRejected !== false && | 1384 | | - internalBinding('sea').isSea()) { | 1385 | | - process.emitWarning('Code cache data rejected.'); | 1386 | | - } | | 1388 | + const isMain = !!(cjsModuleInstance && cjsModuleInstance[kIsMainSymbol]); | | 1389 | + const shouldDetectModule = (format !== 'commonjs' && getOptionValue('--experimental-detect-module')); | | 1390 | + const result = compileFunctionForCJSLoader(content, filename, isMain, shouldDetectModule); | 1387 | 1391 |
| 1388 | | - // Cache the source map for the module if present. | 1389 | | - if (result.sourceMapURL) { | 1390 | | - maybeCacheSourceMap(filename, content, this, false, undefined, result.sourceMapURL); | 1391 | | - } | | 1392 | + // cachedDataRejected is only set for cache coming from SEA. | | 1393 | + if (codeCache && | | 1394 | + result.cachedDataRejected !== false && | | 1395 | + internalBinding('sea').isSea()) { | | 1396 | + process.emitWarning('Code cache data rejected.'); | | 1397 | + } | 1392 | 1398 |
| 1393 | | - return result; | 1394 | | - } catch (err) { | 1395 | | - if (process.mainModule === cjsModuleInstance) { | 1396 | | - if (getOptionValue('--experimental-detect-module')) { | 1397 | | - // For the main entry point, cache the source to potentially retry as ESM. | 1398 | | - module.exports.entryPointSource = content; | 1399 | | - } else { | 1400 | | - // We only enrich the error (print a warning) if we're sure we're going to for-sure throw it; so if we're | 1401 | | - // retrying as ESM, wait until we know whether we're going to retry before calling `enrichCJSError`. | 1402 | | - const { enrichCJSError } = require('internal/modules/esm/translators'); | 1403 | | - enrichCJSError(err, content, filename); | 1404 | | - } | 1405 | | - } | 1406 | | - throw err; | | 1399 | + // Cache the source map for the module if present. | | 1400 | + if (result.sourceMapURL) { | | 1401 | + maybeCacheSourceMap(filename, content, this, false, undefined, result.sourceMapURL); | 1407 | 1402 | } | | 1403 | + | | 1404 | + return result; | 1408 | 1405 | } | 1409 | 1406 |
| 1410 | 1407 | /** | 1411 | 1408 | * Run the file contents in the correct scope or sandbox. Expose the correct helper variables (`require`, `module`, | 1412 | 1409 | * `exports`) to the file. Returns exception, if any. | 1413 | 1410 | * @param {string} content The source code of the module | 1414 | 1411 | * @param {string} filename The file path of the module | 1415 | | - * @param {boolean} loadAsESM Whether it's known to be ESM via .mjs or "type" in package.json. | | 1412 | + * @param {'module'|'commonjs'|undefined} format Intended format of the module. | 1416 | 1413 | */ | 1417 | | -Module.prototype._compile = function(content, filename, loadAsESM = false) { | | 1414 | +Module.prototype._compile = function(content, filename, format) { | 1418 | 1415 | let moduleURL; | 1419 | 1416 | let redirects; | 1420 | 1417 | const manifest = policy()?.manifest; | @@ -1424,17 +1421,24 @@ Module.prototype._compile = function(content, filename, loadAsESM = false) { | 1424 | 1421 | manifest.assertIntegrity(moduleURL, content); | 1425 | 1422 | } | 1426 | 1423 |
| | 1424 | + let compiledWrapper; | | 1425 | + if (format !== 'module') { | | 1426 | + const result = wrapSafe(filename, content, this, undefined, format); | | 1427 | + compiledWrapper = result.function; | | 1428 | + if (result.canParseAsESM) { | | 1429 | + format = 'module'; | | 1430 | + } | | 1431 | + } | | 1432 | + | 1427 | 1433 | // TODO(joyeecheung): when the module is the entry point, consider allowing TLA. | 1428 | 1434 | // Only modules being require()'d really need to avoid TLA. | 1429 | | - if (loadAsESM) { | | 1435 | + if (format === 'module') { | 1430 | 1436 | // Pass the source into the .mjs extension handler indirectly through the cache. | 1431 | 1437 | this[kModuleSource] = content; | 1432 | 1438 | loadESMFromCJS(this, filename); | 1433 | 1439 | return; | 1434 | 1440 | } | 1435 | 1441 |
| 1436 | | - const { function: compiledWrapper } = wrapSafe(filename, content, this); | 1437 | | - | 1438 | 1442 | // TODO(joyeecheung): the detection below is unnecessarily complex. Using the | 1439 | 1443 | // kIsMainSymbol, or a kBreakOnStartSymbol that gets passed from | 1440 | 1444 | // higher level instead of doing hacky detection here. | @@ -1511,12 +1515,13 @@ Module._extensions['.js'] = function(module, filename) { | 1511 | 1515 | // If already analyzed the source, then it will be cached. | 1512 | 1516 | const content = getMaybeCachedSource(module, filename); | 1513 | 1517 |
| | 1518 | + let format; | 1514 | 1519 | if (StringPrototypeEndsWith(filename, '.js')) { | 1515 | 1520 | const pkg = packageJsonReader.readPackageScope(filename) || { __proto__: null }; | 1516 | 1521 | // Function require shouldn't be used in ES modules. | 1517 | 1522 | if (pkg.data?.type === 'module') { | 1518 | 1523 | if (getOptionValue('--experimental-require-module')) { | 1519 | | - module._compile(content, filename, true); | | 1524 | + module._compile(content, filename, 'module'); | 1520 | 1525 | return; | 1521 | 1526 | } | 1522 | 1527 |
| @@ -1550,10 +1555,14 @@ Module._extensions['.js'] = function(module, filename) { | 1550 | 1555 | } | 1551 | 1556 | } | 1552 | 1557 | throw err; | | 1558 | + } else if (pkg.data?.type === 'commonjs') { | | 1559 | + format = 'commonjs'; | 1553 | 1560 | } | | 1561 | + } else if (StringPrototypeEndsWith(filename, '.cjs')) { | | 1562 | + format = 'commonjs'; | 1554 | 1563 | } | 1555 | 1564 |
| 1556 | | - module._compile(content, filename, false); | | 1565 | + module._compile(content, filename, format); | 1557 | 1566 | }; | 1558 | 1567 |
| 1559 | 1568 | /** | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/esm/translators.js +11-43Lines changed: 11 additions & 43 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 4 | 4 | ArrayPrototypeMap, | 5 | 5 | Boolean, | 6 | 6 | JSONParse, | 7 | | - ObjectGetPrototypeOf, | 8 | 7 | ObjectPrototypeHasOwnProperty, | 9 | 8 | ObjectKeys, | 10 | 9 | ReflectApply, | @@ -15,7 +14,6 @@ const { | 15 | 14 | StringPrototypeReplaceAll, | 16 | 15 | StringPrototypeSlice, | 17 | 16 | StringPrototypeStartsWith, | 18 | | - SyntaxErrorPrototype, | 19 | 17 | globalThis: { WebAssembly }, | 20 | 18 | } = primordials; | 21 | 19 |
| @@ -30,7 +28,6 @@ function lazyTypes() { | 30 | 28 | } | 31 | 29 |
| 32 | 30 | const { | 33 | | - containsModuleSyntax, | 34 | 31 | compileFunctionForCJSLoader, | 35 | 32 | } = internalBinding('contextify'); | 36 | 33 |
| @@ -62,7 +59,6 @@ const { | 62 | 59 | const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache'); | 63 | 60 | const moduleWrap = internalBinding('module_wrap'); | 64 | 61 | const { ModuleWrap } = moduleWrap; | 65 | | -const { emitWarningSync } = require('internal/process/warning'); | 66 | 62 |
| 67 | 63 | // Lazy-loading to avoid circular dependencies. | 68 | 64 | let getSourceSync; | @@ -107,7 +103,6 @@ function initCJSParseSync() { | 107 | 103 |
| 108 | 104 | const translators = new SafeMap(); | 109 | 105 | exports.translators = translators; | 110 | | -exports.enrichCJSError = enrichCJSError; | 111 | 106 |
| 112 | 107 | let DECODER = null; | 113 | 108 | /** | @@ -169,25 +164,6 @@ translators.set('module', function moduleStrategy(url, source, isMain) { | 169 | 164 | return module; | 170 | 165 | }); | 171 | 166 |
| 172 | | -/** | 173 | | - * Provide a more informative error for CommonJS imports. | 174 | | - * @param {Error | any} err | 175 | | - * @param {string} [content] Content of the file, if known. | 176 | | - * @param {string} [filename] The filename of the erroring module. | 177 | | - */ | 178 | | -function enrichCJSError(err, content, filename) { | 179 | | - if (err != null && ObjectGetPrototypeOf(err) === SyntaxErrorPrototype && | 180 | | - containsModuleSyntax(content, filename)) { | 181 | | - // Emit the warning synchronously because we are in the middle of handling | 182 | | - // a SyntaxError that will throw and likely terminate the process before an | 183 | | - // asynchronous warning would be emitted. | 184 | | - emitWarningSync( | 185 | | - 'To load an ES module, set "type": "module" in the package.json or use ' + | 186 | | - 'the .mjs extension.', | 187 | | - ); | 188 | | - } | 189 | | -} | 190 | | - | 191 | 167 | /** | 192 | 168 | * Loads a CommonJS module via the ESM Loader sync CommonJS translator. | 193 | 169 | * This translator creates its own version of the `require` function passed into CommonJS modules. | @@ -197,15 +173,11 @@ function enrichCJSError(err, content, filename) { | 197 | 173 | * @param {string} source - The source code of the module. | 198 | 174 | * @param {string} url - The URL of the module. | 199 | 175 | * @param {string} filename - The filename of the module. | | 176 | + * @param {boolean} isMain - Whether the module is the entrypoint | 200 | 177 | */ | 201 | | -function loadCJSModule(module, source, url, filename) { | 202 | | - let compileResult; | 203 | | - try { | 204 | | - compileResult = compileFunctionForCJSLoader(source, filename); | 205 | | - } catch (err) { | 206 | | - enrichCJSError(err, source, filename); | 207 | | - throw err; | 208 | | - } | | 178 | +function loadCJSModule(module, source, url, filename, isMain) { | | 179 | + const compileResult = compileFunctionForCJSLoader(source, filename, isMain, false); | | 180 | + | 209 | 181 | // Cache the source map for the cjs module if present. | 210 | 182 | if (compileResult.sourceMapURL) { | 211 | 183 | maybeCacheSourceMap(url, source, null, false, undefined, compileResult.sourceMapURL); | @@ -283,7 +255,7 @@ function createCJSModuleWrap(url, source, isMain, loadCJS = loadCJSModule) { | 283 | 255 | debug(`Loading CJSModule ${url}`); | 284 | 256 |
| 285 | 257 | if (!module.loaded) { | 286 | | - loadCJS(module, source, url, filename); | | 258 | + loadCJS(module, source, url, filename, !!isMain); | 287 | 259 | } | 288 | 260 |
| 289 | 261 | let exports; | @@ -315,9 +287,10 @@ translators.set('commonjs-sync', function requireCommonJS(url, source, isMain) { | 315 | 287 | initCJSParseSync(); | 316 | 288 | assert(!isMain); // This is only used by imported CJS modules. | 317 | 289 |
| 318 | | - return createCJSModuleWrap(url, source, isMain, (module, source, url, filename) => { | | 290 | + return createCJSModuleWrap(url, source, isMain, (module, source, url, filename, isMain) => { | 319 | 291 | assert(module === CJSModule._cache[filename]); | 320 | | - CJSModule._load(filename); | | 292 | + assert(!isMain); | | 293 | + CJSModule._load(filename, null, isMain); | 321 | 294 | }); | 322 | 295 | }); | 323 | 296 |
| @@ -340,14 +313,9 @@ translators.set('commonjs', async function commonjsStrategy(url, source, | 340 | 313 | // For backward-compatibility, it's possible to return a nullish value for | 341 | 314 | // CJS source associated with a file: URL. In this case, the source is | 342 | 315 | // obtained by calling the monkey-patchable CJS loader. | 343 | | - const cjsLoader = source == null ? (module, source, url, filename) => { | 344 | | - try { | 345 | | - assert(module === CJSModule._cache[filename]); | 346 | | - CJSModule._load(filename); | 347 | | - } catch (err) { | 348 | | - enrichCJSError(err, source, filename); | 349 | | - throw err; | 350 | | - } | | 316 | + const cjsLoader = source == null ? (module, source, url, filename, isMain) => { | | 317 | + assert(module === CJSModule._cache[filename]); | | 318 | + CJSModule._load(filename, undefined, isMain); | 351 | 319 | } : loadCJSModule; | 352 | 320 |
| 353 | 321 | try { | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/run_main.js +2-28Lines changed: 2 additions & 28 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 1 | 1 | 'use strict'; | 2 | 2 |
| 3 | 3 | const { | 4 | | - ObjectGetPrototypeOf, | 5 | 4 | StringPrototypeEndsWith, | 6 | | - SyntaxErrorPrototype, | 7 | 5 | } = primordials; | 8 | 6 |
| 9 | 7 | const { getOptionValue } = require('internal/options'); | @@ -160,35 +158,11 @@ function executeUserEntryPoint(main = process.argv[1]) { | 160 | 158 | let mainURL; | 161 | 159 | // Unless we know we should use the ESM loader to handle the entry point per the checks in `shouldUseESMLoader`, first | 162 | 160 | // try to run the entry point via the CommonJS loader; and if that fails under certain conditions, retry as ESM. | 163 | | - let retryAsESM = false; | 164 | 161 | if (!useESMLoader) { | 165 | 162 | const cjsLoader = require('internal/modules/cjs/loader'); | 166 | 163 | const { Module } = cjsLoader; | 167 | | - if (getOptionValue('--experimental-detect-module')) { | 168 | | - // TODO(joyeecheung): handle this in the CJS loader. Don't try-catch here. | 169 | | - try { | 170 | | - // Module._load is the monkey-patchable CJS module loader. | 171 | | - Module._load(main, null, true); | 172 | | - } catch (error) { | 173 | | - if (error != null && ObjectGetPrototypeOf(error) === SyntaxErrorPrototype) { | 174 | | - const { shouldRetryAsESM } = internalBinding('contextify'); | 175 | | - const mainPath = resolvedMain || main; | 176 | | - mainURL = pathToFileURL(mainPath).href; | 177 | | - retryAsESM = shouldRetryAsESM(error.message, cjsLoader.entryPointSource, mainURL); | 178 | | - // In case the entry point is a large file, such as a bundle, | 179 | | - // ensure no further references can prevent it being garbage-collected. | 180 | | - cjsLoader.entryPointSource = undefined; | 181 | | - } | 182 | | - if (!retryAsESM) { | 183 | | - throw error; | 184 | | - } | 185 | | - } | 186 | | - } else { // `--experimental-detect-module` is not passed | 187 | | - Module._load(main, null, true); | 188 | | - } | 189 | | - } | 190 | | - | 191 | | - if (useESMLoader || retryAsESM) { | | 164 | + Module._load(main, null, true); | | 165 | + } else { | 192 | 166 | const mainPath = resolvedMain || main; | 193 | 167 | if (mainURL === undefined) { | 194 | 168 | mainURL = pathToFileURL(mainPath).href; | |
|