Skip to content

Commit f3e3429

Browse files
guybedfordMylesBorins
authored andcommitted
module: support main w/o extension, pjson cache
This adds support for ensuring that the top-level main into Node is supported loading when it has no extension for backwards-compat with NodeJS bin workflows. In addition package.json caching is implemented in the module lookup process. Backport-PR-URL: #18923 PR-URL: #18728 Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
1 parent 50d1233 commit f3e3429

15 files changed

+216
-133
lines changed

doc/api/esm.md

+12-4
Original file line numberDiff line numberDiff line change
@@ -113,17 +113,22 @@ The resolve hook returns the resolved file URL and module format for a
113113
given module specifier and parent file URL:
114114

115115
```js
116-
import url from 'url';
116+
const baseURL = new URL('file://');
117+
baseURL.pathname = process.cwd() + '/';
117118

118-
export async function resolve(specifier, parentModuleURL, defaultResolver) {
119+
export async function resolve(specifier,
120+
parentModuleURL = baseURL,
121+
defaultResolver) {
119122
return {
120123
url: new URL(specifier, parentModuleURL).href,
121124
format: 'esm'
122125
};
123126
}
124127
```
125128

126-
The default NodeJS ES module resolution function is provided as a third
129+
The parentURL is provided as `undefined` when performing main Node.js load itself.
130+
131+
The default Node.js ES module resolution function is provided as a third
127132
argument to the resolver for easy compatibility workflows.
128133

129134
In addition to returning the resolved file URL value, the resolve hook also
@@ -152,7 +157,10 @@ import Module from 'module';
152157
const builtins = Module.builtinModules;
153158
const JS_EXTENSIONS = new Set(['.js', '.mjs']);
154159

155-
export function resolve(specifier, parentModuleURL/*, defaultResolve */) {
160+
const baseURL = new URL('file://');
161+
baseURL.pathname = process.cwd() + '/';
162+
163+
export function resolve(specifier, parentModuleURL = baseURL, defaultResolve) {
156164
if (builtins.includes(specifier)) {
157165
return {
158166
url: specifier,

lib/internal/bootstrap_node.js

+1
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@
105105
process.emitWarning(
106106
'The ESM module loader is experimental.',
107107
'ExperimentalWarning', undefined);
108+
NativeModule.require('internal/process/modules').setup();
108109
}
109110

110111

lib/internal/loader/DefaultResolve.js

+16-5
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
const { URL } = require('url');
44
const CJSmodule = require('module');
5-
const internalURLModule = require('internal/url');
65
const internalFS = require('internal/fs');
76
const NativeModule = require('native_module');
87
const { extname } = require('path');
@@ -11,6 +10,7 @@ const preserveSymlinks = !!process.binding('config').preserveSymlinks;
1110
const errors = require('internal/errors');
1211
const { resolve: moduleWrapResolve } = internalBinding('module_wrap');
1312
const StringStartsWith = Function.call.bind(String.prototype.startsWith);
13+
const { getURLFromFilePath, getPathFromURL } = require('internal/url');
1414

1515
const realpathCache = new Map();
1616

@@ -57,7 +57,8 @@ function resolve(specifier, parentURL) {
5757

5858
let url;
5959
try {
60-
url = search(specifier, parentURL);
60+
url = search(specifier,
61+
parentURL || getURLFromFilePath(`${process.cwd()}/`).href);
6162
} catch (e) {
6263
if (typeof e.message === 'string' &&
6364
StringStartsWith(e.message, 'Cannot find module'))
@@ -66,17 +67,27 @@ function resolve(specifier, parentURL) {
6667
}
6768

6869
if (!preserveSymlinks) {
69-
const real = realpathSync(internalURLModule.getPathFromURL(url), {
70+
const real = realpathSync(getPathFromURL(url), {
7071
[internalFS.realpathCacheKey]: realpathCache
7172
});
7273
const old = url;
73-
url = internalURLModule.getURLFromFilePath(real);
74+
url = getURLFromFilePath(real);
7475
url.search = old.search;
7576
url.hash = old.hash;
7677
}
7778

7879
const ext = extname(url.pathname);
79-
return { url: `${url}`, format: extensionFormatMap[ext] || ext };
80+
81+
let format = extensionFormatMap[ext];
82+
if (!format) {
83+
const isMain = parentURL === undefined;
84+
if (isMain)
85+
format = 'cjs';
86+
else
87+
throw new errors.Error('ERR_UNKNOWN_FILE_EXTENSION', url.pathname);
88+
}
89+
90+
return { url: `${url}`, format };
8091
}
8192

8293
module.exports = resolve;

lib/internal/loader/Loader.js

+11-48
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,21 @@
11
'use strict';
22

3-
const path = require('path');
4-
const { getURLFromFilePath, URL } = require('internal/url');
53
const errors = require('internal/errors');
6-
74
const ModuleMap = require('internal/loader/ModuleMap');
85
const ModuleJob = require('internal/loader/ModuleJob');
96
const defaultResolve = require('internal/loader/DefaultResolve');
107
const createDynamicModule = require('internal/loader/CreateDynamicModule');
118
const translators = require('internal/loader/Translators');
12-
const { setImportModuleDynamicallyCallback } = internalBinding('module_wrap');
9+
1310
const FunctionBind = Function.call.bind(Function.prototype.bind);
1411

1512
const debug = require('util').debuglog('esm');
1613

17-
// Returns a file URL for the current working directory.
18-
function getURLStringForCwd() {
19-
try {
20-
return getURLFromFilePath(`${process.cwd()}/`).href;
21-
} catch (e) {
22-
e.stack;
23-
// If the current working directory no longer exists.
24-
if (e.code === 'ENOENT') {
25-
return undefined;
26-
}
27-
throw e;
28-
}
29-
}
30-
31-
function normalizeReferrerURL(referrer) {
32-
if (typeof referrer === 'string' && path.isAbsolute(referrer)) {
33-
return getURLFromFilePath(referrer).href;
34-
}
35-
return new URL(referrer).href;
36-
}
37-
3814
/* A Loader instance is used as the main entry point for loading ES modules.
3915
* Currently, this is a singleton -- there is only one used for loading
4016
* the main module and everything in its dependency graph. */
4117
class Loader {
42-
constructor(base = getURLStringForCwd()) {
43-
if (typeof base !== 'string')
44-
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'base', 'string');
45-
46-
this.base = base;
47-
this.isMain = true;
48-
18+
constructor() {
4919
// methods which translate input code or other information
5020
// into es modules
5121
this.translators = translators;
@@ -71,8 +41,9 @@ class Loader {
7141
this._dynamicInstantiate = undefined;
7242
}
7343

74-
async resolve(specifier, parentURL = this.base) {
75-
if (typeof parentURL !== 'string')
44+
async resolve(specifier, parentURL) {
45+
const isMain = parentURL === undefined;
46+
if (!isMain && typeof parentURL !== 'string')
7647
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'parentURL', 'string');
7748

7849
const { url, format } =
@@ -93,7 +64,7 @@ class Loader {
9364
return { url, format };
9465
}
9566

96-
async import(specifier, parent = this.base) {
67+
async import(specifier, parent) {
9768
const job = await this.getModuleJob(specifier, parent);
9869
const module = await job.run();
9970
return module.namespace();
@@ -107,7 +78,7 @@ class Loader {
10778
this._dynamicInstantiate = FunctionBind(dynamicInstantiate, null);
10879
}
10980

110-
async getModuleJob(specifier, parentURL = this.base) {
81+
async getModuleJob(specifier, parentURL) {
11182
const { url, format } = await this.resolve(specifier, parentURL);
11283
let job = this.moduleMap.get(url);
11384
if (job !== undefined)
@@ -134,24 +105,16 @@ class Loader {
134105
}
135106

136107
let inspectBrk = false;
137-
if (this.isMain) {
138-
if (process._breakFirstLine) {
139-
delete process._breakFirstLine;
140-
inspectBrk = true;
141-
}
142-
this.isMain = false;
108+
if (process._breakFirstLine) {
109+
delete process._breakFirstLine;
110+
inspectBrk = true;
143111
}
144112
job = new ModuleJob(this, url, loaderInstance, inspectBrk);
145113
this.moduleMap.set(url, job);
146114
return job;
147115
}
148-
149-
static registerImportDynamicallyCallback(loader) {
150-
setImportModuleDynamicallyCallback(async (referrer, specifier) => {
151-
return loader.import(specifier, normalizeReferrerURL(referrer));
152-
});
153-
}
154116
}
155117

156118
Object.setPrototypeOf(Loader.prototype, null);
119+
157120
module.exports = Loader;

lib/internal/loader/Translators.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const JsonParse = JSON.parse;
1919
const translators = new SafeMap();
2020
module.exports = translators;
2121

22-
// Stragety for loading a standard JavaScript module
22+
// Strategy for loading a standard JavaScript module
2323
translators.set('esm', async (url) => {
2424
const source = `${await readFileAsync(new URL(url))}`;
2525
debug(`Translating StandardModule ${url}`);
@@ -62,7 +62,7 @@ translators.set('builtin', async (url) => {
6262
});
6363
});
6464

65-
// Stragety for loading a node native module
65+
// Strategy for loading a node native module
6666
translators.set('addon', async (url) => {
6767
debug(`Translating NativeModule ${url}`);
6868
return createDynamicModule(['default'], url, (reflect) => {
@@ -74,7 +74,7 @@ translators.set('addon', async (url) => {
7474
});
7575
});
7676

77-
// Stragety for loading a JSON file
77+
// Strategy for loading a JSON file
7878
translators.set('json', async (url) => {
7979
debug(`Translating JSONModule ${url}`);
8080
return createDynamicModule(['default'], url, (reflect) => {

lib/internal/process/modules.js

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
'use strict';
2+
3+
const {
4+
setImportModuleDynamicallyCallback
5+
} = internalBinding('module_wrap');
6+
7+
const { getURLFromFilePath } = require('internal/url');
8+
const Loader = require('internal/loader/Loader');
9+
const path = require('path');
10+
const { URL } = require('url');
11+
12+
function normalizeReferrerURL(referrer) {
13+
if (typeof referrer === 'string' && path.isAbsolute(referrer)) {
14+
return getURLFromFilePath(referrer).href;
15+
}
16+
return new URL(referrer).href;
17+
}
18+
19+
let loaderResolve;
20+
exports.loaderPromise = new Promise((resolve, reject) => {
21+
loaderResolve = resolve;
22+
});
23+
24+
exports.ESMLoader = undefined;
25+
26+
exports.setup = function() {
27+
let ESMLoader = new Loader();
28+
const loaderPromise = (async () => {
29+
const userLoader = process.binding('config').userLoader;
30+
if (userLoader) {
31+
const hooks = await ESMLoader.import(
32+
userLoader, getURLFromFilePath(`${process.cwd()}/`).href);
33+
ESMLoader = new Loader();
34+
ESMLoader.hook(hooks);
35+
exports.ESMLoader = ESMLoader;
36+
}
37+
return ESMLoader;
38+
})();
39+
loaderResolve(loaderPromise);
40+
41+
setImportModuleDynamicallyCallback(async (referrer, specifier) => {
42+
const loader = await loaderPromise;
43+
return loader.import(specifier, normalizeReferrerURL(referrer));
44+
});
45+
46+
exports.ESMLoader = ESMLoader;
47+
};

lib/module.js

+8-21
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
const NativeModule = require('native_module');
2525
const util = require('util');
2626
const { decorateErrorStack } = require('internal/util');
27-
const internalModule = require('internal/module');
2827
const { getURLFromFilePath } = require('internal/url');
2928
const vm = require('vm');
3029
const assert = require('assert').ok;
@@ -35,6 +34,7 @@ const {
3534
internalModuleReadFile,
3635
internalModuleStat
3736
} = process.binding('fs');
37+
const internalModule = require('internal/module');
3838
const preserveSymlinks = !!process.binding('config').preserveSymlinks;
3939
const experimentalModules = !!process.binding('config').experimentalModules;
4040

@@ -43,10 +43,9 @@ const errors = require('internal/errors');
4343
module.exports = Module;
4444

4545
// these are below module.exports for the circular reference
46-
const Loader = require('internal/loader/Loader');
46+
const internalESModule = require('internal/process/modules');
4747
const ModuleJob = require('internal/loader/ModuleJob');
4848
const createDynamicModule = require('internal/loader/CreateDynamicModule');
49-
let ESMLoader;
5049

5150
function stat(filename) {
5251
filename = path.toNamespacedPath(filename);
@@ -444,7 +443,6 @@ Module._resolveLookupPaths = function(request, parent, newReturn) {
444443
return (newReturn ? parentDir : [id, parentDir]);
445444
};
446445

447-
448446
// Check the cache for the requested file.
449447
// 1. If a module already exists in the cache: return its exports object.
450448
// 2. If the module is native: call `NativeModule.require()` with the
@@ -457,22 +455,10 @@ Module._load = function(request, parent, isMain) {
457455
debug('Module._load REQUEST %s parent: %s', request, parent.id);
458456
}
459457

460-
if (isMain && experimentalModules) {
461-
(async () => {
462-
// loader setup
463-
if (!ESMLoader) {
464-
ESMLoader = new Loader();
465-
const userLoader = process.binding('config').userLoader;
466-
if (userLoader) {
467-
ESMLoader.isMain = false;
468-
const hooks = await ESMLoader.import(userLoader);
469-
ESMLoader = new Loader();
470-
ESMLoader.hook(hooks);
471-
}
472-
}
473-
Loader.registerImportDynamicallyCallback(ESMLoader);
474-
await ESMLoader.import(getURLFromFilePath(request).pathname);
475-
})()
458+
if (experimentalModules && isMain) {
459+
internalESModule.loaderPromise.then((loader) => {
460+
return loader.import(getURLFromFilePath(request).pathname);
461+
})
476462
.catch((e) => {
477463
decorateErrorStack(e);
478464
console.error(e);
@@ -575,7 +561,8 @@ Module.prototype.load = function(filename) {
575561
Module._extensions[extension](this, filename);
576562
this.loaded = true;
577563

578-
if (ESMLoader) {
564+
if (experimentalModules) {
565+
const ESMLoader = internalESModule.ESMLoader;
579566
const url = getURLFromFilePath(filename);
580567
const urlString = `${url}`;
581568
const exports = this.exports;

node.gyp

+1
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@
115115
'lib/internal/net.js',
116116
'lib/internal/module.js',
117117
'lib/internal/os.js',
118+
'lib/internal/process/modules.js',
118119
'lib/internal/process/next_tick.js',
119120
'lib/internal/process/promises.js',
120121
'lib/internal/process/stdio.js',

0 commit comments

Comments
 (0)