Skip to content

Commit f9dc1ea

Browse files
joyeecheungmarco-ippolito
authored andcommitted
module: add __esModule to require()'d ESM
Tooling in the ecosystem have been using the __esModule property to recognize transpiled ESM in consuming code. For example, a 'log' package written in ESM: export function log(val) { console.log(val); } Can be transpiled as: exports.__esModule = true; exports.default = function log(val) { console.log(val); } The consuming code may be written like this in ESM: import log from 'log' Which gets transpiled to: const _mod = require('log'); const log = _mod.__esModule ? _mod.default : _mod; So to allow transpiled consuming code to recognize require()'d real ESM as ESM and pick up the default exports, we add a __esModule property by building a source text module facade for any module that has a default export and add .__esModule = true to the exports. We don't do this to modules that don't have default exports to avoid the unnecessary overhead. This maintains the enumerability of the re-exported names and the live binding of the exports. The source of the facade is defined as a constant per-isolate property required_module_facade_source_string, which looks like this export * from 'original'; export { default } from 'original'; export const __esModule = true; And the 'original' module request is always resolved by createRequiredModuleFacade() to wrap which is a ModuleWrap wrapping over the original module. PR-URL: #52166 Backport-PR-URL: #56927 Refs: #52134 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Filip Skokan <panva.ip@gmail.com> Reviewed-By: Chengzhong Wu <legendecas@gmail.com> Reviewed-By: Guy Bedford <guybedford@gmail.com> Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com> Refs: #52697
1 parent 18593b7 commit f9dc1ea

30 files changed

+316
-55
lines changed

doc/api/modules.md

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -195,33 +195,51 @@ loaded by `require()` meets the following requirements:
195195
`"type": "commonjs"`, and `--experimental-detect-module` is enabled.
196196

197197
`require()` will load the requested module as an ES Module, and return
198-
the module name space object. In this case it is similar to dynamic
198+
the module namespace object. In this case it is similar to dynamic
199199
`import()` but is run synchronously and returns the name space object
200200
directly.
201201

202+
With the following ES Modules:
203+
202204
```mjs
203-
// point.mjs
205+
// distance.mjs
204206
export function distance(a, b) { return (b.x - a.x) ** 2 + (b.y - a.y) ** 2; }
207+
```
208+
209+
```mjs
210+
// point.mjs
205211
class Point {
206212
constructor(x, y) { this.x = x; this.y = y; }
207213
}
208214
export default Point;
209215
```
210216

217+
A CommonJS module can load them with `require()` under `--experimental-detect-module`:
218+
211219
```cjs
212-
const required = require('./point.mjs');
220+
const distance = require('./distance.mjs');
221+
console.log(distance);
213222
// [Module: null prototype] {
214-
// default: [class Point],
215223
// distance: [Function: distance]
216224
// }
217-
console.log(required);
218225

219-
(async () => {
220-
const imported = await import('./point.mjs');
221-
console.log(imported === required); // true
222-
})();
226+
const point = require('./point.mjs');
227+
console.log(point);
228+
// [Module: null prototype] {
229+
// default: [class Point],
230+
// __esModule: true,
231+
// }
223232
```
224233

234+
For interoperability with existing tools that convert ES Modules into CommonJS,
235+
which could then load real ES Modules through `require()`, the returned namespace
236+
would contain a `__esModule: true` property if it has a `default` export so that
237+
consuming code generated by tools can recognize the default exports in real
238+
ES Modules. If the namespace already defines `__esModule`, this would not be added.
239+
This property is experimental and can change in the future. It should only be used
240+
by tools converting ES modules into CommonJS modules, following existing ecosystem
241+
conventions. Code authored directly in CommonJS should avoid depending on it.
242+
225243
If the module being `require()`'d contains top-level `await`, or the module
226244
graph it `import`s contains top-level `await`,
227245
[`ERR_REQUIRE_ASYNC_MODULE`][] will be thrown. In this case, users should

lib/internal/modules/cjs/loader.js

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const {
4141
ObjectFreeze,
4242
ObjectGetOwnPropertyDescriptor,
4343
ObjectGetPrototypeOf,
44+
ObjectHasOwn,
4445
ObjectKeys,
4546
ObjectPrototype,
4647
ObjectPrototypeHasOwnProperty,
@@ -71,7 +72,7 @@ const {
7172
},
7273
} = internalBinding('util');
7374

74-
const { kEvaluated } = internalBinding('module_wrap');
75+
const { kEvaluated, createRequiredModuleFacade } = internalBinding('module_wrap');
7576

7677
// Internal properties for Module instances.
7778
/**
@@ -1340,9 +1341,55 @@ function loadESMFromCJS(mod, filename) {
13401341
// ESM won't be accessible via process.mainModule.
13411342
setOwnProperty(process, 'mainModule', undefined);
13421343
} else {
1343-
// TODO(joyeecheung): we may want to invent optional special handling for default exports here.
1344-
// For now, it's good enough to be identical to what `import()` returns.
1345-
mod.exports = cascadedLoader.importSyncForRequire(mod, filename, source, isMain, mod[kModuleParent]);
1344+
const {
1345+
wrap,
1346+
namespace,
1347+
} = cascadedLoader.importSyncForRequire(mod, filename, source, isMain, mod[kModuleParent]);
1348+
// Tooling in the ecosystem have been using the __esModule property to recognize
1349+
// transpiled ESM in consuming code. For example, a 'log' package written in ESM:
1350+
//
1351+
// export default function log(val) { console.log(val); }
1352+
//
1353+
// Can be transpiled as:
1354+
//
1355+
// exports.__esModule = true;
1356+
// exports.default = function log(val) { console.log(val); }
1357+
//
1358+
// The consuming code may be written like this in ESM:
1359+
//
1360+
// import log from 'log'
1361+
//
1362+
// Which gets transpiled to:
1363+
//
1364+
// const _mod = require('log');
1365+
// const log = _mod.__esModule ? _mod.default : _mod;
1366+
//
1367+
// So to allow transpiled consuming code to recognize require()'d real ESM
1368+
// as ESM and pick up the default exports, we add a __esModule property by
1369+
// building a source text module facade for any module that has a default
1370+
// export and add .__esModule = true to the exports. This maintains the
1371+
// enumerability of the re-exported names and the live binding of the exports,
1372+
// without incurring a non-trivial per-access overhead on the exports.
1373+
//
1374+
// The source of the facade is defined as a constant per-isolate property
1375+
// required_module_default_facade_source_string, which looks like this
1376+
//
1377+
// export * from 'original';
1378+
// export { default } from 'original';
1379+
// export const __esModule = true;
1380+
//
1381+
// And the 'original' module request is always resolved by
1382+
// createRequiredModuleFacade() to `wrap` which is a ModuleWrap wrapping
1383+
// over the original module.
1384+
1385+
// We don't do this to modules that don't have default exports to avoid
1386+
// the unnecessary overhead. If __esModule is already defined, we will
1387+
// also skip the extension to allow users to override it.
1388+
if (!ObjectHasOwn(namespace, 'default') || ObjectHasOwn(namespace, '__esModule')) {
1389+
mod.exports = namespace;
1390+
} else {
1391+
mod.exports = createRequiredModuleFacade(wrap);
1392+
}
13461393
}
13471394
}
13481395

lib/internal/modules/esm/loader.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ class ModuleLoader {
269269
* @param {string} source Source code. TODO(joyeecheung): pass the raw buffer.
270270
* @param {string} isMain Whether this module is a main module.
271271
* @param {CJSModule|undefined} parent Parent module, if any.
272-
* @returns {{ModuleWrap}}
272+
* @returns {{wrap: ModuleWrap, namespace: ModuleNamespaceObject}}
273273
*/
274274
importSyncForRequire(mod, filename, source, isMain, parent) {
275275
const url = pathToFileURL(filename).href;
@@ -294,7 +294,7 @@ class ModuleLoader {
294294
}
295295
throw new ERR_REQUIRE_CYCLE_MODULE(message);
296296
}
297-
return job.module.getNamespaceSync();
297+
return { wrap: job.module, namespace: job.module.getNamespaceSync() };
298298
}
299299
// TODO(joyeecheung): refactor this so that we pre-parse in C++ and hit the
300300
// cache here, or use a carrier object to carry the compiled module script
@@ -306,7 +306,7 @@ class ModuleLoader {
306306
job = new ModuleJobSync(this, url, kEmptyObject, wrap, isMain, inspectBrk);
307307
this.loadCache.set(url, kImplicitAssertType, job);
308308
mod[kRequiredModuleSymbol] = job.module;
309-
return job.runSync().namespace;
309+
return { wrap: job.module, namespace: job.runSync().namespace };
310310
}
311311

312312
/**

src/env.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1061,6 +1061,8 @@ class Environment : public MemoryRetainer {
10611061
std::vector<std::string> supported_hash_algorithms;
10621062
#endif // HAVE_OPENSSL
10631063

1064+
v8::Global<v8::Module> temporary_required_module_facade_original;
1065+
10641066
private:
10651067
// V8 has changed the constructor of exceptions, support both APIs before Node
10661068
// updates to V8 12.1.

src/env_properties.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@
252252
V(openssl_error_stack, "opensslErrorStack") \
253253
V(options_string, "options") \
254254
V(order_string, "order") \
255+
V(original_string, "original") \
255256
V(output_string, "output") \
256257
V(overlapped_string, "overlapped") \
257258
V(parse_error_string, "Parse Error") \
@@ -285,6 +286,11 @@
285286
V(regexp_string, "regexp") \
286287
V(rename_string, "rename") \
287288
V(replacement_string, "replacement") \
289+
V(required_module_facade_url_string, \
290+
"node:internal/require_module_default_facade") \
291+
V(required_module_facade_source_string, \
292+
"export * from 'original'; export { default } from 'original'; export " \
293+
"const __esModule = true;") \
288294
V(require_string, "require") \
289295
V(resource_string, "resource") \
290296
V(retry_string, "retry") \

src/module_wrap.cc

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -966,6 +966,70 @@ void ModuleWrap::CreateCachedData(const FunctionCallbackInfo<Value>& args) {
966966
}
967967
}
968968

969+
// This v8::Module::ResolveModuleCallback simply links `import 'original'`
970+
// to the env->temporary_required_module_facade_original() which is stashed
971+
// right before this callback is called and will be restored as soon as
972+
// v8::Module::Instantiate() returns.
973+
MaybeLocal<Module> LinkRequireFacadeWithOriginal(
974+
Local<Context> context,
975+
Local<String> specifier,
976+
Local<FixedArray> import_attributes,
977+
Local<Module> referrer) {
978+
Environment* env = Environment::GetCurrent(context);
979+
Isolate* isolate = context->GetIsolate();
980+
CHECK(specifier->Equals(context, env->original_string()).ToChecked());
981+
CHECK(!env->temporary_required_module_facade_original.IsEmpty());
982+
return env->temporary_required_module_facade_original.Get(isolate);
983+
}
984+
985+
// Wraps an existing source text module with a facade that adds
986+
// .__esModule = true to the exports.
987+
// See env->required_module_facade_source_string() for the source.
988+
void ModuleWrap::CreateRequiredModuleFacade(
989+
const FunctionCallbackInfo<Value>& args) {
990+
Isolate* isolate = args.GetIsolate();
991+
Local<Context> context = isolate->GetCurrentContext();
992+
Environment* env = Environment::GetCurrent(context);
993+
CHECK(args[0]->IsObject()); // original module
994+
Local<Object> wrap = args[0].As<Object>();
995+
ModuleWrap* original;
996+
ASSIGN_OR_RETURN_UNWRAP(&original, wrap);
997+
998+
// Use the same facade source and URL to hit the compilation cache.
999+
ScriptOrigin origin(isolate,
1000+
env->required_module_facade_url_string(),
1001+
0, // line offset
1002+
0, // column offset
1003+
true, // is cross origin
1004+
-1, // script id
1005+
Local<Value>(), // source map URL
1006+
false, // is opaque (?)
1007+
false, // is WASM
1008+
true); // is ES Module
1009+
ScriptCompiler::Source source(env->required_module_facade_source_string(),
1010+
origin);
1011+
1012+
// The module facade instantiation simply links `import 'original'` in the
1013+
// facade with the original module and should never fail.
1014+
Local<Module> facade =
1015+
ScriptCompiler::CompileModule(isolate, &source).ToLocalChecked();
1016+
// Stash the original module in temporary_required_module_facade_original
1017+
// for the LinkRequireFacadeWithOriginal() callback to pick it up.
1018+
CHECK(env->temporary_required_module_facade_original.IsEmpty());
1019+
env->temporary_required_module_facade_original.Reset(
1020+
isolate, original->module_.Get(isolate));
1021+
CHECK(facade->InstantiateModule(context, LinkRequireFacadeWithOriginal)
1022+
.IsJust());
1023+
env->temporary_required_module_facade_original.Reset();
1024+
1025+
// The evaluation of the facade is synchronous.
1026+
Local<Value> evaluated = facade->Evaluate(context).ToLocalChecked();
1027+
CHECK(evaluated->IsPromise());
1028+
CHECK_EQ(evaluated.As<Promise>()->State(), Promise::PromiseState::kFulfilled);
1029+
1030+
args.GetReturnValue().Set(facade->GetModuleNamespace());
1031+
}
1032+
9691033
void ModuleWrap::CreatePerIsolateProperties(IsolateData* isolate_data,
9701034
Local<ObjectTemplate> target) {
9711035
Isolate* isolate = isolate_data->isolate();
@@ -998,6 +1062,10 @@ void ModuleWrap::CreatePerIsolateProperties(IsolateData* isolate_data,
9981062
target,
9991063
"setInitializeImportMetaObjectCallback",
10001064
SetInitializeImportMetaObjectCallback);
1065+
SetMethod(isolate,
1066+
target,
1067+
"createRequiredModuleFacade",
1068+
CreateRequiredModuleFacade);
10011069
}
10021070

10031071
void ModuleWrap::CreatePerContextProperties(Local<Object> target,
@@ -1038,6 +1106,8 @@ void ModuleWrap::RegisterExternalReferences(
10381106
registry->Register(GetStatus);
10391107
registry->Register(GetError);
10401108

1109+
registry->Register(CreateRequiredModuleFacade);
1110+
10411111
registry->Register(SetImportModuleDynamicallyCallback);
10421112
registry->Register(SetInitializeImportMetaObjectCallback);
10431113
}

src/module_wrap.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ class ModuleWrap : public BaseObject {
8787
std::optional<v8::ScriptCompiler::CachedData*> user_cached_data,
8888
bool* cache_rejected);
8989

90+
static void CreateRequiredModuleFacade(
91+
const v8::FunctionCallbackInfo<v8::Value>& args);
92+
9093
private:
9194
ModuleWrap(Realm* realm,
9295
v8::Local<v8::Object> object,

test/common/index.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -927,9 +927,14 @@ function getPrintedStackTrace(stderr) {
927927
* @param {object} mod result returned by require()
928928
* @param {object} expectation shape of expected namespace.
929929
*/
930-
function expectRequiredModule(mod, expectation) {
930+
function expectRequiredModule(mod, expectation, checkESModule = true) {
931+
const clone = { ...mod };
932+
if (Object.hasOwn(mod, 'default') && checkESModule) {
933+
assert.strictEqual(mod.__esModule, true);
934+
delete clone.__esModule;
935+
}
931936
assert(isModuleNamespaceObject(mod));
932-
assert.deepStrictEqual({ ...mod }, { ...expectation });
937+
assert.deepStrictEqual(clone, { ...expectation });
933938
}
934939

935940
const common = {

test/es-module/test-require-module-default-extension.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
// Flags: --experimental-require-module
22
'use strict';
33

4-
require('../common');
4+
const { expectRequiredModule } = require('../common');
55
const assert = require('assert');
6-
const { isModuleNamespaceObject } = require('util/types');
76

87
const mod = require('../fixtures/es-modules/package-default-extension/index.mjs');
9-
assert.deepStrictEqual({ ...mod }, { entry: 'mjs' });
10-
assert(isModuleNamespaceObject(mod));
8+
expectRequiredModule(mod, { entry: 'mjs' });
119

1210
assert.throws(() => {
1311
const mod = require('../fixtures/es-modules/package-default-extension');
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Flags: --experimental-require-module
2+
'use strict';
3+
const common = require('../common');
4+
5+
// If an ESM already defines __esModule to be something else,
6+
// require(esm) should allow the user override.
7+
{
8+
const mod = require('../fixtures/es-modules/export-es-module.mjs');
9+
common.expectRequiredModule(
10+
mod,
11+
{ default: { hello: 'world' }, __esModule: 'test' },
12+
false,
13+
);
14+
}
15+
16+
{
17+
const mod = require('../fixtures/es-modules/export-es-module-2.mjs');
18+
common.expectRequiredModule(
19+
mod,
20+
{ default: { hello: 'world' }, __esModule: false },
21+
false,
22+
);
23+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy