GeezerVison is showing github.com — module: add __esModule to require()'d ESM · nodejs/node@f9dc1ea
File treeExpand file treeCollapse file tree Open diff view settings transpiled-cjs-require-module
Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/modules.md +27-9Lines changed: 27 additions & 9 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -195,33 +195,51 @@ loaded by `require()` meets the following requirements: | 195 | 195 | `"type": "commonjs"`, and `--experimental-detect-module` is enabled. | 196 | 196 |
| 197 | 197 | `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 | 199 | 199 | `import()` but is run synchronously and returns the name space object | 200 | 200 | directly. | 201 | 201 |
| | 202 | +With the following ES Modules: | | 203 | + | 202 | 204 | ```mjs | 203 | | -// point.mjs | | 205 | +// distance.mjs | 204 | 206 | export function distance(a, b) { return (b.x - a.x) ** 2 + (b.y - a.y) ** 2; } | | 207 | +``` | | 208 | + | | 209 | +```mjs | | 210 | +// point.mjs | 205 | 211 | class Point { | 206 | 212 | constructor(x, y) { this.x = x; this.y = y; } | 207 | 213 | } | 208 | 214 | export default Point; | 209 | 215 | ``` | 210 | 216 |
| | 217 | +A CommonJS module can load them with `require()` under `--experimental-detect-module`: | | 218 | + | 211 | 219 | ```cjs | 212 | | -const required = require('./point.mjs'); | | 220 | +const distance = require('./distance.mjs'); | | 221 | +console.log(distance); | 213 | 222 | // [Module: null prototype] { | 214 | | -// default: [class Point], | 215 | 223 | // distance: [Function: distance] | 216 | 224 | // } | 217 | | -console.log(required); | 218 | 225 |
| 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 | +// } | 223 | 232 | ``` | 224 | 233 |
| | 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 | + | 225 | 243 | If the module being `require()`'d contains top-level `await`, or the module | 226 | 244 | graph it `import`s contains top-level `await`, | 227 | 245 | [`ERR_REQUIRE_ASYNC_MODULE`][] will be thrown. In this case, users should | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/cjs/loader.js +51-4Lines changed: 51 additions & 4 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -41,6 +41,7 @@ const { | 41 | 41 | ObjectFreeze, | 42 | 42 | ObjectGetOwnPropertyDescriptor, | 43 | 43 | ObjectGetPrototypeOf, | | 44 | + ObjectHasOwn, | 44 | 45 | ObjectKeys, | 45 | 46 | ObjectPrototype, | 46 | 47 | ObjectPrototypeHasOwnProperty, | @@ -71,7 +72,7 @@ const { | 71 | 72 | }, | 72 | 73 | } = internalBinding('util'); | 73 | 74 |
| 74 | | -const { kEvaluated } = internalBinding('module_wrap'); | | 75 | +const { kEvaluated, createRequiredModuleFacade } = internalBinding('module_wrap'); | 75 | 76 |
| 76 | 77 | // Internal properties for Module instances. | 77 | 78 | /** | @@ -1340,9 +1341,55 @@ function loadESMFromCJS(mod, filename) { | 1340 | 1341 | // ESM won't be accessible via process.mainModule. | 1341 | 1342 | setOwnProperty(process, 'mainModule', undefined); | 1342 | 1343 | } 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 | + } | 1346 | 1393 | } | 1347 | 1394 | } | 1348 | 1395 |
| |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/esm/loader.js +3-3Lines changed: 3 additions & 3 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -269,7 +269,7 @@ class ModuleLoader { | 269 | 269 | * @param {string} source Source code. TODO(joyeecheung): pass the raw buffer. | 270 | 270 | * @param {string} isMain Whether this module is a main module. | 271 | 271 | * @param {CJSModule|undefined} parent Parent module, if any. | 272 | | - * @returns {{ModuleWrap}} | | 272 | + * @returns {{wrap: ModuleWrap, namespace: ModuleNamespaceObject}} | 273 | 273 | */ | 274 | 274 | importSyncForRequire(mod, filename, source, isMain, parent) { | 275 | 275 | const url = pathToFileURL(filename).href; | @@ -294,7 +294,7 @@ class ModuleLoader { | 294 | 294 | } | 295 | 295 | throw new ERR_REQUIRE_CYCLE_MODULE(message); | 296 | 296 | } | 297 | | - return job.module.getNamespaceSync(); | | 297 | + return { wrap: job.module, namespace: job.module.getNamespaceSync() }; | 298 | 298 | } | 299 | 299 | // TODO(joyeecheung): refactor this so that we pre-parse in C++ and hit the | 300 | 300 | // cache here, or use a carrier object to carry the compiled module script | @@ -306,7 +306,7 @@ class ModuleLoader { | 306 | 306 | job = new ModuleJobSync(this, url, kEmptyObject, wrap, isMain, inspectBrk); | 307 | 307 | this.loadCache.set(url, kImplicitAssertType, job); | 308 | 308 | mod[kRequiredModuleSymbol] = job.module; | 309 | | - return job.runSync().namespace; | | 309 | + return { wrap: job.module, namespace: job.runSync().namespace }; | 310 | 310 | } | 311 | 311 |
| 312 | 312 | /** | |
Collapse file Copy file name to clipboardExpand all lines: src/env.h +2Lines changed: 2 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -1061,6 +1061,8 @@ class Environment : public MemoryRetainer { | 1061 | 1061 | std::vector<std::string> supported_hash_algorithms; | 1062 | 1062 | #endif // HAVE_OPENSSL | 1063 | 1063 |
| | 1064 | + v8::Global<v8::Module> temporary_required_module_facade_original; | | 1065 | + | 1064 | 1066 | private: | 1065 | 1067 | // V8 has changed the constructor of exceptions, support both APIs before Node | 1066 | 1068 | // updates to V8 12.1. | |
Collapse file Copy file name to clipboardExpand all lines: src/env_properties.h +6Lines changed: 6 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 252 | 252 | V(openssl_error_stack, "opensslErrorStack") \ | 253 | 253 | V(options_string, "options") \ | 254 | 254 | V(order_string, "order") \ | | 255 | + V(original_string, "original") \ | 255 | 256 | V(output_string, "output") \ | 256 | 257 | V(overlapped_string, "overlapped") \ | 257 | 258 | V(parse_error_string, "Parse Error") \ | | 285 | 286 | V(regexp_string, "regexp") \ | 286 | 287 | V(rename_string, "rename") \ | 287 | 288 | 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;") \ | 288 | 294 | V(require_string, "require") \ | 289 | 295 | V(resource_string, "resource") \ | 290 | 296 | V(retry_string, "retry") \ | |
Collapse file Copy file name to clipboardExpand all lines: src/module_wrap.cc +70Lines changed: 70 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -966,6 +966,70 @@ void ModuleWrap::CreateCachedData(const FunctionCallbackInfo<Value>& args) { | 966 | 966 | } | 967 | 967 | } | 968 | 968 |
| | 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 | + | 969 | 1033 | void ModuleWrap::CreatePerIsolateProperties(IsolateData* isolate_data, | 970 | 1034 | Local<ObjectTemplate> target) { | 971 | 1035 | Isolate* isolate = isolate_data->isolate(); | @@ -998,6 +1062,10 @@ void ModuleWrap::CreatePerIsolateProperties(IsolateData* isolate_data, | 998 | 1062 | target, | 999 | 1063 | "setInitializeImportMetaObjectCallback", | 1000 | 1064 | SetInitializeImportMetaObjectCallback); | | 1065 | + SetMethod(isolate, | | 1066 | + target, | | 1067 | + "createRequiredModuleFacade", | | 1068 | + CreateRequiredModuleFacade); | 1001 | 1069 | } | 1002 | 1070 |
| 1003 | 1071 | void ModuleWrap::CreatePerContextProperties(Local<Object> target, | @@ -1038,6 +1106,8 @@ void ModuleWrap::RegisterExternalReferences( | 1038 | 1106 | registry->Register(GetStatus); | 1039 | 1107 | registry->Register(GetError); | 1040 | 1108 |
| | 1109 | + registry->Register(CreateRequiredModuleFacade); | | 1110 | + | 1041 | 1111 | registry->Register(SetImportModuleDynamicallyCallback); | 1042 | 1112 | registry->Register(SetInitializeImportMetaObjectCallback); | 1043 | 1113 | } | |
Collapse file Copy file name to clipboardExpand all lines: src/module_wrap.h +3Lines changed: 3 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -87,6 +87,9 @@ class ModuleWrap : public BaseObject { | 87 | 87 | std::optional<v8::ScriptCompiler::CachedData*> user_cached_data, | 88 | 88 | bool* cache_rejected); | 89 | 89 |
| | 90 | + static void CreateRequiredModuleFacade( | | 91 | + const v8::FunctionCallbackInfo<v8::Value>& args); | | 92 | + | 90 | 93 | private: | 91 | 94 | ModuleWrap(Realm* realm, | 92 | 95 | v8::Local<v8::Object> object, | |
Collapse file Copy file name to clipboardExpand all lines: test/common/index.js +7-2Lines changed: 7 additions & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -927,9 +927,14 @@ function getPrintedStackTrace(stderr) { | 927 | 927 | * @param {object} mod result returned by require() | 928 | 928 | * @param {object} expectation shape of expected namespace. | 929 | 929 | */ | 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 | + } | 931 | 936 | assert(isModuleNamespaceObject(mod)); | 932 | | - assert.deepStrictEqual({ ...mod }, { ...expectation }); | | 937 | + assert.deepStrictEqual(clone, { ...expectation }); | 933 | 938 | } | 934 | 939 |
| 935 | 940 | const common = { | |
Collapse file Copy file name to clipboardExpand all lines: test/es-module/test-require-module-default-extension.js +2-4Lines changed: 2 additions & 4 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 1 | 1 | // Flags: --experimental-require-module | 2 | 2 | 'use strict'; | 3 | 3 |
| 4 | | -require('../common'); | | 4 | +const { expectRequiredModule } = require('../common'); | 5 | 5 | const assert = require('assert'); | 6 | | -const { isModuleNamespaceObject } = require('util/types'); | 7 | 6 |
| 8 | 7 | 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' }); | 11 | 9 |
| 12 | 10 | assert.throws(() => { | 13 | 11 | const mod = require('../fixtures/es-modules/package-default-extension'); | |
Collapse file Copy file name to clipboard +23Lines changed: 23 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 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 | +} |
|