Skip to content

Commit c7adb00

Browse files
bfarias-godaddyMylesBorins
authored andcommitted
module: refactor modules bootstrap
PR-URL: #29937 Reviewed-By: Myles Borins <[email protected]> Reviewed-By: Joyee Cheung <[email protected]>
1 parent 69dac4b commit c7adb00

17 files changed

+181
-102
lines changed

doc/api/esm.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -905,13 +905,13 @@ _isMain_ is **true** when resolving the Node.js application entry point.
905905
> 1. Throw a _Module Not Found_ error.
906906
> 1. If _pjson.exports_ is not **null** or **undefined**, then
907907
> 1. If _pjson.exports_ is a String or Array, then
908-
> 1. Return _PACKAGE_EXPORTS_TARGET_RESOLVE(packageURL, pjson.exports,
909-
> "")_.
908+
> 1. Return **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_,
909+
> _pjson.exports_, "")_.
910910
> 1. If _pjson.exports is an Object, then
911911
> 1. If _pjson.exports_ contains a _"."_ property, then
912912
> 1. Let _mainExport_ be the _"."_ property in _pjson.exports_.
913-
> 1. Return _PACKAGE_EXPORTS_TARGET_RESOLVE(packageURL, mainExport,
914-
> "")_.
913+
> 1. Return **PACKAGE_EXPORTS_TARGET_RESOLVE**(_packageURL_,
914+
> _mainExport_, "")_.
915915
> 1. If _pjson.main_ is a String, then
916916
> 1. Let _resolvedMain_ be the URL resolution of _packageURL_, "/", and
917917
> _pjson.main_.

lib/internal/bootstrap/loaders.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,10 @@ NativeModule.prototype.compileForPublicLoader = function(needToSyncExports) {
221221
this.compile();
222222
if (needToSyncExports) {
223223
if (!this.exportKeys) {
224-
this.exportKeys = Object.keys(this.exports);
224+
// When using --expose-internals, we do not want to reflect the named
225+
// exports from core modules as this can trigger unnecessary getters.
226+
const internal = this.id.startsWith('internal/');
227+
this.exportKeys = internal ? [] : Object.keys(this.exports);
225228
}
226229
this.getESMFacade();
227230
this.syncExports();

lib/internal/bootstrap/pre_execution.js

+60-9
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const { Object, SafeWeakMap } = primordials;
55
const { getOptionValue } = require('internal/options');
66
const { Buffer } = require('buffer');
77
const { ERR_MANIFEST_ASSERT_INTEGRITY } = require('internal/errors').codes;
8+
const path = require('path');
89

910
function prepareMainThreadExecution(expandArgv1 = false) {
1011
// Patch the process object with legacy properties and normalizations
@@ -404,7 +405,6 @@ function initializeESMLoader() {
404405
'The ESM module loader is experimental.',
405406
'ExperimentalWarning', undefined);
406407
}
407-
408408
const {
409409
setImportModuleDynamicallyCallback,
410410
setInitializeImportMetaObjectCallback
@@ -414,14 +414,6 @@ function initializeESMLoader() {
414414
// track of for different ESM modules.
415415
setInitializeImportMetaObjectCallback(esm.initializeImportMetaObject);
416416
setImportModuleDynamicallyCallback(esm.importModuleDynamicallyCallback);
417-
const userLoader = getOptionValue('--experimental-loader');
418-
// If --experimental-loader is specified, create a loader with user hooks.
419-
// Otherwise create the default loader.
420-
if (userLoader) {
421-
const { emitExperimentalWarning } = require('internal/util');
422-
emitExperimentalWarning('--experimental-loader');
423-
}
424-
esm.initializeLoader(process.cwd(), userLoader);
425417
}
426418
}
427419

@@ -446,11 +438,70 @@ function loadPreloadModules() {
446438
}
447439
}
448440

441+
function resolveMainPath(main) {
442+
const { toRealPath, Module: CJSModule } =
443+
require('internal/modules/cjs/loader');
444+
445+
// Note extension resolution for the main entry point can be deprecated in a
446+
// future major.
447+
let mainPath = CJSModule._findPath(path.resolve(main), null, true);
448+
if (!mainPath)
449+
return;
450+
451+
const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
452+
if (!preserveSymlinksMain)
453+
mainPath = toRealPath(mainPath);
454+
455+
return mainPath;
456+
}
457+
458+
function shouldUseESMLoader(mainPath) {
459+
const experimentalModules = getOptionValue('--experimental-modules');
460+
if (!experimentalModules)
461+
return false;
462+
const userLoader = getOptionValue('--experimental-loader');
463+
if (userLoader)
464+
return true;
465+
// Determine the module format of the main
466+
if (mainPath && mainPath.endsWith('.mjs'))
467+
return true;
468+
if (!mainPath || mainPath.endsWith('.cjs'))
469+
return false;
470+
const { readPackageScope } = require('internal/modules/cjs/loader');
471+
const pkg = readPackageScope(mainPath);
472+
return pkg && pkg.data.type === 'module';
473+
}
474+
475+
function runMainESM(mainPath) {
476+
const esmLoader = require('internal/process/esm_loader');
477+
const { pathToFileURL } = require('internal/url');
478+
const { hasUncaughtExceptionCaptureCallback } =
479+
require('internal/process/execution');
480+
return esmLoader.initializeLoader().then(() => {
481+
const main = path.isAbsolute(mainPath) ?
482+
pathToFileURL(mainPath).href : mainPath;
483+
return esmLoader.ESMLoader.import(main).catch((e) => {
484+
if (hasUncaughtExceptionCaptureCallback()) {
485+
process._fatalException(e);
486+
return;
487+
}
488+
internalBinding('errors').triggerUncaughtException(
489+
e,
490+
true /* fromPromise */
491+
);
492+
});
493+
});
494+
}
495+
496+
449497
module.exports = {
450498
patchProcessObject,
499+
resolveMainPath,
500+
runMainESM,
451501
setupCoverageHooks,
452502
setupWarningHandler,
453503
setupDebugEnv,
504+
shouldUseESMLoader,
454505
prepareMainThreadExecution,
455506
initializeDeprecations,
456507
initializeESMLoader,

lib/internal/main/run_main_module.js

+4-5
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ const CJSModule = require('internal/modules/cjs/loader').Module;
1010

1111
markBootstrapComplete();
1212

13-
// Note: this actually tries to run the module as a ESM first if
14-
// --experimental-modules is on.
15-
// TODO(joyeecheung): can we move that logic to here? Note that this
16-
// is an undocumented method available via `require('module').runMain`
17-
CJSModule.runMain();
13+
// Note: this loads the module through the ESM loader if
14+
// --experimental-loader is provided or --experimental-modules is on
15+
// and the module is determined to be an ES module
16+
CJSModule.runMain(process.argv[1]);

lib/internal/main/worker_thread.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,9 @@ port.on('message', (message) => {
135135
const { evalScript } = require('internal/process/execution');
136136
evalScript('[worker eval]', filename);
137137
} else {
138-
process.argv[1] = filename; // script filename
139-
require('module').runMain();
138+
// script filename
139+
const CJSModule = require('internal/modules/cjs/loader').Module;
140+
CJSModule.runMain(process.argv[1] = filename);
140141
}
141142
} else if (message.type === STDIO_PAYLOAD) {
142143
const { stream, chunk, encoding } = message;

lib/internal/modules/cjs/loader.js

+37-30
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,14 @@ const {
7070
ERR_REQUIRE_ESM
7171
} = require('internal/errors').codes;
7272
const { validateString } = require('internal/validators');
73+
const {
74+
resolveMainPath,
75+
shouldUseESMLoader,
76+
runMainESM
77+
} = require('internal/bootstrap/pre_execution');
7378
const pendingDeprecation = getOptionValue('--pending-deprecation');
7479

75-
module.exports = { wrapSafe, Module };
80+
module.exports = { wrapSafe, Module, toRealPath, readPackageScope };
7681

7782
let asyncESM, ModuleJob, ModuleWrap, kInstantiated;
7883

@@ -898,6 +903,10 @@ Module.prototype.load = function(filename) {
898903
this.paths = Module._nodeModulePaths(path.dirname(filename));
899904

900905
const extension = findLongestRegisteredExtension(filename);
906+
// allow .mjs to be overridden
907+
if (filename.endsWith('.mjs') && !Module._extensions['.mjs']) {
908+
throw new ERR_REQUIRE_ESM(filename);
909+
}
901910
Module._extensions[extension](this, filename);
902911
this.loaded = true;
903912

@@ -911,14 +920,19 @@ Module.prototype.load = function(filename) {
911920
if (module !== undefined && module.module !== undefined) {
912921
if (module.module.getStatus() >= kInstantiated)
913922
module.module.setExport('default', exports);
914-
} else { // preemptively cache
923+
} else {
924+
// Preemptively cache
925+
// We use a function to defer promise creation for async hooks.
915926
ESMLoader.moduleMap.set(
916927
url,
917-
new ModuleJob(ESMLoader, url, () =>
928+
// Module job creation will start promises.
929+
// We make it a function to lazily trigger those promises
930+
// for async hooks compatibility.
931+
() => new ModuleJob(ESMLoader, url, () =>
918932
new ModuleWrap(url, undefined, ['default'], function() {
919933
this.setExport('default', exports);
920934
})
921-
)
935+
, false /* isMain */, false /* inspectBrk */)
922936
);
923937
}
924938
}
@@ -947,15 +961,15 @@ Module.prototype.require = function(id) {
947961
let resolvedArgv;
948962
let hasPausedEntry = false;
949963

950-
function wrapSafe(filename, content) {
964+
function wrapSafe(filename, content, cjsModuleInstance) {
951965
if (patched) {
952966
const wrapper = Module.wrap(content);
953967
return vm.runInThisContext(wrapper, {
954968
filename,
955969
lineOffset: 0,
956970
displayErrors: true,
957971
importModuleDynamically: experimentalModules ? async (specifier) => {
958-
const loader = await asyncESM.loaderPromise;
972+
const loader = asyncESM.ESMLoader;
959973
return loader.import(specifier, normalizeReferrerURL(filename));
960974
} : undefined,
961975
});
@@ -981,17 +995,16 @@ function wrapSafe(filename, content) {
981995
]
982996
);
983997
} catch (err) {
984-
if (experimentalModules) {
998+
if (experimentalModules && process.mainModule === cjsModuleInstance)
985999
enrichCJSError(err);
986-
}
9871000
throw err;
9881001
}
9891002

9901003
if (experimentalModules) {
9911004
const { callbackMap } = internalBinding('module_wrap');
9921005
callbackMap.set(compiled.cacheKey, {
9931006
importModuleDynamically: async (specifier) => {
994-
const loader = await asyncESM.loaderPromise;
1007+
const loader = asyncESM.ESMLoader;
9951008
return loader.import(specifier, normalizeReferrerURL(filename));
9961009
}
9971010
});
@@ -1014,7 +1027,7 @@ Module.prototype._compile = function(content, filename) {
10141027
}
10151028

10161029
maybeCacheSourceMap(filename, content, this);
1017-
const compiledWrapper = wrapSafe(filename, content);
1030+
const compiledWrapper = wrapSafe(filename, content, this);
10181031

10191032
var inspectorWrapper = null;
10201033
if (getOptionValue('--inspect-brk') && process._eval == null) {
@@ -1070,7 +1083,11 @@ Module._extensions['.js'] = function(module, filename) {
10701083
'files in that package scope as ES modules.\nInstead rename ' +
10711084
`${basename} to end in .cjs, change the requiring code to use ` +
10721085
'import(), or remove "type": "module" from ' +
1073-
`${path.resolve(pkg.path, 'package.json')}.`
1086+
`${path.resolve(pkg.path, 'package.json')}.`,
1087+
undefined,
1088+
undefined,
1089+
undefined,
1090+
true
10741091
);
10751092
warnRequireESM = false;
10761093
}
@@ -1113,26 +1130,16 @@ Module._extensions['.node'] = function(module, filename) {
11131130
return process.dlopen(module, path.toNamespacedPath(filename));
11141131
};
11151132

1116-
Module._extensions['.mjs'] = function(module, filename) {
1117-
throw new ERR_REQUIRE_ESM(filename);
1118-
};
1119-
11201133
// Bootstrap main module.
1121-
Module.runMain = function() {
1122-
// Load the main module--the command line argument.
1123-
if (experimentalModules) {
1124-
asyncESM.loaderPromise.then((loader) => {
1125-
return loader.import(pathToFileURL(process.argv[1]).href);
1126-
})
1127-
.catch((e) => {
1128-
internalBinding('errors').triggerUncaughtException(
1129-
e,
1130-
true /* fromPromise */
1131-
);
1132-
});
1133-
return;
1134+
Module.runMain = function(main = process.argv[1]) {
1135+
const resolvedMain = resolveMainPath(main);
1136+
const useESMLoader = shouldUseESMLoader(resolvedMain);
1137+
module.exports.asyncRunMain = useESMLoader;
1138+
if (useESMLoader) {
1139+
runMainESM(resolvedMain || main);
1140+
} else {
1141+
Module._load(main, null, true);
11341142
}
1135-
Module._load(process.argv[1], null, true);
11361143
};
11371144

11381145
function createRequireFromPath(filename) {
@@ -1238,7 +1245,7 @@ Module.Module = Module;
12381245

12391246
// We have to load the esm things after module.exports!
12401247
if (experimentalModules) {
1241-
asyncESM = require('internal/process/esm_loader');
12421248
ModuleJob = require('internal/modules/esm/module_job');
1249+
asyncESM = require('internal/process/esm_loader');
12431250
({ ModuleWrap, kInstantiated } = internalBinding('module_wrap'));
12441251
}

lib/internal/modules/esm/loader.js

+9-2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const createDynamicModule = require(
2222
'internal/modules/esm/create_dynamic_module');
2323
const { translators } = require('internal/modules/esm/translators');
2424
const { ModuleWrap } = internalBinding('module_wrap');
25+
const { getOptionValue } = require('internal/options');
2526

2627
const debug = require('internal/util/debuglog').debuglog('esm');
2728

@@ -118,7 +119,7 @@ class Loader {
118119
url = pathToFileURL(`${process.cwd()}/[eval${++this.evalIndex}]`).href
119120
) {
120121
const evalInstance = (url) => new ModuleWrap(url, undefined, source, 0, 0);
121-
const job = new ModuleJob(this, url, evalInstance, false);
122+
const job = new ModuleJob(this, url, evalInstance, false, false);
122123
this.moduleMap.set(url, job);
123124
const { module, result } = await job.run();
124125
return {
@@ -146,6 +147,9 @@ class Loader {
146147
async getModuleJob(specifier, parentURL) {
147148
const { url, format } = await this.resolve(specifier, parentURL);
148149
let job = this.moduleMap.get(url);
150+
// CommonJS will set functions for lazy job evaluation.
151+
if (typeof job === 'function')
152+
this.moduleMap.set(url, job = job());
149153
if (job !== undefined)
150154
return job;
151155

@@ -169,7 +173,10 @@ class Loader {
169173
loaderInstance = translators.get(format);
170174
}
171175

172-
job = new ModuleJob(this, url, loaderInstance, parentURL === undefined);
176+
const inspectBrk = parentURL === undefined &&
177+
format === 'module' && getOptionValue('--inspect-brk');
178+
job = new ModuleJob(this, url, loaderInstance, parentURL === undefined,
179+
inspectBrk);
173180
this.moduleMap.set(url, job);
174181
return job;
175182
}

lib/internal/modules/esm/module_job.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ const {
99
const { ModuleWrap } = internalBinding('module_wrap');
1010

1111
const { decorateErrorStack } = require('internal/util');
12-
const { getOptionValue } = require('internal/options');
1312
const assert = require('internal/assert');
1413
const resolvedPromise = SafePromise.resolve();
1514

@@ -22,9 +21,10 @@ let hasPausedEntry = false;
2221
class ModuleJob {
2322
// `loader` is the Loader instance used for loading dependencies.
2423
// `moduleProvider` is a function
25-
constructor(loader, url, moduleProvider, isMain) {
24+
constructor(loader, url, moduleProvider, isMain, inspectBrk) {
2625
this.loader = loader;
2726
this.isMain = isMain;
27+
this.inspectBrk = inspectBrk;
2828

2929
// This is a Promise<{ module, reflect }>, whose fields will be copied
3030
// onto `this` by `link()` below once it has been resolved.
@@ -83,12 +83,12 @@ class ModuleJob {
8383
};
8484
await addJobsToDependencyGraph(this);
8585
try {
86-
if (!hasPausedEntry && this.isMain && getOptionValue('--inspect-brk')) {
86+
if (!hasPausedEntry && this.inspectBrk) {
8787
hasPausedEntry = true;
8888
const initWrapper = internalBinding('inspector').callAndPauseOnStart;
8989
initWrapper(this.module.instantiate, this.module);
9090
} else {
91-
this.module.instantiate();
91+
this.module.instantiate(true);
9292
}
9393
} catch (e) {
9494
decorateErrorStack(e);

lib/internal/modules/esm/module_map.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ class ModuleMap extends SafeMap {
1616
}
1717
set(url, job) {
1818
validateString(url, 'url');
19-
if (job instanceof ModuleJob !== true) {
19+
if (job instanceof ModuleJob !== true &&
20+
typeof job !== 'function') {
2021
throw new ERR_INVALID_ARG_TYPE('job', 'ModuleJob', job);
2122
}
2223
debug(`Storing ${url} in ModuleMap`);

0 commit comments

Comments
 (0)