Skip to content

Commit 1a6e945

Browse files
aduh95RafaelGSS
authored andcommitted
module: make CJS load from ESM loader
PR-URL: #47999 Reviewed-By: Geoffrey Booth <[email protected]> Reviewed-By: Jacob Smith <[email protected]>
1 parent 13bd7a0 commit 1a6e945

10 files changed

+426
-105
lines changed

doc/api/esm.md

+38-12
Original file line numberDiff line numberDiff line change
@@ -908,6 +908,9 @@ export function resolve(specifier, context, nextResolve) {
908908
909909
<!-- YAML
910910
changes:
911+
- version: REPLACEME
912+
pr-url: https://github.com/nodejs/node/pull/47999
913+
description: Add support for `source` with format `commonjs`.
911914
- version:
912915
- v18.6.0
913916
- v16.17.0
@@ -945,20 +948,43 @@ validating the import assertion.
945948
946949
The final value of `format` must be one of the following:
947950
948-
| `format` | Description | Acceptable types for `source` returned by `load` |
949-
| ------------ | ------------------------------ | ----------------------------------------------------- |
950-
| `'builtin'` | Load a Node.js builtin module | Not applicable |
951-
| `'commonjs'` | Load a Node.js CommonJS module | Not applicable |
952-
| `'json'` | Load a JSON file | { [`string`][], [`ArrayBuffer`][], [`TypedArray`][] } |
953-
| `'module'` | Load an ES module | { [`string`][], [`ArrayBuffer`][], [`TypedArray`][] } |
954-
| `'wasm'` | Load a WebAssembly module | { [`ArrayBuffer`][], [`TypedArray`][] } |
951+
| `format` | Description | Acceptable types for `source` returned by `load` |
952+
| ------------ | ------------------------------ | -------------------------------------------------------------------------- |
953+
| `'builtin'` | Load a Node.js builtin module | Not applicable |
954+
| `'commonjs'` | Load a Node.js CommonJS module | { [`string`][], [`ArrayBuffer`][], [`TypedArray`][], `null`, `undefined` } |
955+
| `'json'` | Load a JSON file | { [`string`][], [`ArrayBuffer`][], [`TypedArray`][] } |
956+
| `'module'` | Load an ES module | { [`string`][], [`ArrayBuffer`][], [`TypedArray`][] } |
957+
| `'wasm'` | Load a WebAssembly module | { [`ArrayBuffer`][], [`TypedArray`][] } |
955958
956959
The value of `source` is ignored for type `'builtin'` because currently it is
957-
not possible to replace the value of a Node.js builtin (core) module. The value
958-
of `source` is ignored for type `'commonjs'` because the CommonJS module loader
959-
does not provide a mechanism for the ES module loader to override the
960-
[CommonJS module return value](#commonjs-namespaces). This limitation might be
961-
overcome in the future.
960+
not possible to replace the value of a Node.js builtin (core) module.
961+
962+
The value of `source` can be omitted for type `'commonjs'`. When a `source` is
963+
provided, all `require` calls from this module will be processed by the ESM
964+
loader with registered `resolve` and `load` hooks; all `require.resolve` calls
965+
from this module will be processed by the ESM loader with registered `resolve`
966+
hooks; `require.extensions` and monkey-patching on the CommonJS module loader
967+
will not apply. If `source` is undefined or `null`, it will be handled by the
968+
CommonJS module loader and `require`/`require.resolve` calls will not go through
969+
the registered hooks. This behavior for nullish `source` is temporary — in the
970+
future, nullish `source` will not be supported.
971+
972+
The Node.js own `load` implementation, which is the value of `next` for the last
973+
loader in the `load` chain, returns `null` for `source` when `format` is
974+
`'commonjs'` for backward compatibility. Here is an example loader that would
975+
opt-in to using the non-default behavior:
976+
977+
```js
978+
import { readFile } from 'node:fs/promises';
979+
980+
export async function load(url, context, nextLoad) {
981+
const result = await nextLoad(url, context);
982+
if (result.format === 'commonjs') {
983+
result.source ??= await readFile(new URL(result.responseURL ?? url));
984+
}
985+
return result;
986+
}
987+
```
962988
963989
> **Caveat**: The ESM `load` hook and namespaced exports from CommonJS modules
964990
> are incompatible. Attempting to use them together will result in an empty

lib/internal/modules/esm/load.js

+72-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const { kEmptyObject } = require('internal/util');
1010
const { defaultGetFormat } = require('internal/modules/esm/get_format');
1111
const { validateAssertions } = require('internal/modules/esm/assert');
1212
const { getOptionValue } = require('internal/options');
13+
const { readFileSync } = require('fs');
1314

1415
// Do not eagerly grab .manifest, it may be in TDZ
1516
const policy = getOptionValue('--experimental-policy') ?
@@ -69,12 +70,35 @@ async function getSource(url, context) {
6970
return { __proto__: null, responseURL, source };
7071
}
7172

73+
function getSourceSync(url, context) {
74+
const parsed = new URL(url);
75+
const responseURL = url;
76+
let source;
77+
if (parsed.protocol === 'file:') {
78+
source = readFileSync(parsed);
79+
} else if (parsed.protocol === 'data:') {
80+
const match = RegExpPrototypeExec(DATA_URL_PATTERN, parsed.pathname);
81+
if (!match) {
82+
throw new ERR_INVALID_URL(url);
83+
}
84+
const { 1: base64, 2: body } = match;
85+
source = BufferFrom(decodeURIComponent(body), base64 ? 'base64' : 'utf8');
86+
} else {
87+
const supportedSchemes = ['file', 'data'];
88+
throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(parsed, supportedSchemes);
89+
}
90+
if (policy?.manifest) {
91+
policy.manifest.assertIntegrity(parsed, source);
92+
}
93+
return { __proto__: null, responseURL, source };
94+
}
95+
7296

7397
/**
7498
* Node.js default load hook.
7599
* @param {string} url
76-
* @param {object} context
77-
* @returns {object}
100+
* @param {LoadContext} context
101+
* @returns {LoadReturn}
78102
*/
79103
async function defaultLoad(url, context = kEmptyObject) {
80104
let responseURL = url;
@@ -108,6 +132,51 @@ async function defaultLoad(url, context = kEmptyObject) {
108132
source,
109133
};
110134
}
135+
/**
136+
* @typedef LoadContext
137+
* @property {string} [format] A hint (possibly returned from `resolve`)
138+
* @property {string | Buffer | ArrayBuffer} [source] source
139+
* @property {Record<string, string>} [importAssertions] import attributes
140+
*/
141+
142+
/**
143+
* @typedef LoadReturn
144+
* @property {string} format format
145+
* @property {URL['href']} responseURL The module's fully resolved URL
146+
* @property {Buffer} source source
147+
*/
148+
149+
/**
150+
* @param {URL['href']} url
151+
* @param {LoadContext} [context]
152+
* @returns {LoadReturn}
153+
*/
154+
function defaultLoadSync(url, context = kEmptyObject) {
155+
let responseURL = url;
156+
const { importAssertions } = context;
157+
let {
158+
format,
159+
source,
160+
} = context;
161+
162+
format ??= defaultGetFormat(new URL(url), context);
163+
164+
validateAssertions(url, format, importAssertions);
165+
166+
if (format === 'builtin') {
167+
source = null;
168+
} else if (source == null) {
169+
({ responseURL, source } = getSourceSync(url, context));
170+
}
171+
172+
return {
173+
__proto__: null,
174+
format,
175+
responseURL,
176+
source,
177+
};
178+
}
179+
111180

112181
/**
113182
* throws an error if the protocol is not one of the protocols
@@ -160,5 +229,6 @@ function throwUnknownModuleFormat(url, format) {
160229

161230
module.exports = {
162231
defaultLoad,
232+
defaultLoadSync,
163233
throwUnknownModuleFormat,
164234
};

lib/internal/modules/esm/loader.js

+37-14
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const {
1010
} = primordials;
1111

1212
const {
13+
ERR_REQUIRE_ESM,
1314
ERR_UNKNOWN_MODULE_FORMAT,
1415
} = require('internal/errors').codes;
1516
const { getOptionValue } = require('internal/options');
@@ -18,7 +19,7 @@ const { emitExperimentalWarning } = require('internal/util');
1819
const {
1920
getDefaultConditions,
2021
} = require('internal/modules/esm/utils');
21-
let defaultResolve, defaultLoad, importMetaInitializer;
22+
let defaultResolve, defaultLoad, defaultLoadSync, importMetaInitializer;
2223

2324
function newResolveCache() {
2425
const { ResolveCache } = require('internal/modules/esm/module_map');
@@ -220,7 +221,12 @@ class ModuleLoader {
220221
return this.getJobFromResolveResult(resolveResult, parentURL, importAssertions);
221222
}
222223

223-
getJobFromResolveResult(resolveResult, parentURL, importAssertions) {
224+
getModuleJobSync(specifier, parentURL, importAssertions) {
225+
const resolveResult = this.resolveSync(specifier, parentURL, importAssertions);
226+
return this.getJobFromResolveResult(resolveResult, parentURL, importAssertions, true);
227+
}
228+
229+
getJobFromResolveResult(resolveResult, parentURL, importAssertions, sync) {
224230
const { url, format } = resolveResult;
225231
const resolvedImportAssertions = resolveResult.importAssertions ?? importAssertions;
226232
let job = this.loadCache.get(url, resolvedImportAssertions.type);
@@ -231,7 +237,7 @@ class ModuleLoader {
231237
}
232238

233239
if (job === undefined) {
234-
job = this.#createModuleJob(url, resolvedImportAssertions, parentURL, format);
240+
job = this.#createModuleJob(url, resolvedImportAssertions, parentURL, format, sync);
235241
}
236242

237243
return job;
@@ -248,17 +254,8 @@ class ModuleLoader {
248254
* `resolve` hook
249255
* @returns {Promise<ModuleJob>} The (possibly pending) module job
250256
*/
251-
#createModuleJob(url, importAssertions, parentURL, format) {
252-
const moduleProvider = async (url, isMain) => {
253-
const {
254-
format: finalFormat,
255-
responseURL,
256-
source,
257-
} = await this.load(url, {
258-
format,
259-
importAssertions,
260-
});
261-
257+
#createModuleJob(url, importAssertions, parentURL, format, sync) {
258+
const callTranslator = ({ format: finalFormat, responseURL, source }, isMain) => {
262259
const translator = getTranslators().get(finalFormat);
263260

264261
if (!translator) {
@@ -267,6 +264,10 @@ class ModuleLoader {
267264

268265
return FunctionPrototypeCall(translator, this, responseURL, source, isMain);
269266
};
267+
const context = { format, importAssertions };
268+
const moduleProvider = sync ?
269+
(url, isMain) => callTranslator(this.loadSync(url, context), isMain) :
270+
async (url, isMain) => callTranslator(await this.load(url, context), isMain);
270271

271272
const inspectBrk = (
272273
parentURL === undefined &&
@@ -285,6 +286,7 @@ class ModuleLoader {
285286
moduleProvider,
286287
parentURL === undefined,
287288
inspectBrk,
289+
sync,
288290
);
289291

290292
this.loadCache.set(url, importAssertions.type, job);
@@ -388,6 +390,24 @@ class ModuleLoader {
388390
return result;
389391
}
390392

393+
loadSync(url, context) {
394+
defaultLoadSync ??= require('internal/modules/esm/load').defaultLoadSync;
395+
396+
let result = this.#customizations ?
397+
this.#customizations.loadSync(url, context) :
398+
defaultLoadSync(url, context);
399+
let format = result?.format;
400+
if (format === 'module') {
401+
throw new ERR_REQUIRE_ESM(url, true);
402+
}
403+
if (format === 'commonjs') {
404+
format = 'require-commonjs';
405+
result = { __proto__: result, format };
406+
}
407+
this.validateLoadResult(url, format);
408+
return result;
409+
}
410+
391411
validateLoadResult(url, format) {
392412
if (format == null) {
393413
require('internal/modules/esm/load').throwUnknownModuleFormat(url, format);
@@ -465,6 +485,9 @@ class CustomizedModuleLoader {
465485
load(url, context) {
466486
return hooksProxy.makeAsyncRequest('load', undefined, url, context);
467487
}
488+
loadSync(url, context) {
489+
return hooksProxy.makeSyncRequest('load', undefined, url, context);
490+
}
468491

469492
importMetaInitialize(meta, context, loader) {
470493
hooksProxy.importMetaInitialize(meta, context, loader);

lib/internal/modules/esm/module_job.js

+24-1
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,26 @@ class ModuleJob {
5151
// `loader` is the Loader instance used for loading dependencies.
5252
// `moduleProvider` is a function
5353
constructor(loader, url, importAssertions = { __proto__: null },
54-
moduleProvider, isMain, inspectBrk) {
54+
moduleProvider, isMain, inspectBrk, sync = false) {
5555
this.loader = loader;
5656
this.importAssertions = importAssertions;
5757
this.isMain = isMain;
5858
this.inspectBrk = inspectBrk;
5959

60+
this.url = url;
61+
6062
this.module = undefined;
6163
// Expose the promise to the ModuleWrap directly for linking below.
6264
// `this.module` is also filled in below.
6365
this.modulePromise = ReflectApply(moduleProvider, loader, [url, isMain]);
6466

67+
if (sync) {
68+
this.module = this.modulePromise;
69+
this.modulePromise = PromiseResolve(this.module);
70+
} else {
71+
this.modulePromise = PromiseResolve(this.modulePromise);
72+
}
73+
6574
// Wait for the ModuleWrap instance being linked with all dependencies.
6675
const link = async () => {
6776
this.module = await this.modulePromise;
@@ -186,6 +195,20 @@ class ModuleJob {
186195
}
187196
}
188197

198+
runSync() {
199+
assert(this.module instanceof ModuleWrap);
200+
if (this.instantiated !== undefined) {
201+
return { __proto__: null, module: this.module };
202+
}
203+
204+
this.module.instantiate();
205+
this.instantiated = PromiseResolve();
206+
const timeout = -1;
207+
const breakOnSigint = false;
208+
this.module.evaluate(timeout, breakOnSigint);
209+
return { __proto__: null, module: this.module };
210+
}
211+
189212
async run() {
190213
await this.instantiate();
191214
const timeout = -1;

0 commit comments

Comments
 (0)