GeezerVison is showing github.com — module: add __esModule to require()'d ESM · nodejs/node@aa7df95
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 |
|---|
@@ -188,33 +188,51 @@ loaded by `require()` meets the following requirements: | 188 | 188 | `"type": "commonjs"`, and `--experimental-detect-module` is enabled. | 189 | 189 |
| 190 | 190 | `require()` will load the requested module as an ES Module, and return | 191 | | -the module name space object. In this case it is similar to dynamic | | 191 | +the module namespace object. In this case it is similar to dynamic | 192 | 192 | `import()` but is run synchronously and returns the name space object | 193 | 193 | directly. | 194 | 194 |
| | 195 | +With the following ES Modules: | | 196 | + | 195 | 197 | ```mjs | 196 | | -// point.mjs | | 198 | +// distance.mjs | 197 | 199 | export function distance(a, b) { return (b.x - a.x) ** 2 + (b.y - a.y) ** 2; } | | 200 | +``` | | 201 | + | | 202 | +```mjs | | 203 | +// point.mjs | 198 | 204 | class Point { | 199 | 205 | constructor(x, y) { this.x = x; this.y = y; } | 200 | 206 | } | 201 | 207 | export default Point; | 202 | 208 | ``` | 203 | 209 |
| | 210 | +A CommonJS module can load them with `require()` under `--experimental-detect-module`: | | 211 | + | 204 | 212 | ```cjs | 205 | | -const required = require('./point.mjs'); | | 213 | +const distance = require('./distance.mjs'); | | 214 | +console.log(distance); | 206 | 215 | // [Module: null prototype] { | 207 | | -// default: [class Point], | 208 | 216 | // distance: [Function: distance] | 209 | 217 | // } | 210 | | -console.log(required); | 211 | 218 |
| 212 | | -(async () => { | 213 | | - const imported = await import('./point.mjs'); | 214 | | - console.log(imported === required); // true | 215 | | -})(); | | 219 | +const point = require('./point.mjs'); | | 220 | +console.log(point); | | 221 | +// [Module: null prototype] { | | 222 | +// default: [class Point], | | 223 | +// __esModule: true, | | 224 | +// } | 216 | 225 | ``` | 217 | 226 |
| | 227 | +For interoperability with existing tools that convert ES Modules into CommonJS, | | 228 | +which could then load real ES Modules through `require()`, the returned namespace | | 229 | +would contain a `__esModule: true` property if it has a `default` export so that | | 230 | +consuming code generated by tools can recognize the default exports in real | | 231 | +ES Modules. If the namespace already defines `__esModule`, this would not be added. | | 232 | +This property is experimental and can change in the future. It should only be used | | 233 | +by tools converting ES modules into CommonJS modules, following existing ecosystem | | 234 | +conventions. Code authored directly in CommonJS should avoid depending on it. | | 235 | + | 218 | 236 | If the module being `require()`'d contains top-level `await`, or the module | 219 | 237 | graph it `import`s contains top-level `await`, | 220 | 238 | [`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 | /** | @@ -1333,9 +1334,55 @@ function loadESMFromCJS(mod, filename) { | 1333 | 1334 | // ESM won't be accessible via process.mainModule. | 1334 | 1335 | setOwnProperty(process, 'mainModule', undefined); | 1335 | 1336 | } else { | 1336 | | - // TODO(joyeecheung): we may want to invent optional special handling for default exports here. | 1337 | | - // For now, it's good enough to be identical to what `import()` returns. | 1338 | | - mod.exports = cascadedLoader.importSyncForRequire(mod, filename, source, isMain, mod[kModuleParent]); | | 1337 | + const { | | 1338 | + wrap, | | 1339 | + namespace, | | 1340 | + } = cascadedLoader.importSyncForRequire(mod, filename, source, isMain, mod[kModuleParent]); | | 1341 | + // Tooling in the ecosystem have been using the __esModule property to recognize | | 1342 | + // transpiled ESM in consuming code. For example, a 'log' package written in ESM: | | 1343 | + // | | 1344 | + // export default function log(val) { console.log(val); } | | 1345 | + // | | 1346 | + // Can be transpiled as: | | 1347 | + // | | 1348 | + // exports.__esModule = true; | | 1349 | + // exports.default = function log(val) { console.log(val); } | | 1350 | + // | | 1351 | + // The consuming code may be written like this in ESM: | | 1352 | + // | | 1353 | + // import log from 'log' | | 1354 | + // | | 1355 | + // Which gets transpiled to: | | 1356 | + // | | 1357 | + // const _mod = require('log'); | | 1358 | + // const log = _mod.__esModule ? _mod.default : _mod; | | 1359 | + // | | 1360 | + // So to allow transpiled consuming code to recognize require()'d real ESM | | 1361 | + // as ESM and pick up the default exports, we add a __esModule property by | | 1362 | + // building a source text module facade for any module that has a default | | 1363 | + // export and add .__esModule = true to the exports. This maintains the | | 1364 | + // enumerability of the re-exported names and the live binding of the exports, | | 1365 | + // without incurring a non-trivial per-access overhead on the exports. | | 1366 | + // | | 1367 | + // The source of the facade is defined as a constant per-isolate property | | 1368 | + // required_module_default_facade_source_string, which looks like this | | 1369 | + // | | 1370 | + // export * from 'original'; | | 1371 | + // export { default } from 'original'; | | 1372 | + // export const __esModule = true; | | 1373 | + // | | 1374 | + // And the 'original' module request is always resolved by | | 1375 | + // createRequiredModuleFacade() to `wrap` which is a ModuleWrap wrapping | | 1376 | + // over the original module. | | 1377 | + | | 1378 | + // We don't do this to modules that don't have default exports to avoid | | 1379 | + // the unnecessary overhead. If __esModule is already defined, we will | | 1380 | + // also skip the extension to allow users to override it. | | 1381 | + if (!ObjectHasOwn(namespace, 'default') || ObjectHasOwn(namespace, '__esModule')) { | | 1382 | + mod.exports = namespace; | | 1383 | + } else { | | 1384 | + mod.exports = createRequiredModuleFacade(wrap); | | 1385 | + } | 1339 | 1386 | } | 1340 | 1387 | } | 1341 | 1388 |
| |
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 |
|---|
@@ -280,7 +280,7 @@ class ModuleLoader { | 280 | 280 | * @param {string} source Source code. TODO(joyeecheung): pass the raw buffer. | 281 | 281 | * @param {string} isMain Whether this module is a main module. | 282 | 282 | * @param {CJSModule|undefined} parent Parent module, if any. | 283 | | - * @returns {{ModuleWrap}} | | 283 | + * @returns {{wrap: ModuleWrap, namespace: ModuleNamespaceObject}} | 284 | 284 | */ | 285 | 285 | importSyncForRequire(mod, filename, source, isMain, parent) { | 286 | 286 | const url = pathToFileURL(filename).href; | @@ -305,7 +305,7 @@ class ModuleLoader { | 305 | 305 | } | 306 | 306 | throw new ERR_REQUIRE_CYCLE_MODULE(message); | 307 | 307 | } | 308 | | - return job.module.getNamespaceSync(); | | 308 | + return { wrap: job.module, namespace: job.module.getNamespaceSync() }; | 309 | 309 | } | 310 | 310 | // TODO(joyeecheung): refactor this so that we pre-parse in C++ and hit the | 311 | 311 | // cache here, or use a carrier object to carry the compiled module script | @@ -317,7 +317,7 @@ class ModuleLoader { | 317 | 317 | job = new ModuleJobSync(this, url, kEmptyObject, wrap, isMain, inspectBrk); | 318 | 318 | this.loadCache.set(url, kImplicitTypeAttribute, job); | 319 | 319 | mod[kRequiredModuleSymbol] = job.module; | 320 | | - return job.runSync().namespace; | | 320 | + return { wrap: job.module, namespace: job.runSync().namespace }; | 321 | 321 | } | 322 | 322 |
| 323 | 323 | /** | |
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 |
|---|
@@ -1057,6 +1057,8 @@ class Environment : public MemoryRetainer { | 1057 | 1057 | std::vector<std::string> supported_hash_algorithms; | 1058 | 1058 | #endif // HAVE_OPENSSL | 1059 | 1059 |
| | 1060 | + v8::Global<v8::Module> temporary_required_module_facade_original; | | 1061 | + | 1060 | 1062 | private: | 1061 | 1063 | inline void ThrowError(v8::Local<v8::Value> (*fun)(v8::Local<v8::String>, | 1062 | 1064 | v8::Local<v8::Value>), | |
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 |
|---|
| 256 | 256 | V(openssl_error_stack, "opensslErrorStack") \ | 257 | 257 | V(options_string, "options") \ | 258 | 258 | V(order_string, "order") \ | | 259 | + V(original_string, "original") \ | 259 | 260 | V(output_string, "output") \ | 260 | 261 | V(overlapped_string, "overlapped") \ | 261 | 262 | V(parse_error_string, "Parse Error") \ | | 289 | 290 | V(regexp_string, "regexp") \ | 290 | 291 | V(rename_string, "rename") \ | 291 | 292 | V(replacement_string, "replacement") \ | | 293 | + V(required_module_facade_url_string, \ | | 294 | + "node:internal/require_module_default_facade") \ | | 295 | + V(required_module_facade_source_string, \ | | 296 | + "export * from 'original'; export { default } from 'original'; export " \ | | 297 | + "const __esModule = true;") \ | 292 | 298 | V(require_string, "require") \ | 293 | 299 | V(resource_string, "resource") \ | 294 | 300 | V(retry_string, "retry") \ | |
Collapse file Copy file name to clipboardExpand all lines: src/module_wrap.cc +69Lines changed: 69 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -1019,6 +1019,69 @@ void ModuleWrap::CreateCachedData(const FunctionCallbackInfo<Value>& args) { | 1019 | 1019 | } | 1020 | 1020 | } | 1021 | 1021 |
| | 1022 | +// This v8::Module::ResolveModuleCallback simply links `import 'original'` | | 1023 | +// to the env->temporary_required_module_facade_original() which is stashed | | 1024 | +// right before this callback is called and will be restored as soon as | | 1025 | +// v8::Module::Instantiate() returns. | | 1026 | +MaybeLocal<Module> LinkRequireFacadeWithOriginal( | | 1027 | + Local<Context> context, | | 1028 | + Local<String> specifier, | | 1029 | + Local<FixedArray> import_attributes, | | 1030 | + Local<Module> referrer) { | | 1031 | + Environment* env = Environment::GetCurrent(context); | | 1032 | + Isolate* isolate = context->GetIsolate(); | | 1033 | + CHECK(specifier->Equals(context, env->original_string()).ToChecked()); | | 1034 | + CHECK(!env->temporary_required_module_facade_original.IsEmpty()); | | 1035 | + return env->temporary_required_module_facade_original.Get(isolate); | | 1036 | +} | | 1037 | + | | 1038 | +// Wraps an existing source text module with a facade that adds | | 1039 | +// .__esModule = true to the exports. | | 1040 | +// See env->required_module_facade_source_string() for the source. | | 1041 | +void ModuleWrap::CreateRequiredModuleFacade( | | 1042 | + const FunctionCallbackInfo<Value>& args) { | | 1043 | + Isolate* isolate = args.GetIsolate(); | | 1044 | + Local<Context> context = isolate->GetCurrentContext(); | | 1045 | + Environment* env = Environment::GetCurrent(context); | | 1046 | + CHECK(args[0]->IsObject()); // original module | | 1047 | + Local<Object> wrap = args[0].As<Object>(); | | 1048 | + ModuleWrap* original; | | 1049 | + ASSIGN_OR_RETURN_UNWRAP(&original, wrap); | | 1050 | + | | 1051 | + // Use the same facade source and URL to hit the compilation cache. | | 1052 | + ScriptOrigin origin(env->required_module_facade_url_string(), | | 1053 | + 0, // line offset | | 1054 | + 0, // column offset | | 1055 | + true, // is cross origin | | 1056 | + -1, // script id | | 1057 | + Local<Value>(), // source map URL | | 1058 | + false, // is opaque (?) | | 1059 | + false, // is WASM | | 1060 | + true); // is ES Module | | 1061 | + ScriptCompiler::Source source(env->required_module_facade_source_string(), | | 1062 | + origin); | | 1063 | + | | 1064 | + // The module facade instantiation simply links `import 'original'` in the | | 1065 | + // facade with the original module and should never fail. | | 1066 | + Local<Module> facade = | | 1067 | + ScriptCompiler::CompileModule(isolate, &source).ToLocalChecked(); | | 1068 | + // Stash the original module in temporary_required_module_facade_original | | 1069 | + // for the LinkRequireFacadeWithOriginal() callback to pick it up. | | 1070 | + CHECK(env->temporary_required_module_facade_original.IsEmpty()); | | 1071 | + env->temporary_required_module_facade_original.Reset( | | 1072 | + isolate, original->module_.Get(isolate)); | | 1073 | + CHECK(facade->InstantiateModule(context, LinkRequireFacadeWithOriginal) | | 1074 | + .IsJust()); | | 1075 | + env->temporary_required_module_facade_original.Reset(); | | 1076 | + | | 1077 | + // The evaluation of the facade is synchronous. | | 1078 | + Local<Value> evaluated = facade->Evaluate(context).ToLocalChecked(); | | 1079 | + CHECK(evaluated->IsPromise()); | | 1080 | + CHECK_EQ(evaluated.As<Promise>()->State(), Promise::PromiseState::kFulfilled); | | 1081 | + | | 1082 | + args.GetReturnValue().Set(facade->GetModuleNamespace()); | | 1083 | +} | | 1084 | + | 1022 | 1085 | void ModuleWrap::CreatePerIsolateProperties(IsolateData* isolate_data, | 1023 | 1086 | Local<ObjectTemplate> target) { | 1024 | 1087 | Isolate* isolate = isolate_data->isolate(); | @@ -1051,6 +1114,10 @@ void ModuleWrap::CreatePerIsolateProperties(IsolateData* isolate_data, | 1051 | 1114 | target, | 1052 | 1115 | "setInitializeImportMetaObjectCallback", | 1053 | 1116 | SetInitializeImportMetaObjectCallback); | | 1117 | + SetMethod(isolate, | | 1118 | + target, | | 1119 | + "createRequiredModuleFacade", | | 1120 | + CreateRequiredModuleFacade); | 1054 | 1121 | } | 1055 | 1122 |
| 1056 | 1123 | void ModuleWrap::CreatePerContextProperties(Local<Object> target, | @@ -1091,6 +1158,8 @@ void ModuleWrap::RegisterExternalReferences( | 1091 | 1158 | registry->Register(GetStatus); | 1092 | 1159 | registry->Register(GetError); | 1093 | 1160 |
| | 1161 | + registry->Register(CreateRequiredModuleFacade); | | 1162 | + | 1094 | 1163 | registry->Register(SetImportModuleDynamicallyCallback); | 1095 | 1164 | registry->Register(SetInitializeImportMetaObjectCallback); | 1096 | 1165 | } | |
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 |
|---|
@@ -88,6 +88,9 @@ class ModuleWrap : public BaseObject { | 88 | 88 | std::optional<v8::ScriptCompiler::CachedData*> user_cached_data, | 89 | 89 | bool* cache_rejected); | 90 | 90 |
| | 91 | + static void CreateRequiredModuleFacade( | | 92 | + const v8::FunctionCallbackInfo<v8::Value>& args); | | 93 | + | 91 | 94 | private: | 92 | 95 | ModuleWrap(Realm* realm, | 93 | 96 | 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 |
|---|
@@ -964,9 +964,14 @@ function getPrintedStackTrace(stderr) { | 964 | 964 | * @param {object} mod result returned by require() | 965 | 965 | * @param {object} expectation shape of expected namespace. | 966 | 966 | */ | 967 | | -function expectRequiredModule(mod, expectation) { | | 967 | +function expectRequiredModule(mod, expectation, checkESModule = true) { | | 968 | + const clone = { ...mod }; | | 969 | + if (Object.hasOwn(mod, 'default') && checkESModule) { | | 970 | + assert.strictEqual(mod.__esModule, true); | | 971 | + delete clone.__esModule; | | 972 | + } | 968 | 973 | assert(isModuleNamespaceObject(mod)); | 969 | | - assert.deepStrictEqual({ ...mod }, { ...expectation }); | | 974 | + assert.deepStrictEqual(clone, { ...expectation }); | 970 | 975 | } | 971 | 976 |
| 972 | 977 | 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 | +} |
|