Skip to content
File

Blob: src/workerd/jsg/modules.c++

24.0 KB
1// Copyright (c) 2017-2022 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5#include "jsg.h"
6#include "setup.h"
7#include "util.h"
8 
9#include <v8-wasm.h>
10 
11#include <kj/mutex.h>
12 
13namespace workerd::jsg {
14 
15namespace {
16 
17// Generalized module resolution callback that handles both evaluation and source phase imports
18template <bool IsSourcePhase>
19v8::MaybeLocal<std::conditional_t<IsSourcePhase, v8::Object, v8::Module>> resolveModuleCallback(
20 v8::Local<v8::Context> context,
21 v8::Local<v8::String> specifier,
22 v8::Local<v8::FixedArray> import_attributes,
23 v8::Local<v8::Module> referrer) {
24 using ReturnType = std::conditional_t<IsSourcePhase, v8::Object, v8::Module>;
25 
26 auto& js = Lock::current();
27 v8::MaybeLocal<ReturnType> result;
28 
29 // The specification for import attributes strongly recommends that embedders
30 // reject import attributes and types they do not understand/implement. This
31 // is because import attributes can alter the interpretation of a module and
32 // are considered to be part of the unique key for caching a module.
33 // Throwing an error for things we do not understand is the safest thing to do.
34 // However, historically we have not followed this guideline in the spec
35 // and unfortunately there are applications deployed that will break if we
36 // started enforcing that guideline without a compat flag.
37 if (!import_attributes.IsEmpty() && import_attributes->Length() > 0) {
38 JSG_REQUIRE(!js.getThrowOnUnrecognizedImportAssertion(), Error,
39 "Unrecognized import attributes specified");
40 }
41 
42 js.tryCatch([&] {
43 auto registry = getModulesForResolveCallback(js.v8Isolate);
44 KJ_REQUIRE(registry != nullptr, "didn't expect resolveCallback() now");
45 
46 auto ref = KJ_ASSERT_NONNULL(registry->resolve(js, referrer),
47 "referrer passed to resolveCallback isn't in modules table");
48 
49 auto spec = kj::str(specifier);
50 
51 if (isNodeJsCompatEnabled(js)) {
52 KJ_IF_SOME(nodeSpec, checkNodeSpecifier(spec)) {
53 spec = kj::mv(nodeSpec);
54 }
55 }
56 
57 // Handle process module redirection based on enable_nodejs_process_v2 flag
58 if constexpr (!IsSourcePhase) {
59 if (spec == "node:process") {
60 auto specifierPath =
61 kj::Path::parse(isNodeJsProcessV2Enabled(js) ? "node-internal:public_process"_kj
62 : "node-internal:legacy_process"_kj);
63 KJ_IF_SOME(info,
64 registry->resolve(
65 js, specifierPath, kj::none, ModuleRegistry::ResolveOption::INTERNAL_ONLY)) {
66 result = info.module.getHandle(js.v8Isolate);
67 return;
68 }
69 }
70 }
71 
72 // If the referrer module is a built-in, it is only permitted to resolve
73 // internal modules. If the worker bundle provided an override for a builtin,
74 // then internalOnly will be false.
75 bool internalOnly =
76 ref.type == ModuleRegistry::Type::BUILTIN || ref.type == ModuleRegistry::Type::INTERNAL;
77 
78 kj::Path targetPath = ([&] {
79 // If the specifier begins with one of our known prefixes, let's not resolve
80 // it against the referrer.
81 if (internalOnly || spec.startsWith("node:") || spec.startsWith("cloudflare:") ||
82 spec.startsWith("workerd:")) {
83 return kj::Path::parse(spec);
84 }
85 return ref.specifier.parent().eval(spec);
86 })();
87 
88 KJ_IF_SOME(resolved,
89 registry->resolve(js, targetPath, ref.specifier,
90 internalOnly ? ModuleRegistry::ResolveOption::INTERNAL_ONLY
91 : ModuleRegistry::ResolveOption::DEFAULT,
92 ModuleRegistry::ResolveMethod::IMPORT, spec.asPtr())) {
93 if constexpr (!IsSourcePhase) {
94 result = resolved.module.getHandle(js);
95 } else {
96 KJ_IF_SOME(sourceObject, resolved.getModuleSourceObject(js)) {
97 result = sourceObject;
98 } else {
99 js.throwException(js.v8Ref(v8::Exception::SyntaxError(js.strIntern(
100 kj::str("Source phase import not available for module: ", targetPath.toString(),
101 ".\n imported from \"", ref.specifier.toString(), "\"")))));
102 return;
103 }
104 }
105 } else {
106 // This is a bit annoying. If the module was not found, then
107 // we need to check to see if it is a prefixed specifier. If it is,
108 // we'll try again with only the specifier and not the ref.specifier
109 // as parent. We have to do it this way just in case the worker bundle
110 // is using the prefix itself. (which isn't likely but is possible).
111 // We only need to do this if internalOnly is false.
112 if (!internalOnly && (spec.startsWith("node:") || spec.startsWith("cloudflare:"))) {
113 KJ_IF_SOME(resolve,
114 registry->resolve(js, kj::Path::parse(spec), ref.specifier,
115 ModuleRegistry::ResolveOption::DEFAULT, ModuleRegistry::ResolveMethod::IMPORT,
116 spec.asPtr())) {
117 if constexpr (!IsSourcePhase) {
118 result = resolve.module.getHandle(js);
119 } else {
120 js.throwException(js.v8Ref(v8::Exception::SyntaxError(js.strIntern(
121 kj::str("Source phase import not available for module: ", targetPath.toString(),
122 ".\n imported from \"", ref.specifier.toString(), "\"")))));
123 return;
124 }
125 return;
126 }
127 }
128 JSG_FAIL_REQUIRE(Error, "No such module \"", targetPath.toString(), "\".\n imported from \"",
129 ref.specifier.toString(), "\"");
130 }
131 }, [&](Value value) {
132 // We do not call js.throwException here since that will throw a JsExceptionThrown,
133 // which we do not want here. Instead, we'll schedule an exception on the isolate
134 // directly and set the result to an empty v8::MaybeLocal.
135 js.v8Isolate->ThrowException(value.getHandle(js));
136 result = v8::MaybeLocal<ReturnType>();
137 });
138 
139 return result;
140}
141 
142// Implementation of `v8::Module::SyntheticModuleEvaluationSteps`, which is called to initialize
143// the exports on a synthetic module. Obnoxiously, you can only initialize the exports in this
144// callback; V8 will crash if you try to call `SetSyntheticModuleExport()` from anywhere else.
145v8::MaybeLocal<v8::Value> evaluateSyntheticModuleCallback(
146 v8::Local<v8::Context> context, v8::Local<v8::Module> module) {
147 auto& js = Lock::current();
148 v8::EscapableHandleScope scope(js.v8Isolate);
149 v8::MaybeLocal<v8::Value> result;
150 
151 KJ_IF_SOME(exception, kj::runCatchingExceptions([&]() {
152 auto registry = getModulesForResolveCallback(js.v8Isolate);
153 auto ref = KJ_ASSERT_NONNULL(registry->resolve(js, module),
154 "module passed to evaluateSyntheticModuleCallback isn't in modules table");
155 
156 // V8 doc comments say this callback must always return an already-resolved promise... I don't
157 // know what the point of that is but I guess we'd better do what it says.
158 const auto makeResolvedPromise = [&]() {
159 v8::Local<v8::Promise::Resolver> resolver;
160 if (!v8::Promise::Resolver::New(context).ToLocal(&resolver)) {
161 // Return empty local and allow error to propagate.
162 return v8::Local<v8::Promise>();
163 }
164 if (!resolver->Resolve(context, js.v8Undefined()).IsJust()) {
165 // Return empty local and allow error to propagate.
166 return v8::Local<v8::Promise>();
167 }
168 return resolver->GetPromise();
169 };
170 
171 auto& synthetic = KJ_REQUIRE_NONNULL(ref.module.maybeSynthetic, "Not a synthetic module.");
172 auto defaultStr = js.strIntern("default"_kj);
173 
174 KJ_SWITCH_ONEOF(synthetic) {
175 KJ_CASE_ONEOF(info, ModuleRegistry::CapnpModuleInfo) {
176 bool success = true;
177 success = success &&
178 module->SetSyntheticModuleExport(js.v8Isolate, defaultStr, info.fileScope.getHandle(js))
179 .IsJust();
180 for (auto& decl: info.topLevelDecls) {
181 success = success &&
182 module
183 ->SetSyntheticModuleExport(
184 js.v8Isolate, v8StrIntern(js.v8Isolate, decl.key), decl.value.getHandle(js))
185 .IsJust();
186 }
187 
188 if (success) {
189 result = makeResolvedPromise();
190 } else {
191 // leave `result` empty to propagate the JS exception
192 }
193 }
194 KJ_CASE_ONEOF(info, ModuleRegistry::CommonJsModuleInfo) {
195 v8::TryCatch catcher(js.v8Isolate);
196 // const_cast is safe here because we're protected by the isolate js.
197 auto& commonjs = const_cast<ModuleRegistry::CommonJsModuleInfo&>(info);
198 try {
199 commonjs.evalFunc(js);
200 auto exports = commonjs.getExports(js);
201 if (module->SetSyntheticModuleExport(js.v8Isolate, defaultStr, exports).IsJust()) {
202 KJ_IF_SOME(obj, exports.tryCast<JsObject>()) {
203 KJ_IF_SOME(exports, ref.module.maybeNamedExports) {
204 for (auto& name: exports) {
205 // Ignore default... just in case someone was silly enough to include it.
206 if (name == "default"_kj) continue;
207 auto val = obj.get(js, name);
208 if (!module->SetSyntheticModuleExport(js.v8Isolate, js.strIntern(name), val)
209 .IsJust()) {
210 break;
211 }
212 }
213 }
214 }
215 result = makeResolvedPromise();
216 }
217 } catch (const JsExceptionThrown&) {
218 if (catcher.CanContinue()) catcher.ReThrow();
219 // leave `result` empty to propagate the JS exception
220 }
221 }
222 KJ_CASE_ONEOF(info, ModuleRegistry::TextModuleInfo) {
223 if (module->SetSyntheticModuleExport(js.v8Isolate, defaultStr, info.value.getHandle(js))
224 .IsJust()) {
225 result = makeResolvedPromise();
226 } else {
227 // leave 'result' empty to propagate the JS exception
228 }
229 }
230 KJ_CASE_ONEOF(info, ModuleRegistry::DataModuleInfo) {
231 if (module->SetSyntheticModuleExport(js.v8Isolate, defaultStr, info.value.getHandle(js))
232 .IsJust()) {
233 result = makeResolvedPromise();
234 } else {
235 // leave 'result' empty to propagate the JS exception
236 }
237 }
238 KJ_CASE_ONEOF(info, ModuleRegistry::WasmModuleInfo) {
239 if (module->SetSyntheticModuleExport(js.v8Isolate, defaultStr, info.value.getHandle(js))
240 .IsJust()) {
241 result = makeResolvedPromise();
242 } else {
243 // leave 'result' empty to propagate the JS exception
244 }
245 }
246 KJ_CASE_ONEOF(info, ModuleRegistry::JsonModuleInfo) {
247 if (module->SetSyntheticModuleExport(js.v8Isolate, defaultStr, info.value.getHandle(js))
248 .IsJust()) {
249 result = makeResolvedPromise();
250 } else {
251 // leave 'result' empty to propagate the JS exception
252 }
253 }
254 KJ_CASE_ONEOF(info, ModuleRegistry::ObjectModuleInfo) {
255 if (module->SetSyntheticModuleExport(js.v8Isolate, defaultStr, info.value.getHandle(js))
256 .IsJust()) {
257 result = makeResolvedPromise();
258 } else {
259 // leave 'result' empty to propagate the JS exception
260 }
261 }
262 }
263 })) {
264 // V8 doc comments say in the case of an error, throw the error and return an empty Maybe.
265 // I.e. NOT a rejected promise. OK...
266 js.v8Isolate->ThrowException(js.exceptionToJsValue(kj::mv(exception)).getHandle(js));
267 result = v8::Local<v8::Promise>();
268 }
269 
270 return scope.EscapeMaybe(result);
271}
272 
273} // namespace
274 
275ModuleRegistry* getModulesForResolveCallback(v8::Isolate* isolate) {
276 return &KJ_ASSERT_NONNULL(jsg::getAlignedPointerFromEmbedderData<ModuleRegistry>(
277 isolate->GetCurrentContext(), jsg::ContextPointerSlot::MODULE_REGISTRY));
278}
279 
280void instantiateModule(
281 jsg::Lock& js, v8::Local<v8::Module>& module, InstantiateModuleOptions options) {
282 KJ_ASSERT(!module.IsEmpty());
283 auto isolate = js.v8Isolate;
284 auto context = js.v8Context();
285 
286 auto status = module->GetStatus();
287 
288 // If the previous instantiation failed, throw the exception.
289 if (status == v8::Module::Status::kErrored) {
290 isolate->ThrowException(module->GetException());
291 throw jsg::JsExceptionThrown();
292 }
293 
294 // Nothing to do if the module is already evaluated.
295 if (status == v8::Module::Status::kEvaluated || status == v8::Module::Status::kEvaluating) return;
296 
297 if (status == v8::Module::Status::kUninstantiated) {
298 jsg::check(module->InstantiateModule(
299 context, resolveModuleCallback<false>, resolveModuleCallback<true>));
300 }
301 
302 auto prom = jsg::check(module->Evaluate(context)).As<v8::Promise>();
303 
304 if (module->IsGraphAsync() && prom->State() == v8::Promise::kPending) {
305 // If top level await has been disable, error.
306 JSG_REQUIRE(options != InstantiateModuleOptions::NO_TOP_LEVEL_AWAIT, Error,
307 "Top-level await in module is not permitted at this time.");
308 }
309 // We run microtasks to ensure that any promises that happen to be scheduled
310 // during the evaluation of the top level scope have a chance to be settled,
311 // even if those are not directly awaited.
312 js.runMicrotasks();
313 
314 switch (prom->State()) {
315 case v8::Promise::kPending:
316 // Let's make sure nobody is depending on modules awaiting on pending promises.
317 JSG_FAIL_REQUIRE(Error, "Top-level await in module is unsettled.");
318 case v8::Promise::kRejected:
319 // Since we don't actually support I/O when instantiating a worker, we don't return the
320 // promise from module->Evaluate, which means we lose any errors that happen during
321 // instantiation if we don't throw the rejection exception here.
322 isolate->ThrowException(module->GetException());
323 throw jsg::JsExceptionThrown();
324 case v8::Promise::kFulfilled:
325 break;
326 }
327}
328 
329// ===================================================================================
330 
331namespace {
332 
333static CompilationObserver::Option convertOption(ModuleInfoCompileOption option) {
334 switch (option) {
335 case ModuleInfoCompileOption::BUILTIN:
336 return CompilationObserver::Option::BUILTIN;
337 case ModuleInfoCompileOption::BUNDLE:
338 return CompilationObserver::Option::BUNDLE;
339 }
340 KJ_UNREACHABLE;
341}
342 
343v8::Local<v8::Module> compileEsmModule(jsg::Lock& js,
344 kj::StringPtr name,
345 kj::ArrayPtr<const char> content,
346 kj::ArrayPtr<const kj::byte> compileCache,
347 ModuleInfoCompileOption option,
348 const CompilationObserver& observer) {
349 // destroy the observer after compilation finished to indicate the end of the process.
350 auto compilationObserver =
351 observer.onEsmCompilationStart(js.v8Isolate, name, convertOption(option));
352 
353 // Must pass true for `is_module`, but we can skip everything else.
354 constexpr int resourceLineOffset = 0;
355 constexpr int resourceColumnOffset = 0;
356 constexpr bool resourceIsSharedCrossOrigin = false;
357 constexpr int scriptId = -1;
358 constexpr bool resourceIsOpaque = false;
359 constexpr bool isWasm = false;
360 constexpr bool isModule = true;
361 v8::ScriptOrigin origin(v8StrIntern(js.v8Isolate, name), resourceLineOffset, resourceColumnOffset,
362 resourceIsSharedCrossOrigin, scriptId, {}, resourceIsOpaque, isWasm, isModule);
363 v8::Local<v8::String> contentStr;
364 
365 if (option == ModuleInfoCompileOption::BUILTIN) {
366 // TODO(later): Use of newExternalOneByteString here limits our built-in source
367 // modules (for which this path is used) to only the latin1 character set. We
368 // may need to revisit that to import built-ins as UTF-16 (two-byte).
369 contentStr = jsg::newExternalOneByteString(js, content);
370 } else {
371 contentStr = jsg::v8Str(js.v8Isolate, content);
372 }
373 
374 if (compileCache.size() > 0 && compileCache.begin() != nullptr) {
375 auto cached =
376 std::make_unique<v8::ScriptCompiler::CachedData>(compileCache.begin(), compileCache.size());
377 v8::ScriptCompiler::Source source(contentStr, origin, cached.release());
378 return jsg::check(v8::ScriptCompiler::CompileModule(
379 js.v8Isolate, &source, v8::ScriptCompiler::kConsumeCodeCache));
380 }
381 
382 v8::ScriptCompiler::Source source(contentStr, origin);
383 return jsg::check(v8::ScriptCompiler::CompileModule(js.v8Isolate, &source));
384}
385 
386v8::Local<v8::Module> createSyntheticModule(
387 jsg::Lock& js, kj::StringPtr name, kj::Maybe<kj::ArrayPtr<const kj::StringPtr>> maybeExports) {
388 v8::LocalVector<v8::String> exportNames(js.v8Isolate);
389 exportNames.push_back(v8StrIntern(js.v8Isolate, "default"_kj));
390 KJ_IF_SOME(exports, maybeExports) {
391 exportNames.reserve(exports.size());
392 for (auto& name: exports) {
393 exportNames.push_back(v8StrIntern(js.v8Isolate, name));
394 }
395 }
396 return v8::Module::CreateSyntheticModule(js.v8Isolate, v8StrIntern(js.v8Isolate, name),
397 v8::MemorySpan<const v8::Local<v8::String>>(exportNames.data(), exportNames.size()),
398 &evaluateSyntheticModuleCallback);
399}
400} // namespace
401 
402ModuleRegistry::ModuleInfo::ModuleInfo(
403 jsg::Lock& js, v8::Local<v8::Module> module, kj::Maybe<SyntheticModuleInfo> maybeSynthetic)
404 : module(js.v8Isolate, module),
405 maybeSynthetic(kj::mv(maybeSynthetic)) {}
406 
407ModuleRegistry::ModuleInfo::ModuleInfo(jsg::Lock& js,
408 kj::StringPtr name,
409 kj::ArrayPtr<const char> content,
410 kj::ArrayPtr<const kj::byte> compileCache,
411 ModuleInfoCompileOption flags,
412 const CompilationObserver& observer)
413 : ModuleInfo(js, compileEsmModule(js, name, content, compileCache, flags, observer)) {}
414 
415ModuleRegistry::ModuleInfo::ModuleInfo(jsg::Lock& js,
416 kj::StringPtr name,
417 kj::Maybe<kj::ArrayPtr<const kj::StringPtr>> maybeExports,
418 SyntheticModuleInfo synthetic)
419 : ModuleInfo(js, createSyntheticModule(js, name, maybeExports), kj::mv(synthetic)) {
420 KJ_IF_SOME(exports, maybeExports) {
421 maybeNamedExports = KJ_MAP(name, exports) { return kj::str(name); };
422 }
423}
424 
425jsg::JsValue ModuleRegistry::CommonJsModuleInfo::getExports(jsg::Lock& js) {
426 return provider->getExports(js);
427}
428 
429ModuleRegistry::CapnpModuleInfo::CapnpModuleInfo(
430 Value fileScope, kj::HashMap<kj::StringPtr, jsg::Value> topLevelDecls)
431 : fileScope(kj::mv(fileScope)),
432 topLevelDecls(kj::mv(topLevelDecls)) {}
433 
434v8::Local<v8::WasmModuleObject> compileWasmModule(
435 jsg::Lock& js, kj::ArrayPtr<const uint8_t> code, const CompilationObserver& observer) {
436 // destroy the observer after compilation finishes to indicate the end of the process.
437 auto compilationObserver = observer.onWasmCompilationStart(js.v8Isolate, code.size());
438 
439 return jsg::check(v8::WasmModuleObject::Compile(
440 js.v8Isolate, v8::MemorySpan<const uint8_t>(code.begin(), code.size())));
441}
442 
443// ======================================================================================
444 
445kj::Maybe<kj::OneOf<kj::String, ModuleRegistry::ModuleInfo>> tryResolveFromFallbackService(Lock& js,
446 const kj::Path& specifier,
447 kj::Maybe<const kj::Path&>& referrer,
448 CompilationObserver& observer,
449 ModuleRegistry::ResolveMethod method,
450 kj::Maybe<kj::StringPtr> rawSpecifier) {
451 auto& isolateBase = IsolateBase::from(js.v8Isolate);
452 KJ_IF_SOME(fallback, isolateBase.tryGetModuleFallback()) {
453 kj::Maybe<kj::String> maybeRef;
454 KJ_IF_SOME(ref, referrer) {
455 maybeRef = ref.toString(true);
456 }
457 return fallback(js, specifier.toString(true), kj::mv(maybeRef), observer, method, rawSpecifier);
458 }
459 return kj::none;
460}
461 
462JsValue ModuleRegistry::requireImpl(Lock& js, ModuleInfo& info, RequireImplOptions options) {
463 auto module = info.module.getHandle(js);
464 
465 // If the module status is evaluating or instantiating then the module is likely
466 // has a circular dependency on itself. If the module is a CommonJS or NodeJS
467 // module, we can return the exports object directly here.
468 if (module->GetStatus() == v8::Module::Status::kEvaluating ||
469 module->GetStatus() == v8::Module::Status::kInstantiating) {
470 KJ_IF_SOME(synth, info.maybeSynthetic) {
471 KJ_IF_SOME(cjs, synth.tryGet<ModuleRegistry::CommonJsModuleInfo>()) {
472 return cjs.getExports(js);
473 }
474 }
475 }
476 
477 // When using require(...) we previously allowed the required modules to use
478 // top-level await. With a compat flag we disable use of top-level await but
479 // ONLY when the module is synchronously required. The same module being imported
480 // either statically or dynamically can still use TLA. This aligns with behavior
481 // being implemented in other JS runtimes.
482 auto& isolateBase = IsolateBase::from(js.v8Isolate);
483 jsg::InstantiateModuleOptions opts = jsg::InstantiateModuleOptions::DEFAULT;
484 if (!isolateBase.isTopLevelAwaitEnabled()) {
485 opts = jsg::InstantiateModuleOptions::NO_TOP_LEVEL_AWAIT;
486 
487 // If the module was already evaluated, let's check if it is async.
488 // If it is, we will throw an error. This case can happen if a previous
489 // attempt to require the module failed because the module was async.
490 if (module->GetStatus() == v8::Module::kEvaluated) {
491 JSG_REQUIRE(!module->IsGraphAsync(), Error,
492 "Top-level await in module is not permitted at this time.");
493 }
494 }
495 
496 jsg::instantiateModule(js, module, opts);
497 
498 if (info.maybeSynthetic == kj::none) {
499 // If the module is an ESM and the __cjsUnwrapDefault flag is set to true, we will
500 // always return the default export regardless of the options.
501 // Otherwise fallback to the options. This is an early version of the "module.exports"
502 // convention that Node.js finally adopted for require(esm) that was not officially
503 // adopted but there are a handful of modules in the ecosystem that supported it
504 // early. It's trivial for us to support here so let's just do so.
505 JsObject obj(module->GetModuleNamespace().As<v8::Object>());
506 if (obj.get(js, "__cjsUnwrapDefault"_kj) == js.boolean(true)) {
507 return obj.get(js, "default"_kj);
508 }
509 // If the ES Module namespace exports a "module.exports" key then that will be the
510 // export that is returned by the require(...) call per Node.js' recently added
511 // require(esm) support.
512 // See: https://nodejs.org/docs/latest/api/modules.html#loading-ecmascript-modules-using-require
513 if (obj.has(js, "module.exports"_kj)) {
514 // We only want to return the value if it is explicitly specified, otherwise we'll
515 // always be returning undefined.
516 return obj.get(js, "module.exports"_kj);
517 }
518 }
519 
520 // Originally, require returned an object like `{default: module.exports}` when we really
521 // intended to return the module exports raw. We should be extracting `default` here.
522 // When Node.js recently finally adopted require(esm), they adopted the default behavior
523 // of exporting the module namespace, which is fun. We'll stick with our default here for
524 // now but users can get Node.js-like behavior by switching off the
525 // exportCommonJsDefaultNamespace compat flag.
526 if (options == RequireImplOptions::EXPORT_DEFAULT) {
527 return JsValue(check(module->GetModuleNamespace().As<v8::Object>()->Get(
528 js.v8Context(), v8StrIntern(js.v8Isolate, "default"))));
529 }
530 
531 // When the flag is disabled, return the original module namespace
532 // to maintain backward compatibility (same object as ESM import returns).
533 if (!isRequireReturnsDefaultExportEnabled(js)) {
534 return JsValue(module->GetModuleNamespace());
535 }
536 
537 // When require_returns_default_export flag is enabled:
538 // 1. If module has default export: return it directly (it should be mutable)
539 // 2. If no default export: return a mutable copy of the namespace
540 // This matches Node.js require(esm) behavior and allows monkey-patching.
541 // See: https://github.com/cloudflare/workerd/issues/5844
542 
543 JsObject moduleNamespace(module->GetModuleNamespace().As<v8::Object>());
544 if (moduleNamespace.has(js, "default"_kj)) {
545 // Default export should be a regular mutable object, return it directly.
546 // No caching needed here since we're returning the module's own default export.
547 // Note: Modules should NOT re-export namespace objects as their default.
548 // If they do, the default export will be read-only which breaks monkey-patching.
549 return moduleNamespace.get(js, "default"_kj);
550 }
551 
552 // No default export - return a cached mutable copy of the namespace, or create one.
553 KJ_IF_SOME(cached, info.maybeMutableExports) {
554 return JsValue(cached.getHandle(js));
555 }
556 auto mutableExports = createMutableModuleExports(js, moduleNamespace);
557 info.maybeMutableExports = V8Ref<v8::Object>(js.v8Isolate, mutableExports);
558 return mutableExports;
559}
560 
561} // namespace workerd::jsg