Skip to content

Commit 90b634a

Browse files
esm: convert resolve hook to synchronous
PR-URL: #43363 Reviewed-By: Antoine du Hamel <[email protected]> Reviewed-By: Geoffrey Booth <[email protected]>
1 parent 1737c77 commit 90b634a

30 files changed

+216
-127
lines changed

doc/api/esm.md

+21-8
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,9 @@ added:
324324
- v13.9.0
325325
- v12.16.2
326326
changes:
327+
- version: REPLACEME
328+
pr-url: https://github.com/nodejs/node/pull/43363
329+
description: Convert from asynchronous to synchronous.
327330
- version:
328331
- v16.2.0
329332
- v14.18.0
@@ -339,15 +342,19 @@ command flag enabled.
339342
* `specifier` {string} The module specifier to resolve relative to `parent`.
340343
* `parent` {string|URL} The absolute parent module URL to resolve from. If none
341344
is specified, the value of `import.meta.url` is used as the default.
342-
* Returns: {Promise}
345+
* Returns: {string}
343346
344347
Provides a module-relative resolution function scoped to each module, returning
345-
the URL string.
348+
the URL string. In alignment with browser behavior, this now returns
349+
synchronously.
350+
351+
> **Caveat** This can result in synchronous file-system operations, which
352+
> can impact performance similarly to `require.resolve`.
346353
347354
<!-- eslint-skip -->
348355
349356
```js
350-
const dependencyAsset = await import.meta.resolve('component-lib/asset.css');
357+
const dependencyAsset = import.meta.resolve('component-lib/asset.css');
351358
```
352359
353360
`import.meta.resolve` also accepts a second argument which is the parent module
@@ -356,11 +363,11 @@ from which to resolve from:
356363
<!-- eslint-skip -->
357364
358365
```js
359-
await import.meta.resolve('./dep', import.meta.url);
366+
import.meta.resolve('./dep', import.meta.url);
360367
```
361368
362-
This function is asynchronous because the ES module resolver in Node.js is
363-
allowed to be asynchronous.
369+
This function is synchronous because the ES module resolver in Node.js is
370+
synchronous.
364371
365372
## Interoperability with CommonJS
366373
@@ -731,6 +738,9 @@ prevent unintentional breaks in the chain.
731738
732739
<!-- YAML
733740
changes:
741+
- version: REPLACEME
742+
pr-url: https://github.com/nodejs/node/pull/43363
743+
description: Convert hook from asynchronous to synchronous.
734744
- version: REPLACEME
735745
pr-url: https://github.com/nodejs/node/pull/42623
736746
description: Add support for chaining resolve hooks. Each hook must either
@@ -764,6 +774,9 @@ changes:
764774
terminate the chain of `resolve` hooks. **Default:** `false`
765775
* `url` {string} The absolute URL to which this input resolves
766776
777+
> **Caveat** A resolve hook can contain synchronous file-system operations
778+
> (as `defaultResolveHook()` does), which can impact performance.
779+
767780
The `resolve` hook chain is responsible for resolving file URL for a given
768781
module specifier and parent URL, and optionally its format (such as `'module'`)
769782
as a hint to the `load` hook. If a format is specified, the `load` hook is
@@ -790,7 +803,7 @@ Node.js module specifier resolution behavior_ when calling `defaultResolve`, the
790803
`context.conditions` array originally passed into the `resolve` hook.
791804
792805
```js
793-
export async function resolve(specifier, context, nextResolve) {
806+
export function resolve(specifier, context, nextResolve) {
794807
const { parentURL = null } = context;
795808

796809
if (Math.random() > 0.5) { // Some condition.
@@ -1089,7 +1102,7 @@ const baseURL = pathToFileURL(`${cwd()}/`).href;
10891102
// CoffeeScript files end in .coffee, .litcoffee, or .coffee.md.
10901103
const extensionsRegex = /\.coffee$|\.litcoffee$|\.coffee\.md$/;
10911104
1092-
export async function resolve(specifier, context, nextResolve) {
1105+
export function resolve(specifier, context, nextResolve) {
10931106
if (extensionsRegex.test(specifier)) {
10941107
const { parentURL = baseURL } = context;
10951108

lib/internal/modules/esm/initialize_import_meta.js

+14-12
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,23 @@
33
const { getOptionValue } = require('internal/options');
44
const experimentalImportMetaResolve =
55
getOptionValue('--experimental-import-meta-resolve');
6-
const {
7-
PromisePrototypeThen,
8-
PromiseReject,
9-
} = primordials;
106
const asyncESM = require('internal/process/esm_loader');
117

128
function createImportMetaResolve(defaultParentUrl) {
13-
return async function resolve(specifier, parentUrl = defaultParentUrl) {
14-
return PromisePrototypeThen(
15-
asyncESM.esmLoader.resolve(specifier, parentUrl),
16-
({ url }) => url,
17-
(error) => (
18-
error.code === 'ERR_UNSUPPORTED_DIR_IMPORT' ?
19-
error.url : PromiseReject(error))
20-
);
9+
return function resolve(specifier, parentUrl = defaultParentUrl) {
10+
let url;
11+
12+
try {
13+
({ url } = asyncESM.esmLoader.resolve(specifier, parentUrl));
14+
} catch (error) {
15+
if (error.code === 'ERR_UNSUPPORTED_DIR_IMPORT') {
16+
({ url } = error);
17+
} else {
18+
throw error;
19+
}
20+
}
21+
22+
return url;
2123
};
2224
}
2325

lib/internal/modules/esm/loader.js

+64-31
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ const {
1515
ObjectDefineProperty,
1616
ObjectSetPrototypeOf,
1717
PromiseAll,
18+
PromiseResolve,
19+
PromisePrototypeThen,
1820
ReflectApply,
1921
RegExpPrototypeExec,
2022
SafeArrayIterator,
@@ -109,12 +111,14 @@ let emittedSpecifierResolutionWarning = false;
109111
* position in the hook chain.
110112
* @param {string} meta.hookName The kind of hook the chain is (ex 'resolve')
111113
* @param {boolean} meta.shortCircuited Whether a hook signaled a short-circuit.
112-
* @param {(hookErrIdentifier, hookArgs) => void} validate A wrapper function
114+
* @param {object} validators A wrapper function
113115
* containing all validation of a custom loader hook's intermediary output. Any
114116
* validation within MUST throw.
117+
* @param {(hookErrIdentifier, hookArgs) => void} validators.validateArgs
118+
* @param {(hookErrIdentifier, output) => void} validators.validateOutput
115119
* @returns {function next<HookName>(...hookArgs)} The next hook in the chain.
116120
*/
117-
function nextHookFactory(chain, meta, validate) {
121+
function nextHookFactory(chain, meta, { validateArgs, validateOutput }) {
118122
// First, prepare the current
119123
const { hookName } = meta;
120124
const {
@@ -137,7 +141,7 @@ function nextHookFactory(chain, meta, validate) {
137141
// factory generates the next link in the chain.
138142
meta.hookIndex--;
139143

140-
nextNextHook = nextHookFactory(chain, meta, validate);
144+
nextNextHook = nextHookFactory(chain, meta, { validateArgs, validateOutput });
141145
} else {
142146
// eslint-disable-next-line func-name-matching
143147
nextNextHook = function chainAdvancedTooFar() {
@@ -148,21 +152,36 @@ function nextHookFactory(chain, meta, validate) {
148152
}
149153

150154
return ObjectDefineProperty(
151-
async (...args) => {
155+
(...args) => {
152156
// Update only when hook is invoked to avoid fingering the wrong filePath
153157
meta.hookErrIdentifier = `${hookFilePath} '${hookName}'`;
154158

155-
validate(`${meta.hookErrIdentifier} hook's ${nextHookName}()`, args);
159+
validateArgs(`${meta.hookErrIdentifier} hook's ${nextHookName}()`, args);
160+
161+
const outputErrIdentifier = `${chain[generatedHookIndex].url} '${hookName}' hook's ${nextHookName}()`;
156162

157163
// Set when next<HookName> is actually called, not just generated.
158164
if (generatedHookIndex === 0) { meta.chainFinished = true; }
159165

160166
ArrayPrototypePush(args, nextNextHook);
161-
const output = await ReflectApply(hook, undefined, args);
167+
const output = ReflectApply(hook, undefined, args);
168+
169+
validateOutput(outputErrIdentifier, output);
162170

163-
if (output?.shortCircuit === true) { meta.shortCircuited = true; }
164-
return output;
171+
function checkShortCircuited(output) {
172+
if (output?.shortCircuit === true) { meta.shortCircuited = true; }
173+
174+
return output;
175+
}
176+
177+
if (meta.isChainAsync) {
178+
return PromisePrototypeThen(
179+
PromiseResolve(output),
180+
checkShortCircuited,
181+
);
182+
}
165183

184+
return checkShortCircuited(output);
166185
},
167186
'name',
168187
{ __proto__: null, value: nextHookName },
@@ -421,8 +440,11 @@ class ESMLoader {
421440
);
422441
}
423442

424-
const { format, url } =
425-
await this.resolve(specifier, parentURL, importAssertionsForResolve);
443+
const { format, url } = this.resolve(
444+
specifier,
445+
parentURL,
446+
importAssertionsForResolve,
447+
);
426448

427449
let job = this.moduleMap.get(url, importAssertions.type);
428450

@@ -557,10 +579,11 @@ class ESMLoader {
557579
hookErrIdentifier: '',
558580
hookIndex: chain.length - 1,
559581
hookName: 'load',
582+
isChainAsync: true,
560583
shortCircuited: false,
561584
};
562585

563-
const validate = (hookErrIdentifier, { 0: nextUrl, 1: ctx }) => {
586+
const validateArgs = (hookErrIdentifier, { 0: nextUrl, 1: ctx }) => {
564587
if (typeof nextUrl !== 'string') {
565588
// non-strings can be coerced to a url string
566589
// validateString() throws a less-specific error
@@ -586,19 +609,22 @@ class ESMLoader {
586609

587610
validateObject(ctx, `${hookErrIdentifier} context`);
588611
};
612+
const validateOutput = (hookErrIdentifier, output) => {
613+
if (typeof output !== 'object') { // [2]
614+
throw new ERR_INVALID_RETURN_VALUE(
615+
'an object',
616+
hookErrIdentifier,
617+
output,
618+
);
619+
}
620+
};
589621

590-
const nextLoad = nextHookFactory(chain, meta, validate);
622+
const nextLoad = nextHookFactory(chain, meta, { validateArgs, validateOutput });
591623

592624
const loaded = await nextLoad(url, context);
593625
const { hookErrIdentifier } = meta; // Retrieve the value after all settled
594626

595-
if (typeof loaded !== 'object') { // [2]
596-
throw new ERR_INVALID_RETURN_VALUE(
597-
'an object',
598-
hookErrIdentifier,
599-
loaded,
600-
);
601-
}
627+
validateOutput(hookErrIdentifier, loaded);
602628

603629
if (loaded?.shortCircuit === true) { meta.shortCircuited = true; }
604630

@@ -778,7 +804,7 @@ class ESMLoader {
778804
* statement or expression.
779805
* @returns {{ format: string, url: URL['href'] }}
780806
*/
781-
async resolve(
807+
resolve(
782808
originalSpecifier,
783809
parentURL,
784810
importAssertions = ObjectCreate(null)
@@ -802,36 +828,43 @@ class ESMLoader {
802828
hookErrIdentifier: '',
803829
hookIndex: chain.length - 1,
804830
hookName: 'resolve',
831+
isChainAsync: false,
805832
shortCircuited: false,
806833
};
807-
808834
const context = {
809835
conditions: DEFAULT_CONDITIONS,
810836
importAssertions,
811837
parentURL,
812838
};
813-
const validate = (hookErrIdentifier, { 0: suppliedSpecifier, 1: ctx }) => {
814839

840+
const validateArgs = (hookErrIdentifier, { 0: suppliedSpecifier, 1: ctx }) => {
815841
validateString(
816842
suppliedSpecifier,
817843
`${hookErrIdentifier} specifier`,
818844
); // non-strings can be coerced to a url string
819845

820846
validateObject(ctx, `${hookErrIdentifier} context`);
821847
};
848+
const validateOutput = (hookErrIdentifier, output) => {
849+
if (
850+
typeof output !== 'object' || // [2]
851+
output === null ||
852+
(output.url == null && typeof output.then === 'function')
853+
) {
854+
throw new ERR_INVALID_RETURN_VALUE(
855+
'an object',
856+
hookErrIdentifier,
857+
output,
858+
);
859+
}
860+
};
822861

823-
const nextResolve = nextHookFactory(chain, meta, validate);
862+
const nextResolve = nextHookFactory(chain, meta, { validateArgs, validateOutput });
824863

825-
const resolution = await nextResolve(originalSpecifier, context);
864+
const resolution = nextResolve(originalSpecifier, context);
826865
const { hookErrIdentifier } = meta; // Retrieve the value after all settled
827866

828-
if (typeof resolution !== 'object') { // [2]
829-
throw new ERR_INVALID_RETURN_VALUE(
830-
'an object',
831-
hookErrIdentifier,
832-
resolution,
833-
);
834-
}
867+
validateOutput(hookErrIdentifier, resolution);
835868

836869
if (resolution?.shortCircuit === true) { meta.shortCircuited = true; }
837870

lib/internal/modules/esm/resolve.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -1081,7 +1081,7 @@ function throwIfUnsupportedURLScheme(parsed, experimentalNetworkImports) {
10811081
}
10821082
}
10831083

1084-
async function defaultResolve(specifier, context = {}) {
1084+
function defaultResolve(specifier, context = {}) {
10851085
let { parentURL, conditions } = context;
10861086
if (parentURL && policy?.manifest) {
10871087
const redirects = policy.manifest.getDependencyMapper(parentURL);
@@ -1227,11 +1227,11 @@ const {
12271227

12281228
if (policy) {
12291229
const $defaultResolve = defaultResolve;
1230-
module.exports.defaultResolve = async function defaultResolve(
1230+
module.exports.defaultResolve = function defaultResolve(
12311231
specifier,
12321232
context
12331233
) {
1234-
const ret = await $defaultResolve(specifier, context, $defaultResolve);
1234+
const ret = $defaultResolve(specifier, context);
12351235
// This is a preflight check to avoid data exfiltration by query params etc.
12361236
policy.manifest.mightAllow(ret.url, () =>
12371237
new ERR_MANIFEST_DEPENDENCY_MISSING(

0 commit comments

Comments
 (0)