Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 612b870

Browse files
committedMar 5, 2024
module: support require()ing synchronous ESM graphs
This patch adds `require()` support for synchronous ESM graphs under the flag --experimental-require-module. This is based on the the following design aspect of ESM: - The resolution can be synchronous (up to the host) - The evaluation of a synchronous graph (without top-level await) is also synchronous, and, by the time the module graph is instantiated (before evaluation starts), this is is already known. When the module being require()ed has .mjs extension or there are other explicit indicators that it's an ES module, we load it as an ES module. If the graph is synchronous, we return the module namespace as the exports. If the graph contains top-level await, we throw an error before evaluating the module. If an additional flag --print-pending-tla is passed, we proceeds to evaluation but do not run the microtasks, only to find out where the TLA is and print their location to help users fix them. If there are not explicit indicators whether a .js file is CJS or ESM, we parse it as CJS first. If the parse error indicates that it contains ESM syntax, we parse it again as ESM. If the second parsing succeeds, we continue to treat it as ESM.
1 parent ff4fb7e commit 612b870

File tree

15 files changed

+431
-134
lines changed

15 files changed

+431
-134
lines changed
 

‎.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ doc/changelogs/CHANGELOG_v1*.md
1313
!doc/changelogs/CHANGELOG_v18.md
1414
!doc/api_assets/*.js
1515
!.eslintrc.js
16+
test/es-module/test-require-module-entry-point.js

‎doc/api/cli.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -871,6 +871,22 @@ added: v11.8.0
871871

872872
Use the specified file as a security policy.
873873

874+
### `--experimental-require-module`
875+
876+
<!-- YAML
877+
added: REPLACEME
878+
-->
879+
880+
> Stability: 1.1 - Active Developement
881+
882+
Supports loading a synchronous ES module graph in `require()`. If the module
883+
graph is not synchronous (contains top-level await), it throws an error.
884+
885+
By default, a `.js` file will be parsed as a CommonJS module first. If it
886+
contains ES module syntax, Node.js will try to parse and evaluate the module
887+
again as an ES module. If it turns out to be synchronous and can be evaluated
888+
successfully, the module namespace object will be returned by `require()`.
889+
874890
### `--experimental-sea-config`
875891

876892
<!-- YAML
@@ -2523,6 +2539,7 @@ Node.js options that are allowed are:
25232539
* `--experimental-network-imports`
25242540
* `--experimental-permission`
25252541
* `--experimental-policy`
2542+
* `--experimental-require-module`
25262543
* `--experimental-shadow-realm`
25272544
* `--experimental-specifier-resolution`
25282545
* `--experimental-top-level-await`

‎lib/internal/modules/cjs/loader.js

Lines changed: 66 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ const {
6060
StringPrototypeSlice,
6161
StringPrototypeSplit,
6262
StringPrototypeStartsWith,
63+
Symbol,
6364
} = primordials;
6465

6566
// Map used to store CJS parsing data.
@@ -107,7 +108,6 @@ const { safeGetenv } = internalBinding('credentials');
107108
const {
108109
privateSymbols: {
109110
require_private_symbol,
110-
host_defined_option_symbol,
111111
},
112112
} = internalBinding('util');
113113
const {
@@ -161,6 +161,8 @@ let requireDepth = 0;
161161
let isPreloading = false;
162162
let statCache = null;
163163

164+
const is_main_symbol = Symbol('is-main-module');
165+
164166
/**
165167
* Our internal implementation of `require`.
166168
* @param {Module} module Parent module of what is being required
@@ -271,6 +273,7 @@ function Module(id = '', parent) {
271273
setOwnProperty(this.__proto__, 'require', makeRequireFunction(this, redirects));
272274
}
273275
this[require_private_symbol] = internalRequire;
276+
this[is_main_symbol] = false; // Set to true by the entry point handler.
274277
}
275278

276279
/** @type {Record<string, Module>} */
@@ -396,6 +399,10 @@ function initializeCJS() {
396399
// TODO(joyeecheung): deprecate this in favor of a proper hook?
397400
Module.runMain =
398401
require('internal/modules/run_main').executeUserEntryPoint;
402+
403+
if (getOptionValue('--experimental-require-module')) {
404+
Module._extensions['.mjs'] = loadESMFromCJS;
405+
}
399406
}
400407

401408
// Given a module name, and a list of paths to test, returns the first
@@ -1010,6 +1017,7 @@ Module._load = function(request, parent, isMain) {
10101017
setOwnProperty(process, 'mainModule', module);
10111018
setOwnProperty(module.require, 'main', process.mainModule);
10121019
module.id = '.';
1020+
module[is_main_symbol] = true;
10131021
}
10141022

10151023
reportModuleToWatchMode(filename);
@@ -1270,46 +1278,58 @@ function wrapSafe(filename, content, cjsModuleInstance, codeCache) {
12701278
);
12711279

12721280
// Cache the source map for the module if present.
1273-
if (script.sourceMapURL) {
1274-
maybeCacheSourceMap(filename, content, this, false, undefined, script.sourceMapURL);
1281+
const { sourceMapURL } = script;
1282+
if (sourceMapURL) {
1283+
maybeCacheSourceMap(filename, content, this, false, undefined, sourceMapURL);
12751284
}
12761285

1277-
return runScriptInThisContext(script, true, false);
1286+
return {
1287+
__proto__: null,
1288+
function: runScriptInThisContext(script, true, false),
1289+
sourceMapURL,
1290+
retryAsESM: false,
1291+
};
12781292
}
12791293

1280-
try {
1281-
const result = compileFunctionForCJSLoader(content, filename);
1282-
result.function[host_defined_option_symbol] = hostDefinedOptionId;
1283-
1284-
// cachedDataRejected is only set for cache coming from SEA.
1285-
if (codeCache &&
1286-
result.cachedDataRejected !== false &&
1287-
internalBinding('sea').isSea()) {
1288-
process.emitWarning('Code cache data rejected.');
1289-
}
1294+
const result = compileFunctionForCJSLoader(content, filename);
12901295

1291-
// Cache the source map for the module if present.
1292-
if (result.sourceMapURL) {
1293-
maybeCacheSourceMap(filename, content, this, false, undefined, result.sourceMapURL);
1294-
}
1296+
// cachedDataRejected is only set for cache coming from SEA.
1297+
if (codeCache &&
1298+
result.cachedDataRejected !== false &&
1299+
internalBinding('sea').isSea()) {
1300+
process.emitWarning('Code cache data rejected.');
1301+
}
12951302

1296-
return result.function;
1297-
} catch (err) {
1298-
if (process.mainModule === cjsModuleInstance) {
1299-
const { enrichCJSError } = require('internal/modules/esm/translators');
1300-
enrichCJSError(err, content, filename);
1301-
}
1302-
throw err;
1303+
// Cache the source map for the module if present.
1304+
if (result.sourceMapURL) {
1305+
maybeCacheSourceMap(filename, content, this, false, undefined, result.sourceMapURL);
13031306
}
1307+
1308+
return result;
1309+
}
1310+
1311+
// Resolve and evaluate as ESM, synchronously.
1312+
function loadESMFromCJS(mod, filename) {
1313+
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
1314+
// Note that we are still using the CJS's path resolution here.
1315+
const parent = moduleParentCache.get(mod)?.filename;
1316+
const base = parent ? pathToFileURL(parent) : parent;
1317+
// console.log('loadESMFromCJS', mod, filename, base);
1318+
const specifier = mod[is_main_symbol] ? pathToFileURL(mod.filename) : mod.id;
1319+
const job = cascadedLoader.getModuleJobSync(specifier, base, kEmptyObject, 'from-cjs-error');
1320+
const { namespace } = job.runSync();
1321+
// TODO(joyeecheung): maybe we can do some special handling for default here. Maybe we don't.
1322+
mod.exports = namespace;
13041323
}
13051324

13061325
/**
13071326
* Run the file contents in the correct scope or sandbox. Expose the correct helper variables (`require`, `module`,
13081327
* `exports`) to the file. Returns exception, if any.
13091328
* @param {string} content The source code of the module
13101329
* @param {string} filename The file path of the module
1330+
* @param {boolean} loadAsESM Whether it's known to be ESM - i.e. suffix is .mjs.
13111331
*/
1312-
Module.prototype._compile = function(content, filename) {
1332+
Module.prototype._compile = function(content, filename, loadAsESM = false) {
13131333
let moduleURL;
13141334
let redirects;
13151335
const manifest = policy()?.manifest;
@@ -1319,8 +1339,21 @@ Module.prototype._compile = function(content, filename) {
13191339
manifest.assertIntegrity(moduleURL, content);
13201340
}
13211341

1322-
const compiledWrapper = wrapSafe(filename, content, this);
1342+
let compiledWrapper;
1343+
if (!loadAsESM) {
1344+
const result = wrapSafe(filename, content, this);
1345+
compiledWrapper = result.function;
1346+
loadAsESM = result.retryAsESM;
1347+
}
1348+
1349+
if (loadAsESM) {
1350+
loadESMFromCJS(this);
1351+
return;
1352+
}
13231353

1354+
// TODO(joyeecheung): the detection below is unnecessarily complex. Maybe just
1355+
// use the is_main_symbol, or a break_on_start_symbol that gets passed from
1356+
// higher level instead of doing hacky detecion here.
13241357
let inspectorWrapper = null;
13251358
if (getOptionValue('--inspect-brk') && process._eval == null) {
13261359
if (!resolvedArgv) {
@@ -1344,6 +1377,7 @@ Module.prototype._compile = function(content, filename) {
13441377
inspectorWrapper = internalBinding('inspector').callAndPauseOnStart;
13451378
}
13461379
}
1380+
13471381
const dirname = path.dirname(filename);
13481382
const require = makeRequireFunction(this, redirects);
13491383
let result;
@@ -1370,6 +1404,7 @@ Module.prototype._compile = function(content, filename) {
13701404
*/
13711405
Module._extensions['.js'] = function(module, filename) {
13721406
// If already analyzed the source, then it will be cached.
1407+
// TODO(joyeecheung): pass as buffer.
13731408
const cached = cjsParseCache.get(module);
13741409
let content;
13751410
if (cached?.source) {
@@ -1378,7 +1413,8 @@ Module._extensions['.js'] = function(module, filename) {
13781413
} else {
13791414
content = fs.readFileSync(filename, 'utf8');
13801415
}
1381-
if (StringPrototypeEndsWith(filename, '.js')) {
1416+
if (!getOptionValue('--experimental-require-module') &&
1417+
StringPrototypeEndsWith(filename, '.js')) {
13821418
const pkg = packageJsonReader.getNearestParentPackageJSON(filename);
13831419
// Function require shouldn't be used in ES modules.
13841420
if (pkg?.data.type === 'module') {
@@ -1414,7 +1450,8 @@ Module._extensions['.js'] = function(module, filename) {
14141450
throw err;
14151451
}
14161452
}
1417-
module._compile(content, filename);
1453+
1454+
module._compile(content, filename, false);
14181455
};
14191456

14201457
/**

‎lib/internal/modules/esm/loader.js

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const {
1515
hardenRegExp,
1616
} = primordials;
1717

18+
const assert = require('internal/assert');
1819
const {
1920
ERR_REQUIRE_ESM,
2021
ERR_UNKNOWN_MODULE_FORMAT,
@@ -228,12 +229,12 @@ class ModuleLoader {
228229
return this.getJobFromResolveResult(resolveResult, parentURL, importAttributes);
229230
}
230231

231-
getModuleJobSync(specifier, parentURL, importAttributes) {
232-
const resolveResult = this.resolveSync(specifier, parentURL, importAttributes);
233-
return this.getJobFromResolveResult(resolveResult, parentURL, importAttributes, true);
232+
getModuleJobSync(specifier, parentURL, importAttributes, requireESMHint) {
233+
const resolveResult = this.resolveSync(specifier, parentURL, importAttributes, requireESMHint);
234+
return this.getJobFromResolveResult(resolveResult, parentURL, importAttributes, true, requireESMHint);
234235
}
235236

236-
getJobFromResolveResult(resolveResult, parentURL, importAttributes, sync) {
237+
getJobFromResolveResult(resolveResult, parentURL, importAttributes, sync, requireESMHint) {
237238
const { url, format } = resolveResult;
238239
const resolvedImportAttributes = resolveResult.importAttributes ?? importAttributes;
239240
let job = this.loadCache.get(url, resolvedImportAttributes.type);
@@ -244,7 +245,7 @@ class ModuleLoader {
244245
}
245246

246247
if (job === undefined) {
247-
job = this.#createModuleJob(url, resolvedImportAttributes, parentURL, format, sync);
248+
job = this.#createModuleJob(url, resolvedImportAttributes, parentURL, format, sync, requireESMHint);
248249
}
249250

250251
return job;
@@ -261,7 +262,7 @@ class ModuleLoader {
261262
* `resolve` hook
262263
* @returns {Promise<ModuleJob>} The (possibly pending) module job
263264
*/
264-
#createModuleJob(url, importAttributes, parentURL, format, sync) {
265+
#createModuleJob(url, importAttributes, parentURL, format, sync, requireESMHint) {
265266
const callTranslator = ({ format: finalFormat, responseURL, source }, isMain) => {
266267
const translator = getTranslators().get(finalFormat);
267268

@@ -274,7 +275,7 @@ class ModuleLoader {
274275
const context = { format, importAttributes };
275276

276277
const moduleProvider = sync ?
277-
(url, isMain) => callTranslator(this.loadSync(url, context), isMain) :
278+
(url, isMain) => callTranslator(this.loadSync(url, context, requireESMHint), isMain) :
278279
async (url, isMain) => callTranslator(await this.load(url, context), isMain);
279280

280281
const inspectBrk = (
@@ -358,26 +359,30 @@ class ModuleLoader {
358359
* Just like `resolve` except synchronous. This is here specifically to support
359360
* `import.meta.resolve` which must happen synchronously.
360361
*/
361-
resolveSync(originalSpecifier, parentURL, importAttributes) {
362-
if (this.#customizations) {
362+
resolveSync(originalSpecifier, parentURL, importAttributes, requireESMHint) {
363+
// If this comes from the require(esm) fallback, don't apply loader hooks which are on
364+
// a separate thread. This is ignored by require(cjs) already anyway.
365+
// TODO(joyeecheung): add support in hooks for this?
366+
if (this.#customizations && !requireESMHint) {
363367
return this.#customizations.resolveSync(originalSpecifier, parentURL, importAttributes);
364368
}
365-
return this.defaultResolve(originalSpecifier, parentURL, importAttributes);
369+
return this.defaultResolve(originalSpecifier, parentURL, importAttributes, requireESMHint);
366370
}
367371

368372
/**
369373
* Our `defaultResolve` is synchronous and can be used in both
370374
* `resolve` and `resolveSync`. This function is here just to avoid
371375
* repeating the same code block twice in those functions.
372376
*/
373-
defaultResolve(originalSpecifier, parentURL, importAttributes) {
377+
defaultResolve(originalSpecifier, parentURL, importAttributes, requireESMHint) {
374378
defaultResolve ??= require('internal/modules/esm/resolve').defaultResolve;
375379

376380
const context = {
377381
__proto__: null,
378382
conditions: this.#defaultConditions,
379383
importAttributes,
380384
parentURL,
385+
requireESMHint,
381386
};
382387

383388
return defaultResolve(originalSpecifier, context);
@@ -398,14 +403,23 @@ class ModuleLoader {
398403
return result;
399404
}
400405

401-
loadSync(url, context) {
406+
loadSync(url, context, requireESMHint) {
402407
defaultLoadSync ??= require('internal/modules/esm/load').defaultLoadSync;
403-
404-
let result = this.#customizations ?
408+
const isRequireModuleAllowed = getOptionValue('--experimental-require-module');
409+
if (requireESMHint === 'from-cjs-error') {
410+
assert(isRequireModuleAllowed);
411+
context.format = 'module';
412+
}
413+
// If this comes from the require(esm) fallback, don't apply loader hooks which are on
414+
// a separate thread. This is ignored by require(cjs) already anyway.
415+
// TODO(joyeecheung): add support in hooks for this?
416+
let result = this.#customizations && !requireESMHint ?
405417
this.#customizations.loadSync(url, context) :
406418
defaultLoadSync(url, context);
419+
420+
// TODO(joyeecheung): we need a better way to detect the format and cache the result.
407421
let format = result?.format;
408-
if (format === 'module') {
422+
if (format === 'module' && !isRequireModuleAllowed) {
409423
throw new ERR_REQUIRE_ESM(url, true);
410424
}
411425
if (format === 'commonjs') {

‎lib/internal/modules/esm/module_job.js

Lines changed: 39 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -64,42 +64,51 @@ class ModuleJob {
6464
this.module = undefined;
6565
// Expose the promise to the ModuleWrap directly for linking below.
6666
// `this.module` is also filled in below.
67-
this.modulePromise = ReflectApply(moduleProvider, loader, [url, isMain]);
67+
const moduleWrapMaybePromise = ReflectApply(moduleProvider, loader, [url, isMain]);
6868

6969
if (sync) {
70-
this.module = this.modulePromise;
70+
this.module = moduleWrapMaybePromise;
71+
// TODO(joyeecheung): not needed?
7172
this.modulePromise = PromiseResolve(this.module);
73+
assert(this.module instanceof ModuleWrap);
74+
this.linked = this.module.linkSync((specifier, attributes) => {
75+
// TODO(joyeecheung): do this entirely in C++.
76+
const job = this.loader.getModuleJobSync(specifier, url, attributes, 'from-cjs-fallback');
77+
return job.module;
78+
});
7279
} else {
73-
this.modulePromise = PromiseResolve(this.modulePromise);
74-
}
80+
// TODO(joyeecheung): PromiseResolve not needed?
81+
this.modulePromise = PromiseResolve(moduleWrapMaybePromise);
82+
// Wait for the ModuleWrap instance being linked with all dependencies.
83+
const link = async () => {
84+
this.module = await this.modulePromise;
85+
assert(this.module instanceof ModuleWrap);
7586

76-
// Wait for the ModuleWrap instance being linked with all dependencies.
77-
const link = async () => {
78-
this.module = await this.modulePromise;
79-
assert(this.module instanceof ModuleWrap);
87+
// Explicitly keeping track of dependency jobs is needed in order
88+
// to flatten out the dependency graph below in `_instantiate()`,
89+
// so that circular dependencies can't cause a deadlock by two of
90+
// these `link` callbacks depending on each other.
91+
const dependencyJobs = [];
92+
const promises = this.module.link(async (specifier, attributes) => {
93+
const job = await this.loader.getModuleJob(specifier, url, attributes);
94+
ArrayPrototypePush(dependencyJobs, job);
95+
return job.modulePromise;
96+
});
8097

81-
// Explicitly keeping track of dependency jobs is needed in order
82-
// to flatten out the dependency graph below in `_instantiate()`,
83-
// so that circular dependencies can't cause a deadlock by two of
84-
// these `link` callbacks depending on each other.
85-
const dependencyJobs = [];
86-
const promises = this.module.link(async (specifier, attributes) => {
87-
const job = await this.loader.getModuleJob(specifier, url, attributes);
88-
ArrayPrototypePush(dependencyJobs, job);
89-
return job.modulePromise;
90-
});
98+
if (promises !== undefined) {
99+
await SafePromiseAllReturnVoid(promises);
100+
}
91101

92-
if (promises !== undefined) {
93-
await SafePromiseAllReturnVoid(promises);
94-
}
102+
return SafePromiseAllReturnArrayLike(dependencyJobs);
103+
};
95104

96-
return SafePromiseAllReturnArrayLike(dependencyJobs);
97-
};
98-
// Promise for the list of all dependencyJobs.
99-
this.linked = link();
100-
// This promise is awaited later anyway, so silence
101-
// 'unhandled rejection' warnings.
102-
PromisePrototypeThen(this.linked, undefined, noop);
105+
// Promise for the list of all dependencyJobs.
106+
this.linked = link();
107+
108+
// This promise is awaited later anyway, so silence
109+
// 'unhandled rejection' warnings.
110+
PromisePrototypeThen(this.linked, undefined, noop);
111+
}
103112

104113
// instantiated == deep dependency jobs wrappers are instantiated,
105114
// and module wrapper is instantiated.
@@ -204,13 +213,9 @@ class ModuleJob {
204213
return { __proto__: null, module: this.module };
205214
}
206215

207-
this.module.instantiate();
208-
this.instantiated = PromiseResolve();
209-
const timeout = -1;
210-
const breakOnSigint = false;
211216
setHasStartedUserESMExecution();
212-
this.module.evaluate(timeout, breakOnSigint);
213-
return { __proto__: null, module: this.module };
217+
const namespace = this.module.runSync();
218+
return { __proto__: null, module: this.module, namespace };
214219
}
215220

216221
async run() {

‎lib/internal/modules/esm/resolve.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ const encodedSepRegEx = /%2F|%5C/i;
233233
* @throws {ERR_UNSUPPORTED_DIR_IMPORT} - If the resolved pathname is a directory.
234234
* @throws {ERR_MODULE_NOT_FOUND} - If the resolved pathname is not a file.
235235
*/
236-
function finalizeResolution(resolved, base, preserveSymlinks) {
236+
function finalizeResolution(resolved, base, preserveSymlinks, requireESMHint) {
237237
if (RegExpPrototypeExec(encodedSepRegEx, resolved.pathname) !== null) {
238238
throw new ERR_INVALID_MODULE_SPECIFIER(
239239
resolved.pathname, 'must not include encoded "/" or "\\" characters',
@@ -253,6 +253,9 @@ function finalizeResolution(resolved, base, preserveSymlinks) {
253253
const stats = internalModuleStat(toNamespacedPath(StringPrototypeEndsWith(path, '/') ?
254254
StringPrototypeSlice(path, -1) : path));
255255

256+
// TODO(joyeecheung): figure out what we should do when require('./dir') resolves to
257+
// a directory.
258+
// if (!requireESMHint) {
256259
// Check for stats.isDirectory()
257260
if (stats === 1) {
258261
throw new ERR_UNSUPPORTED_DIR_IMPORT(path, fileURLToPath(base), String(resolved));
@@ -264,6 +267,7 @@ function finalizeResolution(resolved, base, preserveSymlinks) {
264267
throw new ERR_MODULE_NOT_FOUND(
265268
path || resolved.pathname, base && fileURLToPath(base), resolved);
266269
}
270+
// }
267271

268272
if (!preserveSymlinks) {
269273
const real = realpathSync(path, {
@@ -884,7 +888,7 @@ function shouldBeTreatedAsRelativeOrAbsolutePath(specifier) {
884888
* @param {Set<string>} conditions - An object containing environment conditions.
885889
* @param {boolean} preserveSymlinks - Whether to preserve symlinks in the resolved URL.
886890
*/
887-
function moduleResolve(specifier, base, conditions, preserveSymlinks) {
891+
function moduleResolve(specifier, base, conditions, preserveSymlinks, requireESMHint) {
888892
const protocol = typeof base === 'string' ?
889893
StringPrototypeSlice(base, 0, StringPrototypeIndexOf(base, ':') + 1) :
890894
base.protocol;
@@ -921,7 +925,7 @@ function moduleResolve(specifier, base, conditions, preserveSymlinks) {
921925
if (resolved.protocol !== 'file:') {
922926
return resolved;
923927
}
924-
return finalizeResolution(resolved, base, preserveSymlinks);
928+
return finalizeResolution(resolved, base, preserveSymlinks, requireESMHint);
925929
}
926930

927931
/**
@@ -1150,10 +1154,12 @@ function defaultResolve(specifier, context = {}) {
11501154
parentURL,
11511155
conditions,
11521156
isMain ? preserveSymlinksMain : preserveSymlinks,
1157+
context.requireESMHint,
11531158
);
11541159
} catch (error) {
11551160
// Try to give the user a hint of what would have been the
11561161
// resolved CommonJS module
1162+
// TODO(joyeecheung): not needed for --require-module.
11571163
if (error.code === 'ERR_MODULE_NOT_FOUND' ||
11581164
error.code === 'ERR_UNSUPPORTED_DIR_IMPORT') {
11591165
if (StringPrototypeStartsWith(specifier, 'file://')) {

‎lib/internal/modules/esm/translators.js

Lines changed: 14 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,6 @@ const {
1919
globalThis: { WebAssembly },
2020
} = primordials;
2121

22-
const {
23-
privateSymbols: {
24-
host_defined_option_symbol,
25-
},
26-
} = internalBinding('util');
27-
2822
/** @type {import('internal/util/types')} */
2923
let _TYPES = null;
3024
/**
@@ -66,9 +60,6 @@ const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache');
6660
const moduleWrap = internalBinding('module_wrap');
6761
const { ModuleWrap } = moduleWrap;
6862
const { emitWarningSync } = require('internal/process/warning');
69-
const {
70-
vm_dynamic_import_default_internal,
71-
} = internalBinding('symbols');
7263
// Lazy-loading to avoid circular dependencies.
7364
let getSourceSync;
7465
/**
@@ -84,21 +75,12 @@ function getSource(url) {
8475
let cjsParse;
8576
/**
8677
* Initializes the CommonJS module lexer parser.
87-
* If WebAssembly is available, it uses the optimized version from the dist folder.
88-
* Otherwise, it falls back to the JavaScript version from the lexer folder.
8978
*/
90-
async function initCJSParse() {
91-
if (typeof WebAssembly === 'undefined') {
79+
function initCJSParse() {
80+
// TODO(joyeecheung): implement a binding that directly compiles using
81+
// v8::WasmModuleObject::Compile() synchronously.
82+
if (cjsParse === undefined) {
9283
cjsParse = require('internal/deps/cjs-module-lexer/lexer').parse;
93-
} else {
94-
const { parse, init } =
95-
require('internal/deps/cjs-module-lexer/dist/lexer');
96-
try {
97-
await init();
98-
cjsParse = parse;
99-
} catch {
100-
cjsParse = require('internal/deps/cjs-module-lexer/lexer').parse;
101-
}
10284
}
10385
}
10486

@@ -170,7 +152,7 @@ async function importModuleDynamically(specifier, { url }, attributes) {
170152
}
171153

172154
// Strategy for loading a standard JavaScript module.
173-
translators.set('module', async function moduleStrategy(url, source, isMain) {
155+
translators.set('module', function moduleStrategy(url, source, isMain) {
174156
assertBufferSource(source, true, 'load');
175157
source = stringify(source);
176158
debug(`Translating StandardModule ${url}`);
@@ -218,14 +200,8 @@ function enrichCJSError(err, content, filename) {
218200
* @param {string} filename - The filename of the module.
219201
*/
220202
function loadCJSModule(module, source, url, filename) {
221-
let compileResult;
222-
try {
223-
compileResult = compileFunctionForCJSLoader(source, filename);
224-
compileResult.function[host_defined_option_symbol] = vm_dynamic_import_default_internal;
225-
} catch (err) {
226-
enrichCJSError(err, source, filename);
227-
throw err;
228-
}
203+
const compileResult = compileFunctionForCJSLoader(source, filename);
204+
229205
// Cache the source map for the cjs module if present.
230206
if (compileResult.sourceMapURL) {
231207
maybeCacheSourceMap(url, source, null, false, undefined, compileResult.sourceMapURL);
@@ -335,18 +311,17 @@ function createCJSModuleWrap(url, source, isMain, loadCJS = loadCJSModule) {
335311
// Handle CommonJS modules referenced by `require` calls.
336312
// This translator function must be sync, as `require` is sync.
337313
translators.set('require-commonjs', (url, source, isMain) => {
338-
assert(cjsParse);
314+
initCJSParse();
339315

316+
// TODO(joyeecheung): remove the unnecessary overhead of the ESM facade.
340317
return createCJSModuleWrap(url, source);
341318
});
342319

343320
// Handle CommonJS modules referenced by `import` statements or expressions,
344321
// or as the initial entry point when the ESM loader handles a CommonJS entry.
345-
translators.set('commonjs', async function commonjsStrategy(url, source,
346-
isMain) {
347-
if (!cjsParse) {
348-
await initCJSParse();
349-
}
322+
translators.set('commonjs', /* async */function commonjsStrategy(url, source,
323+
isMain) {
324+
initCJSParse();
350325

351326
// For backward-compatibility, it's possible to return a nullish value for
352327
// CJS source associated with a file: URL. In this case, the source is
@@ -531,6 +506,8 @@ translators.set('wasm', async function(url, source) {
531506

532507
let compiled;
533508
try {
509+
// TODO(joyeecheung): implement a binding that directly compiles using
510+
// v8::WasmModuleObject::Compile() synchronously.
534511
compiled = await WebAssembly.compile(source);
535512
} catch (err) {
536513
err.message = errPath(url) + ': ' + err.message;

‎lib/internal/util/embedding.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const { getCodePath, isSea } = internalBinding('sea');
1616

1717
function embedderRunCjs(contents) {
1818
const filename = process.execPath;
19-
const compiledWrapper = wrapSafe(
19+
const { function: compiledWrapper } = wrapSafe(
2020
isSea() ? getCodePath() : filename,
2121
contents);
2222

‎src/module_wrap.cc

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,72 @@ static Local<Object> createImportAttributesContainer(
283283
return attributes;
284284
}
285285

286+
void ModuleWrap::LinkSync(const FunctionCallbackInfo<Value>& args) {
287+
Realm* realm = Realm::GetCurrent(args);
288+
Isolate* isolate = args.GetIsolate();
289+
290+
CHECK_EQ(args.Length(), 1);
291+
CHECK(args[0]->IsFunction());
292+
293+
Local<Object> that = args.This();
294+
295+
ModuleWrap* obj;
296+
ASSIGN_OR_RETURN_UNWRAP(&obj, that);
297+
298+
if (obj->linked_) return;
299+
obj->linked_ = true;
300+
301+
Local<Function> resolver_arg = args[0].As<Function>();
302+
303+
Local<Context> mod_context = obj->context();
304+
Local<Module> module = obj->module_.Get(isolate);
305+
306+
Local<FixedArray> module_requests = module->GetModuleRequests();
307+
const int module_requests_length = module_requests->Length();
308+
309+
MaybeStackBuffer<Local<Value>, 16> modules(module_requests_length);
310+
// call the dependency resolve callbacks
311+
for (int i = 0; i < module_requests_length; i++) {
312+
Local<ModuleRequest> module_request =
313+
module_requests->Get(realm->context(), i).As<ModuleRequest>();
314+
Local<String> specifier = module_request->GetSpecifier();
315+
Utf8Value specifier_utf8(realm->isolate(), specifier);
316+
std::string specifier_std(*specifier_utf8, specifier_utf8.length());
317+
318+
Local<FixedArray> raw_attributes = module_request->GetImportAssertions();
319+
Local<Object> attributes =
320+
createImportAttributesContainer(realm, isolate, raw_attributes, 3);
321+
322+
Local<Value> argv[] = {
323+
specifier,
324+
attributes,
325+
};
326+
327+
MaybeLocal<Value> maybe_resolve_return_value =
328+
resolver_arg->Call(mod_context, that, arraysize(argv), argv);
329+
if (maybe_resolve_return_value.IsEmpty()) {
330+
return;
331+
}
332+
Local<Value> resolve_return_value =
333+
maybe_resolve_return_value.ToLocalChecked();
334+
// TODO(joyeecheung): the resolve cache should not hold on to the wraps
335+
// using a Global<Promise>, it's unnecessary and leaky. We should create a
336+
// map of ModuleWraps directly instead.
337+
Local<Promise::Resolver> resolver;
338+
if (!Promise::Resolver::New(mod_context).ToLocal(&resolver)) {
339+
return;
340+
}
341+
if (resolver->Resolve(mod_context, resolve_return_value).IsNothing()) {
342+
return;
343+
}
344+
obj->resolve_cache_[specifier_std].Reset(isolate, resolver->GetPromise());
345+
modules[i] = resolve_return_value;
346+
}
347+
348+
args.GetReturnValue().Set(
349+
Array::New(isolate, modules.out(), modules.length()));
350+
}
351+
286352
void ModuleWrap::Link(const FunctionCallbackInfo<Value>& args) {
287353
Realm* realm = Realm::GetCurrent(args);
288354
Isolate* isolate = args.GetIsolate();
@@ -448,6 +514,87 @@ void ModuleWrap::Evaluate(const FunctionCallbackInfo<Value>& args) {
448514
args.GetReturnValue().Set(result.ToLocalChecked());
449515
}
450516

517+
void ModuleWrap::RunSync(const FunctionCallbackInfo<Value>& args) {
518+
Realm* realm = Realm::GetCurrent(args);
519+
Isolate* isolate = args.GetIsolate();
520+
ModuleWrap* obj;
521+
ASSIGN_OR_RETURN_UNWRAP(&obj, args.This());
522+
Local<Context> context = obj->context();
523+
Local<Module> module = obj->module_.Get(isolate);
524+
Environment* env = realm->env();
525+
526+
// TODO(joyeecheung): use a separate callback that doesn't use promises
527+
// for the cache.
528+
{
529+
TryCatchScope try_catch(env);
530+
USE(module->InstantiateModule(context, ResolveModuleCallback));
531+
532+
// clear resolve cache on instantiate
533+
obj->resolve_cache_.clear();
534+
535+
if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
536+
CHECK(!try_catch.Message().IsEmpty());
537+
CHECK(!try_catch.Exception().IsEmpty());
538+
AppendExceptionLine(env,
539+
try_catch.Exception(),
540+
try_catch.Message(),
541+
ErrorHandlingMode::MODULE_ERROR);
542+
try_catch.ReThrow();
543+
return;
544+
}
545+
}
546+
547+
// If --print-pending-tla is true, proceeds to evaluation even if it's
548+
// async because we want to search for the TLA and help users locate them.
549+
if (module->IsGraphAsync() && !env->options()->print_pending_tla) {
550+
env->ThrowError(
551+
"require() cannot be used on an ESM graph with top-level "
552+
"await. Use import() instead.");
553+
return;
554+
}
555+
556+
Local<Value> result;
557+
{
558+
TryCatchScope try_catch(env);
559+
if (!module->Evaluate(context).ToLocal(&result)) {
560+
if (try_catch.HasCaught()) {
561+
if (!try_catch.HasTerminated()) try_catch.ReThrow();
562+
return;
563+
}
564+
}
565+
}
566+
567+
CHECK(result->IsPromise());
568+
Local<Promise> promise = result.As<Promise>();
569+
if (promise->State() == Promise::PromiseState::kRejected) {
570+
isolate->ThrowException(promise->Result());
571+
return;
572+
}
573+
574+
if (module->IsGraphAsync()) {
575+
CHECK(env->options()->print_pending_tla);
576+
auto stalled = module->GetStalledTopLevelAwaitMessage(isolate);
577+
if (stalled.size() != 0) {
578+
for (auto pair : stalled) {
579+
Local<v8::Message> message = std::get<1>(pair);
580+
581+
std::string reason = "Error: unexpected top-level await at ";
582+
std::string info = FormatMessage(isolate, context, "", message, true);
583+
reason += info;
584+
FPrintF(stderr, "%s\n", reason);
585+
}
586+
}
587+
env->ThrowError(
588+
"require() cannot be used on an ESM graph with top-level "
589+
"await. Use import() instead.");
590+
return;
591+
}
592+
593+
CHECK_EQ(promise->State(), Promise::PromiseState::kFulfilled);
594+
595+
args.GetReturnValue().Set(module->GetModuleNamespace());
596+
}
597+
451598
void ModuleWrap::GetNamespace(const FunctionCallbackInfo<Value>& args) {
452599
Realm* realm = Realm::GetCurrent(args);
453600
Isolate* isolate = args.GetIsolate();
@@ -780,6 +927,8 @@ void ModuleWrap::CreatePerIsolateProperties(IsolateData* isolate_data,
780927
ModuleWrap::kInternalFieldCount);
781928

782929
SetProtoMethod(isolate, tpl, "link", Link);
930+
SetProtoMethod(isolate, tpl, "linkSync", LinkSync);
931+
SetProtoMethod(isolate, tpl, "runSync", RunSync);
783932
SetProtoMethod(isolate, tpl, "instantiate", Instantiate);
784933
SetProtoMethod(isolate, tpl, "evaluate", Evaluate);
785934
SetProtoMethod(isolate, tpl, "setExport", SetSyntheticExport);
@@ -831,6 +980,8 @@ void ModuleWrap::RegisterExternalReferences(
831980
registry->Register(New);
832981

833982
registry->Register(Link);
983+
registry->Register(LinkSync);
984+
registry->Register(RunSync);
834985
registry->Register(Instantiate);
835986
registry->Register(Evaluate);
836987
registry->Register(SetSyntheticExport);

‎src/module_wrap.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ class ModuleWrap : public BaseObject {
7878
~ModuleWrap() override;
7979

8080
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
81+
static void LinkSync(const v8::FunctionCallbackInfo<v8::Value>& args);
82+
static void RunSync(const v8::FunctionCallbackInfo<v8::Value>& args);
8183
static void Link(const v8::FunctionCallbackInfo<v8::Value>& args);
8284
static void Instantiate(const v8::FunctionCallbackInfo<v8::Value>& args);
8385
static void Evaluate(const v8::FunctionCallbackInfo<v8::Value>& args);

‎src/node_contextify.cc

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#include "node_errors.h"
2929
#include "node_external_reference.h"
3030
#include "node_internals.h"
31+
#include "node_process-inl.h"
3132
#include "node_sea.h"
3233
#include "node_snapshot_builder.h"
3334
#include "node_watchdog.h"
@@ -56,6 +57,7 @@ using v8::Maybe;
5657
using v8::MaybeLocal;
5758
using v8::MeasureMemoryExecution;
5859
using v8::MeasureMemoryMode;
60+
using v8::Message;
5961
using v8::MicrotaskQueue;
6062
using v8::MicrotasksPolicy;
6163
using v8::Name;
@@ -1399,6 +1401,22 @@ constexpr std::array<std::string_view, 3> esm_syntax_error_messages = {
13991401
"Unexpected token 'export'", // `export` statements
14001402
"Cannot use 'import.meta' outside a module"}; // `import.meta` references
14011403

1404+
// TODO(joyeecheung): see if we can upstream an API to V8 for this.
1405+
bool IsESMSyntaxError(Isolate* isolate, Local<Message> error_message) {
1406+
if (error_message.IsEmpty()) {
1407+
return false;
1408+
}
1409+
Utf8Value message_utf8(isolate, error_message->Get());
1410+
auto message = message_utf8.ToStringView();
1411+
1412+
for (const auto& error_message : esm_syntax_error_messages) {
1413+
if (message.find(error_message) != std::string_view::npos) {
1414+
return true;
1415+
}
1416+
}
1417+
return false;
1418+
}
1419+
14021420
void ContextifyContext::ContainsModuleSyntax(
14031421
const FunctionCallbackInfo<Value>& args) {
14041422
Environment* env = Environment::GetCurrent(args);
@@ -1473,15 +1491,8 @@ void ContextifyContext::ContainsModuleSyntax(
14731491

14741492
bool found_error_message_caused_by_module_syntax = false;
14751493
if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
1476-
Utf8Value message_value(env->isolate(), try_catch.Message()->Get());
1477-
auto message = message_value.ToStringView();
1478-
1479-
for (const auto& error_message : esm_syntax_error_messages) {
1480-
if (message.find(error_message) != std::string_view::npos) {
1481-
found_error_message_caused_by_module_syntax = true;
1482-
break;
1483-
}
1484-
}
1494+
found_error_message_caused_by_module_syntax =
1495+
IsESMSyntaxError(env->isolate(), try_catch.Message());
14851496
}
14861497
args.GetReturnValue().Set(found_error_message_caused_by_module_syntax);
14871498
}
@@ -1550,13 +1561,35 @@ static void CompileFunctionForCJSLoader(
15501561
v8::ScriptCompiler::NoCacheReason::kNoCacheNoReason);
15511562

15521563
Local<Function> fn;
1564+
bool retry_as_esm = false;
15531565
if (!maybe_fn.ToLocal(&fn)) {
1554-
if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
1555-
errors::DecorateErrorStack(env, try_catch);
1556-
if (!try_catch.HasTerminated()) {
1566+
if (try_catch.HasTerminated()) {
1567+
return;
1568+
}
1569+
if (try_catch.HasCaught()) {
1570+
Local<Value> error = try_catch.Exception();
1571+
Local<Message> message = try_catch.Message();
1572+
if (!error->IsNativeError() || message.IsEmpty() ||
1573+
!IsESMSyntaxError(isolate, message)) {
1574+
errors::DecorateErrorStack(env, try_catch);
15571575
try_catch.ReThrow();
1576+
return;
15581577
}
1559-
return;
1578+
1579+
if (!env->options()->require_module) {
1580+
if (ProcessEmitWarningSync(env,
1581+
"(To load an ES module, set \"type\": "
1582+
"\"module\" in the package.json "
1583+
"or use the .mjs extension.)")
1584+
.IsJust()) {
1585+
errors::DecorateErrorStack(env, try_catch);
1586+
try_catch.ReThrow();
1587+
}
1588+
return;
1589+
}
1590+
1591+
// The file being compiled is likely ESM.
1592+
retry_as_esm = true;
15601593
}
15611594
}
15621595

@@ -1571,11 +1604,14 @@ static void CompileFunctionForCJSLoader(
15711604
env->cached_data_rejected_string(),
15721605
env->source_map_url_string(),
15731606
env->function_string(),
1607+
FIXED_ONE_BYTE_STRING(isolate, "retryAsESM"),
15741608
};
15751609
std::vector<Local<Value>> values = {
15761610
Boolean::New(isolate, cache_rejected),
1577-
fn->GetScriptOrigin().SourceMapUrl(),
1578-
fn,
1611+
retry_as_esm ? v8::Undefined(isolate).As<Value>()
1612+
: fn->GetScriptOrigin().SourceMapUrl(),
1613+
retry_as_esm ? v8::Undefined(isolate).As<Value>() : fn.As<Value>(),
1614+
Boolean::New(isolate, retry_as_esm),
15791615
};
15801616
Local<Object> result = Object::New(
15811617
isolate, v8::Null(isolate), names.data(), values.data(), names.size());

‎src/node_options.cc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,11 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
359359
"top-level await in the graph",
360360
&EnvironmentOptions::print_pending_tla,
361361
kAllowedInEnvvar);
362+
AddOption("--experimental-require-module",
363+
"Allow loading ES Module in require()",
364+
&EnvironmentOptions::require_module,
365+
kAllowedInEnvvar);
366+
362367
AddOption("--diagnostic-dir",
363368
"set dir for all output files"
364369
" (default: current working directory)",

‎src/node_options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ class EnvironmentOptions : public Options {
106106
std::vector<std::string> conditions;
107107
bool detect_module = false;
108108
bool print_pending_tla = false;
109+
bool require_module = false;
109110
std::string dns_result_order;
110111
bool enable_source_maps = false;
111112
bool experimental_fetch = true;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Flags: --experimental-require-module
2+
'use strict';
3+
4+
import { mustCall } from '../common/index.mjs';
5+
const fn = mustCall(() => {
6+
console.log('hello');
7+
});
8+
fn();

‎test/es-module/test-require-module.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Flags: --experimental-require-module
2+
'use strict';
3+
4+
const common = require('../common'); // This is a test too.
5+
const assert = require('assert');
6+
const { isModuleNamespaceObject } = require('util/types');
7+
8+
{
9+
const mjs = require('../common/index.mjs');
10+
// Only comparing a few properties because the ESM version doesn't re-export everything
11+
// from the CJS version.
12+
assert.strictEqual(common.mustCall, mjs.mustCall);
13+
assert(!isModuleNamespaceObject(common));
14+
assert(isModuleNamespaceObject(mjs));
15+
}
16+
{
17+
const mod = require('../fixtures/es-module-loaders/module-named-exports.mjs');
18+
assert.deepStrictEqual({ ...mod }, { foo: 'foo', bar: 'bar' });
19+
assert(isModuleNamespaceObject(mod));
20+
}
21+
22+
{
23+
const mod = require('../fixtures/source-map/esm-basic.mjs');
24+
assert.deepStrictEqual({ ...mod }, {});
25+
assert(isModuleNamespaceObject(mod));
26+
}
27+
{
28+
const mod = require('../fixtures/es-modules/loose.js');
29+
assert.deepStrictEqual({ ...mod }, { default: 'module' });
30+
assert(isModuleNamespaceObject(mod));
31+
}
32+
33+
assert.throws(() => {
34+
require('../fixtures/es-modules/tla/a.mjs');
35+
}, {
36+
message: /require\(\) cannot be used on an ESM graph with top-level await\. Use import\(\) instead\./
37+
});

0 commit comments

Comments
 (0)
Please sign in to comment.