Skip to content

Commit 336109f

Browse files
sercheraduh95GeoffreyBoothtargos
committed
esm: move hook execution to separate thread
PR-URL: nodejs/node#44710 Backport-PR-URL: nodejs/node#50669 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com> Co-authored-by: Antoine du Hamel <duhamelantoine1995@gmail.com> Co-authored-by: Geoffrey Booth <webadmin@geoffreybooth.com> Co-authored-by: Michaël Zasso <targos@protonmail.com>
1 parent af92d08 commit 336109f

37 files changed

Lines changed: 1007 additions & 461 deletions

graal-nodejs/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

graal-nodejs/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,
@@ -97,23 +98,25 @@ port.on('message', (message) => {
9798
const {
9899
argv,
99100
cwdCounter,
100-
filename,
101101
doEval,
102-
workerData,
103102
environmentData,
104-
publicPort,
103+
filename,
104+
hasStdin,
105105
manifestSrc,
106106
manifestURL,
107-
hasStdin,
107+
publicPort,
108+
workerData,
108109
} = message;
109110

110-
if (argv !== undefined) {
111-
ArrayPrototypePushApply(process.argv, argv);
112-
}
111+
if (doEval !== 'internal') {
112+
if (argv !== undefined) {
113+
ArrayPrototypePushApply(process.argv, argv);
114+
}
113115

114-
const publicWorker = require('worker_threads');
115-
publicWorker.parentPort = publicPort;
116-
publicWorker.workerData = workerData;
116+
const publicWorker = require('worker_threads');
117+
publicWorker.parentPort = publicPort;
118+
publicWorker.workerData = workerData;
119+
}
117120

118121
require('internal/worker').assignEnvironmentData(environmentData);
119122

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

0 commit comments

Comments
 (0)