Skip to content

Commit a55b75d

Browse files
guybedfordMayaLekova
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. PR-URL: nodejs#18728 Reviewed-By: Benjamin Gruenbaum <[email protected]>
1 parent 4b4b1a2 commit a55b75d

File tree

13 files changed

+208
-137
lines changed

13 files changed

+208
-137
lines changed

doc/api/esm.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,17 +117,22 @@ The resolve hook returns the resolved file URL and module format for a
117117
given module specifier and parent file URL:
118118

119119
```js
120-
import url from 'url';
120+
const baseURL = new URL('file://');
121+
baseURL.pathname = process.cwd() + '/';
121122

122-
export async function resolve(specifier, parentModuleURL, defaultResolver) {
123+
export async function resolve(specifier,
124+
parentModuleURL = baseURL,
125+
defaultResolver) {
123126
return {
124127
url: new URL(specifier, parentModuleURL).href,
125128
format: 'esm'
126129
};
127130
}
128131
```
129132

130-
The default NodeJS ES module resolution function is provided as a third
133+
The parentURL is provided as `undefined` when performing main Node.js load itself.
134+
135+
The default Node.js ES module resolution function is provided as a third
131136
argument to the resolver for easy compatibility workflows.
132137

133138
In addition to returning the resolved file URL value, the resolve hook also
@@ -155,7 +160,10 @@ import Module from 'module';
155160
const builtins = Module.builtinModules;
156161
const JS_EXTENSIONS = new Set(['.js', '.mjs']);
157162

158-
export function resolve(specifier, parentModuleURL/*, defaultResolve */) {
163+
const baseURL = new URL('file://');
164+
baseURL.pathname = process.cwd() + '/';
165+
166+
export function resolve(specifier, parentModuleURL = baseURL, defaultResolve) {
159167
if (builtins.includes(specifier)) {
160168
return {
161169
url: specifier,

lib/internal/loader/DefaultResolve.js

Lines changed: 16 additions & 5 deletions
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

Lines changed: 11 additions & 48 deletions
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

Lines changed: 3 additions & 3 deletions
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

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,54 @@
11
'use strict';
22

33
const {
4+
setImportModuleDynamicallyCallback,
45
setInitializeImportMetaObjectCallback
56
} = internalBinding('module_wrap');
67

8+
const { getURLFromFilePath } = require('internal/url');
9+
const Loader = require('internal/loader/Loader');
10+
const path = require('path');
11+
const { URL } = require('url');
12+
13+
function normalizeReferrerURL(referrer) {
14+
if (typeof referrer === 'string' && path.isAbsolute(referrer)) {
15+
return getURLFromFilePath(referrer).href;
16+
}
17+
return new URL(referrer).href;
18+
}
19+
720
function initializeImportMetaObject(wrap, meta) {
821
meta.url = wrap.url;
922
}
1023

11-
function setupModules() {
24+
let loaderResolve;
25+
exports.loaderPromise = new Promise((resolve, reject) => {
26+
loaderResolve = resolve;
27+
});
28+
29+
exports.ESMLoader = undefined;
30+
31+
exports.setup = function() {
1232
setInitializeImportMetaObjectCallback(initializeImportMetaObject);
13-
}
1433

15-
module.exports = {
16-
setup: setupModules
34+
let ESMLoader = new Loader();
35+
const loaderPromise = (async () => {
36+
const userLoader = process.binding('config').userLoader;
37+
if (userLoader) {
38+
const hooks = await ESMLoader.import(
39+
userLoader, getURLFromFilePath(`${process.cwd()}/`).href);
40+
ESMLoader = new Loader();
41+
ESMLoader.hook(hooks);
42+
exports.ESMLoader = ESMLoader;
43+
}
44+
return ESMLoader;
45+
})();
46+
loaderResolve(loaderPromise);
47+
48+
setImportModuleDynamicallyCallback(async (referrer, specifier) => {
49+
const loader = await loaderPromise;
50+
return loader.import(specifier, normalizeReferrerURL(referrer));
51+
});
52+
53+
exports.ESMLoader = ESMLoader;
1754
};

lib/module.js

Lines changed: 8 additions & 21 deletions
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
internalModuleReadJSON,
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);
@@ -447,7 +446,6 @@ Module._resolveLookupPaths = function(request, parent, newReturn) {
447446
return (newReturn ? parentDir : [id, parentDir]);
448447
};
449448

450-
451449
// Check the cache for the requested file.
452450
// 1. If a module already exists in the cache: return its exports object.
453451
// 2. If the module is native: call `NativeModule.require()` with the
@@ -460,22 +458,10 @@ Module._load = function(request, parent, isMain) {
460458
debug('Module._load REQUEST %s parent: %s', request, parent.id);
461459
}
462460

463-
if (isMain && experimentalModules) {
464-
(async () => {
465-
// loader setup
466-
if (!ESMLoader) {
467-
ESMLoader = new Loader();
468-
const userLoader = process.binding('config').userLoader;
469-
if (userLoader) {
470-
ESMLoader.isMain = false;
471-
const hooks = await ESMLoader.import(userLoader);
472-
ESMLoader = new Loader();
473-
ESMLoader.hook(hooks);
474-
}
475-
}
476-
Loader.registerImportDynamicallyCallback(ESMLoader);
477-
await ESMLoader.import(getURLFromFilePath(request).pathname);
478-
})()
461+
if (experimentalModules && isMain) {
462+
internalESModule.loaderPromise.then((loader) => {
463+
return loader.import(getURLFromFilePath(request).pathname);
464+
})
479465
.catch((e) => {
480466
decorateErrorStack(e);
481467
console.error(e);
@@ -578,7 +564,8 @@ Module.prototype.load = function(filename) {
578564
Module._extensions[extension](this, filename);
579565
this.loaded = true;
580566

581-
if (ESMLoader) {
567+
if (experimentalModules) {
568+
const ESMLoader = internalESModule.ESMLoader;
582569
const url = getURLFromFilePath(filename);
583570
const urlString = `${url}`;
584571
const exports = this.exports;

0 commit comments

Comments
 (0)