12 files changed

+206
-129
lines changed
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ module.exports = {
5858
'test/es-module/test-esm-example-loader.js',
5959
'test/es-module/test-esm-type-flag.js',
6060
'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',
6163
],
6264
parserOptions: { sourceType: 'module' },
6365
},
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,12 @@ regarding which files are parsed as ECMAScript modules.
187187
If `--experimental-require-module` is enabled, and the ECMAScript module being
188188
loaded by `require()` meets the following requirements:
189189

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.
193196

194197
`require()` will load the requested module as an ES Module, and return
195198
the module name space object. In this case it is similar to dynamic
@@ -256,18 +259,27 @@ require(X) from module at path Y
256259
6. LOAD_NODE_MODULES(X, dirname(Y))
257260
7. THROW "not found"
258261
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+
259271
LOAD_AS_FILE(X)
260272
1. If X is a file, load X as its file extension format. STOP
261273
2. If X.js is a file,
262274
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)
264277
c. If the SCOPE/package.json contains "type" field,
265278
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)
267281
3. If X.json is a file, load X.json to a JavaScript Object. STOP
268282
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
271283
272284
LOAD_INDEX(X)
273285
1. If X/index.js is a file
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,6 @@ module.exports = {
106106
kModuleExportNames,
107107
kModuleCircularVisited,
108108
initializeCJS,
109-
entryPointSource: undefined, // Set below.
110109
Module,
111110
wrapSafe,
112111
kIsMainSymbol,
@@ -1333,9 +1332,18 @@ function loadESMFromCJS(mod, filename) {
13331332
const source = getMaybeCachedSource(mod, filename);
13341333
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
13351334
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+
}
13391347
}
13401348

13411349
/**
@@ -1344,8 +1352,10 @@ function loadESMFromCJS(mod, filename) {
13441352
* @param {string} content The content of the file being loaded
13451353
* @param {Module} cjsModuleInstance The CommonJS loader instance
13461354
* @param {object} codeCache The SEA code cache
1355+
* @param {'commonjs'|undefined} format Intended format of the module.
13471356
*/
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().
13491359
const hostDefinedOptionId = vm_dynamic_import_default_internal;
13501360
const importModuleDynamically = vm_dynamic_import_default_internal;
13511361
if (ed) {
@@ -1375,46 +1385,33 @@ function wrapSafe(filename, content, cjsModuleInstance, codeCache) {
13751385
};
13761386
}
13771387

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);
13871391

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+
}
13921398

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);
14071402
}
1403+
1404+
return result;
14081405
}
14091406

14101407
/**
14111408
* Run the file contents in the correct scope or sandbox. Expose the correct helper variables (`require`, `module`,
14121409
* `exports`) to the file. Returns exception, if any.
14131410
* @param {string} content The source code of the module
14141411
* @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.
14161413
*/
1417-
Module.._compile = function(content, filename, loadAsESM = false) {
1414+
Module.._compile = function(content, filename, format) {
14181415
let moduleURL;
14191416
let redirects;
14201417
const manifest = policy()?.manifest;
@@ -1424,17 +1421,24 @@ Module.._compile = function(content, filename, loadAsESM = false) {
14241421
manifest.assertIntegrity(moduleURL, content);
14251422
}
14261423

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+
14271433
// TODO(joyeecheung): when the module is the entry point, consider allowing TLA.
14281434
// Only modules being require()'d really need to avoid TLA.
1429-
if (loadAsESM) {
1435+
if (format === 'module') {
14301436
// Pass the source into the .mjs extension handler indirectly through the cache.
14311437
this[kModuleSource] = content;
14321438
loadESMFromCJS(this, filename);
14331439
return;
14341440
}
14351441

1436-
const { function: compiledWrapper } = wrapSafe(filename, content, this);
1437-
14381442
// TODO(joyeecheung): the detection below is unnecessarily complex. Using the
14391443
// kIsMainSymbol, or a kBreakOnStartSymbol that gets passed from
14401444
// higher level instead of doing hacky detection here.
@@ -1511,12 +1515,13 @@ Module._extensions['.js'] = function(module, filename) {
15111515
// If already analyzed the source, then it will be cached.
15121516
const content = getMaybeCachedSource(module, filename);
15131517

1518+
let format;
15141519
if (StringEndsWith(filename, '.js')) {
15151520
const pkg = packageJsonReader.readPackageScope(filename) || { __proto__: null };
15161521
// Function require shouldn't be used in ES modules.
15171522
if (pkg.data?.type === 'module') {
15181523
if (getOptionValue('--experimental-require-module')) {
1519-
module._compile(content, filename, true);
1524+
module._compile(content, filename, 'module');
15201525
return;
15211526
}
15221527

@@ -1550,10 +1555,14 @@ Module._extensions['.js'] = function(module, filename) {
15501555
}
15511556
}
15521557
throw err;
1558+
} else if (pkg.data?.type === 'commonjs') {
1559+
format = 'commonjs';
15531560
}
1561+
} else if (StringEndsWith(filename, '.cjs')) {
1562+
format = 'commonjs';
15541563
}
15551564

1556-
module._compile(content, filename, false);
1565+
module._compile(content, filename, format);
15571566
};
15581567

15591568
/**
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ const {
44
ArrayMap,
55
Boolean,
66
JSONParse,
7-
ObjectGetOf,
87
ObjectHasOwnProperty,
98
ObjectKeys,
109
ReflectApply,
@@ -15,7 +14,6 @@ const {
1514
StringReplaceAll,
1615
StringSlice,
1716
StringStartsWith,
18-
SyntaxError,
1917
globalThis: { WebAssembly },
2018
} = primordials;
2119

@@ -30,7 +28,6 @@ function lazyTypes() {
3028
}
3129

3230
const {
33-
containsModuleSyntax,
3431
compileFunctionForCJSLoader,
3532
} = internalBinding('contextify');
3633

@@ -62,7 +59,6 @@ const {
6259
const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache');
6360
const moduleWrap = internalBinding('module_wrap');
6461
const { ModuleWrap } = moduleWrap;
65-
const { emitWarningSync } = require('internal/process/warning');
6662

6763
// Lazy-loading to avoid circular dependencies.
6864
let getSourceSync;
@@ -107,7 +103,6 @@ function initCJSParseSync() {
107103

108104
const translators = new SafeMap();
109105
exports.translators = translators;
110-
exports.enrichCJSError = enrichCJSError;
111106

112107
let DECODER = null;
113108
/**
@@ -169,25 +164,6 @@ translators.set('module', function moduleStrategy(url, source, isMain) {
169164
return module;
170165
});
171166

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 && ObjectGetOf(err) === SyntaxError &&
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-
191167
/**
192168
* Loads a CommonJS module via the ESM Loader sync CommonJS translator.
193169
* This translator creates its own version of the `require` function passed into CommonJS modules.
@@ -197,15 +173,11 @@ function enrichCJSError(err, content, filename) {
197173
* @param {string} source - The source code of the module.
198174
* @param {string} url - The URL of the module.
199175
* @param {string} filename - The filename of the module.
176+
* @param {boolean} isMain - Whether the module is the entrypoint
200177
*/
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+
209181
// Cache the source map for the cjs module if present.
210182
if (compileResult.sourceMapURL) {
211183
maybeCacheSourceMap(url, source, null, false, undefined, compileResult.sourceMapURL);
@@ -283,7 +255,7 @@ function createCJSModuleWrap(url, source, isMain, loadCJS = loadCJSModule) {
283255
debug(`Loading CJSModule ${url}`);
284256

285257
if (!module.loaded) {
286-
loadCJS(module, source, url, filename);
258+
loadCJS(module, source, url, filename, !!isMain);
287259
}
288260

289261
let exports;
@@ -315,9 +287,10 @@ translators.set('commonjs-sync', function requireCommonJS(url, source, isMain) {
315287
initCJSParseSync();
316288
assert(!isMain); // This is only used by imported CJS modules.
317289

318-
return createCJSModuleWrap(url, source, isMain, (module, source, url, filename) => {
290+
return createCJSModuleWrap(url, source, isMain, (module, source, url, filename, isMain) => {
319291
assert(module === CJSModule._cache[filename]);
320-
CJSModule._load(filename);
292+
assert(!isMain);
293+
CJSModule._load(filename, null, isMain);
321294
});
322295
});
323296

@@ -340,14 +313,9 @@ translators.set('commonjs', async function commonjsStrategy(url, source,
340313
// For backward-compatibility, it's possible to return a nullish value for
341314
// CJS source associated with a file: URL. In this case, the source is
342315
// obtained by calling the monkey-able 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);
351319
} : loadCJSModule;
352320

353321
try {
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
'use strict';
22

33
const {
4-
ObjectGetOf,
54
StringEndsWith,
6-
SyntaxError,
75
} = primordials;
86

97
const { getOptionValue } = require('internal/options');
@@ -160,35 +158,11 @@ function executeUserEntryPoint(main = process.argv[1]) {
160158
let mainURL;
161159
// Unless we know we should use the ESM loader to handle the entry point per the checks in `shouldUseESMLoader`, first
162160
// try to run the entry point via the CommonJS loader; and if that fails under certain conditions, retry as ESM.
163-
let retryAsESM = false;
164161
if (!useESMLoader) {
165162
const cjsLoader = require('internal/modules/cjs/loader');
166163
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-able CJS module loader.
171-
Module._load(main, null, true);
172-
} catch (error) {
173-
if (error != null && ObjectGetOf(error) === SyntaxError) {
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 {
192166
const mainPath = resolvedMain || main;
193167
if (mainURL === undefined) {
194168
mainURL = pathToFileURL(mainPath).href;

0 commit comments

Comments
 (0)