Skip to content

Commit bac9b17

Browse files
JakobJingleheimeraduh95GeoffreyBoothtargos
committed
esm: move hook execution to separate thread
PR-URL: #44710 Backport-PR-URL: #50669 Reviewed-By: Yagiz Nizipli <[email protected]> Reviewed-By: Geoffrey Booth <[email protected]> Co-authored-by: Antoine du Hamel <[email protected]> Co-authored-by: Geoffrey Booth <[email protected]> Co-authored-by: Michaël Zasso <[email protected]>
1 parent 272e55c commit bac9b17

37 files changed

+1007
-461
lines changed

doc/api/esm.md

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
<!-- YAML
88
added: v8.5.0
99
changes:
10+
- version: REPLACEME
11+
pr-url: https://github.com/nodejs/node/pull/44710
12+
description: Loader hooks are executed off the main thread.
1013
- version:
1114
- v18.6.0
1215
pr-url: https://github.com/nodejs/node/pull/42623
@@ -325,6 +328,9 @@ added:
325328
- v13.9.0
326329
- v12.16.2
327330
changes:
331+
- version: REPLACEME
332+
pr-url: https://github.com/nodejs/node/pull/44710
333+
description: This API now returns a string synchronously instead of a Promise.
328334
- version:
329335
- v16.2.0
330336
- v14.18.0
@@ -340,29 +346,26 @@ command flag enabled.
340346
* `specifier` {string} The module specifier to resolve relative to `parent`.
341347
* `parent` {string|URL} The absolute parent module URL to resolve from. If none
342348
is specified, the value of `import.meta.url` is used as the default.
343-
* Returns: {Promise}
349+
* Returns: {string}
344350
345351
Provides a module-relative resolution function scoped to each module, returning
346-
the URL string.
352+
the URL string. In alignment with browser behavior, this now returns
353+
synchronously.
347354
348-
<!-- eslint-skip -->
355+
> **Caveat** This can result in synchronous file-system operations, which
356+
> can impact performance similarly to `require.resolve`.
349357
350358
```js
351-
const dependencyAsset = await import.meta.resolve('component-lib/asset.css');
359+
const dependencyAsset = import.meta.resolve('component-lib/asset.css');
352360
```
353361
354362
`import.meta.resolve` also accepts a second argument which is the parent module
355-
from which to resolve from:
356-
357-
<!-- eslint-skip -->
363+
from which to resolve:
358364
359365
```js
360-
await import.meta.resolve('./dep', import.meta.url);
366+
import.meta.resolve('./dep', import.meta.url);
361367
```
362368
363-
This function is asynchronous because the ES module resolver in Node.js is
364-
allowed to be asynchronous.
365-
366369
## Interoperability with CommonJS
367370
368371
### `import` statements
@@ -729,6 +732,11 @@ A hook that returns without calling `next<hookName>()` _and_ without returning
729732
`shortCircuit: true` also triggers an exception. These errors are to help
730733
prevent unintentional breaks in the chain.
731734
735+
Hooks are run in a separate thread, isolated from the main. That means it is a
736+
different [realm](https://tc39.es/ecma262/#realm). The hooks thread may be
737+
terminated by the main thread at any time, so do not depend on asynchronous
738+
operations to (like `console.log`) complete.
739+
732740
#### `resolve(specifier, context, nextResolve)`
733741
734742
<!-- YAML
@@ -759,7 +767,7 @@ changes:
759767
Node.js default `resolve` hook after the last user-supplied `resolve` hook
760768
* `specifier` {string}
761769
* `context` {Object}
762-
* Returns: {Object}
770+
* Returns: {Object|Promise}
763771
* `format` {string|null|undefined} A hint to the load hook (it might be
764772
ignored)
765773
`'builtin' | 'commonjs' | 'json' | 'module' | 'wasm'`
@@ -769,6 +777,9 @@ changes:
769777
terminate the chain of `resolve` hooks. **Default:** `false`
770778
* `url` {string} The absolute URL to which this input resolves
771779
780+
> **Caveat** Despite support for returning promises and async functions, calls
781+
> to `resolve` may block the main thread which can impact performance.
782+
772783
The `resolve` hook chain is responsible for telling Node.js where to find and
773784
how to cache a given `import` statement or expression. It can optionally return
774785
its format (such as `'module'`) as a hint to the `load` hook. If a format is
@@ -794,7 +805,7 @@ Node.js module specifier resolution behavior_ when calling `defaultResolve`, the
794805
`context.conditions` array originally passed into the `resolve` hook.
795806
796807
```js
797-
export async function resolve(specifier, context, nextResolve) {
808+
export function resolve(specifier, context, nextResolve) {
798809
const { parentURL = null } = context;
799810

800811
if (Math.random() > 0.5) { // Some condition.
@@ -1067,6 +1078,25 @@ import CoffeeScript from 'coffeescript';
10671078
10681079
const baseURL = pathToFileURL(`${cwd()}/`).href;
10691080
1081+
// CoffeeScript files end in .coffee, .litcoffee, or .coffee.md.
1082+
const extensionsRegex = /\.coffee$|\.litcoffee$|\.coffee\.md$/;
1083+
1084+
export function resolve(specifier, context, nextResolve) {
1085+
if (extensionsRegex.test(specifier)) {
1086+
const { parentURL = baseURL } = context;
1087+
1088+
// Node.js normally errors on unknown file extensions, so return a URL for
1089+
// specifiers ending in the CoffeeScript file extensions.
1090+
return {
1091+
shortCircuit: true,
1092+
url: new URL(specifier, parentURL).href,
1093+
};
1094+
}
1095+
1096+
// Let Node.js handle all other specifiers.
1097+
return nextResolve(specifier);
1098+
}
1099+
10701100
export async function load(url, context, nextLoad) {
10711101
if (extensionsRegex.test(url)) {
10721102
// Now that we patched resolve to let CoffeeScript URLs through, we need to

lib/internal/main/worker_thread.js

Lines changed: 54 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const {
1010
ObjectDefineProperty,
1111
PromisePrototypeThen,
1212
RegExpPrototypeExec,
13+
SafeWeakMap,
1314
globalThis: {
1415
Atomics,
1516
SharedArrayBuffer,
@@ -88,23 +89,25 @@ port.on('message', (message) => {
8889
const {
8990
argv,
9091
cwdCounter,
91-
filename,
9292
doEval,
93-
workerData,
9493
environmentData,
95-
publicPort,
94+
filename,
95+
hasStdin,
9696
manifestSrc,
9797
manifestURL,
98-
hasStdin,
98+
publicPort,
99+
workerData,
99100
} = message;
100101

101-
if (argv !== undefined) {
102-
ArrayPrototypePushApply(process.argv, argv);
103-
}
102+
if (doEval !== 'internal') {
103+
if (argv !== undefined) {
104+
ArrayPrototypePushApply(process.argv, argv);
105+
}
104106

105-
const publicWorker = require('worker_threads');
106-
publicWorker.parentPort = publicPort;
107-
publicWorker.workerData = workerData;
107+
const publicWorker = require('worker_threads');
108+
publicWorker.parentPort = publicPort;
109+
publicWorker.workerData = workerData;
110+
}
108111

109112
require('internal/worker').assignEnvironmentData(environmentData);
110113

@@ -137,31 +140,47 @@ port.on('message', (message) => {
137140
debug(`[${threadId}] starts worker script ${filename} ` +
138141
`(eval = ${doEval}) at cwd = ${process.cwd()}`);
139142
port.postMessage({ type: UP_AND_RUNNING });
140-
if (doEval === 'classic') {
141-
const { evalScript } = require('internal/process/execution');
142-
const name = '[worker eval]';
143-
// This is necessary for CJS module compilation.
144-
// TODO: pass this with something really internal.
145-
ObjectDefineProperty(process, '_eval', {
146-
__proto__: null,
147-
configurable: true,
148-
enumerable: true,
149-
value: filename,
150-
});
151-
ArrayPrototypeSplice(process.argv, 1, 0, name);
152-
evalScript(name, filename);
153-
} else if (doEval === 'module') {
154-
const { evalModule } = require('internal/process/execution');
155-
PromisePrototypeThen(evalModule(filename), undefined, (e) => {
156-
workerOnGlobalUncaughtException(e, true);
157-
});
158-
} else {
159-
// script filename
160-
// runMain here might be monkey-patched by users in --require.
161-
// XXX: the monkey-patchability here should probably be deprecated.
162-
ArrayPrototypeSplice(process.argv, 1, 0, filename);
163-
const CJSLoader = require('internal/modules/cjs/loader');
164-
CJSLoader.Module.runMain(filename);
143+
switch (doEval) {
144+
case 'internal': {
145+
// Create this WeakMap in js-land because V8 has no C++ API for WeakMap.
146+
internalBinding('module_wrap').callbackMap = new SafeWeakMap();
147+
require(filename)(workerData, publicPort);
148+
break;
149+
}
150+
151+
case 'classic': {
152+
const { evalScript } = require('internal/process/execution');
153+
const name = '[worker eval]';
154+
// This is necessary for CJS module compilation.
155+
// TODO: pass this with something really internal.
156+
ObjectDefineProperty(process, '_eval', {
157+
__proto__: null,
158+
configurable: true,
159+
enumerable: true,
160+
value: filename,
161+
});
162+
ArrayPrototypeSplice(process.argv, 1, 0, name);
163+
evalScript(name, filename);
164+
break;
165+
}
166+
167+
case 'module': {
168+
const { evalModule } = require('internal/process/execution');
169+
PromisePrototypeThen(evalModule(filename), undefined, (e) => {
170+
workerOnGlobalUncaughtException(e, true);
171+
});
172+
break;
173+
}
174+
175+
default: {
176+
// script filename
177+
// runMain here might be monkey-patched by users in --require.
178+
// XXX: the monkey-patchability here should probably be deprecated.
179+
ArrayPrototypeSplice(process.argv, 1, 0, filename);
180+
const CJSLoader = require('internal/modules/cjs/loader');
181+
CJSLoader.Module.runMain(filename);
182+
break;
183+
}
165184
}
166185
} else if (message.type === STDIO_PAYLOAD) {
167186
const { stream, chunks } = message;

0 commit comments

Comments
 (0)