| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 18593b7 commit f9dc1ea
30 files changed
| 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 | |
| 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 | ||
| 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 | /** | |
| 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. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -252,6 +252,7 @@ | |||
| 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,6 +286,11 @@ | |||
| 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") \ | |
| 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 | } | |
| 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, | |
| 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 = { | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,13 +1,11 @@ | |||
| 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'); | |
| Original file line number | Diff line number | Diff 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 | + } | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments