Skip to content
File

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

86.3 KB
1// Copyright (c) 2017-2026 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 "modules-new.h"
6 
7#include "buffersource.h"
8 
9#include <workerd/jsg/function.h>
10#include <workerd/jsg/jsg.h>
11#include <workerd/jsg/util.h>
12 
13#include <kj/mutex.h>
14#include <kj/table.h>
15 
16namespace workerd::jsg::modules {
17 
18namespace {
19// Returns kj::none if this given module is incapable of resolving the given
20// context. Otherwise, returns the module.
21kj::Maybe<const Module&> checkModule(const ResolveContext& context, const Module& module) {
22 if (!module.evaluateContext(context)) {
23 return kj::none;
24 }
25 return module;
26};
27 
28// If the specifier is "node:process", returns the appropriate internal module
29// URL based on the enable_nodejs_process_v2 flag. Otherwise returns kj::none.
30kj::Maybe<const Url&> maybeRedirectNodeProcess(Lock& js, kj::ArrayPtr<const char> spec) {
31 if (spec == "node:process"_kjb.asChars()) {
32 static const auto publicProcess = "node-internal:public_process"_url;
33 static const auto legacyProcess = "node-internal:legacy_process"_url;
34 return isNodeJsProcessV2Enabled(js) ? publicProcess : legacyProcess;
35 }
36 return kj::none;
37}
38 
39kj::String specifierToString(jsg::Lock& js, v8::Local<v8::String> spec) {
40 // Source files in workers end up being converted to UTF-8 bytes, so if the specifier
41 // string contains non-ASCII unicode characters, those will be directly encoded as UTF-8
42 // bytes, which unfortunately end up double-encoded if we try to read them using the
43 // regular js.toString() method. Doh! Fortunately they come through as one-byte strings,
44 // so we can detect that case and handle those correctly here.
45 if (spec->ContainsOnlyOneByte()) {
46 auto buf = kj::heapArray<char>(spec->Length() + 1);
47 spec->WriteOneByteV2(js.v8Isolate, 0, spec->Length(), buf.asBytes().begin(),
48 v8::String::WriteFlags::kNullTerminate);
49 KJ_ASSERT(buf[buf.size() - 1] == '\0');
50 return kj::String(kj::mv(buf));
51 }
52 return js.toString(spec);
53}
54 
55// Ensure that the given module has been instantiated or errored.
56// If false is returned, then an exception should have been scheduled
57// on the isolate.
58bool ensureInstantiated(Lock& js,
59 v8::Local<v8::Module> module,
60 const CompilationObserver& observer,
61 const Module& self) {
62 return module->GetStatus() != v8::Module::kUninstantiated ||
63 self.instantiate(js, module, observer);
64}
65 
66constexpr ResolveContext::Type moduleTypeToResolveContextType(Module::Type type) {
67 switch (type) {
68 case Module::Type::BUNDLE: {
69 return ResolveContext::Type::BUNDLE;
70 }
71 case Module::Type::BUILTIN: {
72 return ResolveContext::Type::BUILTIN;
73 }
74 case Module::Type::BUILTIN_ONLY: {
75 return ResolveContext::Type::BUILTIN_ONLY;
76 }
77 case Module::Type::FALLBACK: {
78 return ResolveContext::Type::BUNDLE;
79 }
80 }
81 KJ_UNREACHABLE;
82}
83 
84constexpr ModuleBundle::Type toModuleBuilderType(ModuleBundle::BuiltinBuilder::Type type) {
85 switch (type) {
86 case ModuleBundle::BuiltinBuilder::Type::BUILTIN:
87 return ModuleBundle::Type::BUILTIN;
88 case ModuleBundle::BuiltinBuilder::Type::BUILTIN_ONLY:
89 return ModuleBundle::Type::BUILTIN_ONLY;
90 }
91 KJ_UNREACHABLE;
92}
93 
94// The implementation of Module for ESM.
95class EsModule final: public Module {
96 public:
97 explicit EsModule(Url id, Type type, Flags flags, kj::ArrayPtr<const char> source)
98 : Module(kj::mv(id), type, flags | Flags::ESM | Flags::EVAL),
99 source(source),
100 cachedData(kj::none) {
101 KJ_DASSERT(isEsm());
102 }
103 KJ_DISALLOW_COPY_AND_MOVE(EsModule);
104 
105 v8::MaybeLocal<v8::Module> getDescriptor(
106 Lock& js, const CompilationObserver& observer) const override {
107 auto metrics = observer.onEsmCompilationStart(js.v8Isolate, kj::str(id().getHref()),
108 type() == Type::BUNDLE ? CompilationObserver::Option::BUNDLE
109 : CompilationObserver::Option::BUILTIN);
110 
111 static constexpr int resourceLineOffset = 0;
112 static constexpr int resourceColumnOffset = 0;
113 static constexpr bool resourceIsSharedCrossOrigin = false;
114 static constexpr int scriptId = -1;
115 static constexpr bool resourceIsOpaque = false;
116 static constexpr bool isWasm = false;
117 v8::ScriptOrigin origin(js.str(id().getHref()), resourceLineOffset, resourceColumnOffset,
118 resourceIsSharedCrossOrigin, scriptId, {}, resourceIsOpaque, isWasm, true);
119 
120 auto options = v8::ScriptCompiler::CompileOptions::kNoCompileOptions;
121 bool cacheWasRejected = false;
122 
123 v8::Local<v8::Module> module;
124 {
125 v8::ScriptCompiler::CachedData* data = nullptr;
126 
127 // Check to see if we have cached compilation data for this module.
128 // Importantly, we want to allow multiple threads to be capable of
129 // reading and using the cached data without blocking each other
130 // (which is fine since using the cache does not modify it).
131 auto lock = cachedData.lockShared();
132 KJ_IF_SOME(c, *lock) {
133 // We new new here because v8 will take ownership of the CachedData instance,
134 // even tho we are maintaining ownership of the underlying buffer.
135 data = new v8::ScriptCompiler::CachedData(
136 c->data, c->length, v8::ScriptCompiler::CachedData::BufferPolicy::BufferNotOwned);
137 auto check = data->CompatibilityCheck(js.v8Isolate);
138 if (check != v8::ScriptCompiler::CachedData::kSuccess) {
139 // The cached data is not compatible with the current isolate. Let's
140 // not try using it.
141 delete data;
142 data = nullptr;
143 } else {
144 observer.onCompileCacheFound(js.v8Isolate);
145 }
146 }
147 
148 // Note that the Source takes ownership of the CachedData pointer that we pass in.
149 // (but not the actual buffer it holds). Do not use data after this point.
150 v8::ScriptCompiler::Source source(js.strExtern(this->source), origin, data);
151 
152 auto maybeCached = source.GetCachedData();
153 if (maybeCached != nullptr) {
154 if (!maybeCached->rejected) {
155 // We found valid cached data and set the option to consume it to avoid
156 // compiling again below...
157 options = v8::ScriptCompiler::CompileOptions::kConsumeCodeCache;
158 } else {
159 // In this case we'll just log a warning and continue on. This is potentially
160 // a signal that something with the compile cache is not working correctly but
161 // it is not a fatal error. If we spot this in the wild, it warrants some
162 // investigation but is not critical.
163 LOG_WARNING_ONCE("NOSENTRY Cached data for an ESM module was rejected");
164 observer.onCompileCacheRejected(js.v8Isolate);
165 cacheWasRejected = true;
166 }
167 }
168 
169 // Let's just double check that our options are valid. They should be
170 // since we're either consuming cached data or not using any options at all.
171 KJ_ASSERT(v8::ScriptCompiler::CompileOptionsIsValid(options));
172 if (!v8::ScriptCompiler::CompileModule(js.v8Isolate, &source, options).ToLocal(&module)) {
173 return {};
174 }
175 }
176 
177 // If the cached data was rejected, clear it so subsequent isolates don't
178 // repeatedly check stale data. We then fall through to regenerate the cache
179 // below. In practice this is exceedingly unlikely since V8 version changes
180 // are the primary cause of cache rejection and we don't change V8 versions
181 // within a single binary, but we handle it for correctness.
182 if (cacheWasRejected) {
183 auto lock = cachedData.lockExclusive();
184 *lock = kj::none;
185 }
186 
187 // If options is still kNoCompileOptions at this point, it means that we did not
188 // find any cached data for this module, or the cached data was rejected. In
189 // either case, we try generating it and store it. Multiple threads can end up
190 // lining up here to acquire the lock and generate the cache. We'll test to see
191 // if the cached data is still empty once the lock is acquired, and if it is
192 // not, we'll skip generation.
193 if (options == v8::ScriptCompiler::CompileOptions::kNoCompileOptions) {
194 auto lock = cachedData.lockExclusive();
195 if (*lock == kj::none) {
196 if (auto ptr = v8::ScriptCompiler::CreateCodeCache(module->GetUnboundModuleScript())) {
197 // Using the technically private kj::_::HeapDisposer to wrap the V8-allocated
198 // CachedData in a kj::Own. This pattern has precedent in io-own.h.
199 kj::Own<v8::ScriptCompiler::CachedData> cached(
200 ptr, kj::_::HeapDisposer<v8::ScriptCompiler::CachedData>::instance);
201 *lock = kj::mv(cached);
202 observer.onCompileCacheGenerated(js.v8Isolate);
203 } else {
204 observer.onCompileCacheGenerationFailed(js.v8Isolate);
205 }
206 }
207 }
208 
209 return module;
210 }
211 
212 private:
213 v8::MaybeLocal<v8::Value> actuallyEvaluate(
214 Lock& js, v8::Local<v8::Module> module, const CompilationObserver& observer) const override {
215 return module->Evaluate(js.v8Context());
216 }
217 
218 v8::MaybeLocal<v8::Value> evaluate(Lock& js,
219 v8::Local<v8::Module> module,
220 const CompilationObserver& observer,
221 const Evaluator& maybeEvaluate) const override {
222 if (!ensureInstantiated(js, module, observer, *this)) {
223 if (!js.v8Isolate->HasPendingException()) {
224 js.v8Isolate->ThrowError(js.str("Failed to instantiate module"_kj));
225 }
226 return {};
227 }
228 
229 KJ_IF_SOME(result, maybeEvaluate(js, *this, module, observer)) {
230 v8::Local<v8::Value> val = result;
231 return val;
232 }
233 
234 return actuallyEvaluate(js, module, observer);
235 }
236 
237 kj::ArrayPtr<const char> source;
238 
239 // The cachedData holds the cached compilation data for this module, if any. It is
240 // generated on-demand the first time the module is compiled, if possible.
241 kj::MutexGuarded<kj::Maybe<kj::Own<v8::ScriptCompiler::CachedData>>> cachedData;
242};
243 
244// A SyntheticModule is essentially any type of module that is not backed by an ESM
245// script. More specifically, it's a module in which we synthetically construct the
246// module namespace (i.e. the exports) and the evaluation steps. This is used for things
247// like CommonJS modules, JSON modules, etc.
248class SyntheticModule final: public Module {
249 public:
250 // The name of the default export.
251 static constexpr auto DEFAULT = "default"_kjc;
252 
253 SyntheticModule(Url id,
254 Type type,
255 ModuleBundle::BundleBuilder::EvaluateCallback callback,
256 kj::Array<kj::String> namedExports,
257 Flags flags = Flags::NONE,
258 ContentType contentType = ContentType::NONE)
259 : Module(kj::mv(id), type, flags, contentType),
260 callback(kj::mv(callback)),
261 namedExports(kj::mv(namedExports)) {
262 // Synthetic modules can never be ESM or Main
263 KJ_DASSERT(!isEsm() && !isMain());
264 }
265 
266 v8::MaybeLocal<v8::Module> getDescriptor(Lock& js, const CompilationObserver&) const override {
267 // We add one to the size to accomodate the default export.
268 v8::LocalVector<v8::String> exports(js.v8Isolate, namedExports.size() + 1);
269 int n = 0;
270 exports[n++] = js.strIntern(DEFAULT);
271 for (const auto& exp: namedExports) {
272 exports[n++] = js.strIntern(exp);
273 }
274 return v8::Module::CreateSyntheticModule(js.v8Isolate, js.str(id().getHref()),
275 v8::MemorySpan<const v8::Local<v8::String>>(exports.data(), exports.size()),
276 evaluationSteps);
277 }
278 
279 private:
280 static v8::MaybeLocal<v8::Value> evaluationSteps(
281 v8::Local<v8::Context> context, v8::Local<v8::Module> module);
282 
283 v8::MaybeLocal<v8::Value> actuallyEvaluate(
284 Lock& js, v8::Local<v8::Module> module, const CompilationObserver& observer) const override {
285 // The return value will be a resolved promise.
286 v8::Local<v8::Promise::Resolver> resolver;
287 if (!v8::Promise::Resolver::New(js.v8Context()).ToLocal(&resolver)) {
288 return {};
289 }
290 
291 ModuleNamespace ns(module, namedExports);
292 if (!callback(js, id(), ns, observer) ||
293 resolver->Resolve(js.v8Context(), js.v8Undefined()).IsNothing()) {
294 // An exception should already be scheduled with the isolate
295 return {};
296 }
297 
298 return resolver->GetPromise();
299 }
300 
301 v8::MaybeLocal<v8::Value> evaluate(Lock& js,
302 v8::Local<v8::Module> module,
303 const CompilationObserver& observer,
304 const Evaluator& maybeEvaluate) const override {
305 if (!ensureInstantiated(js, module, observer, *this)) {
306 if (!js.v8Isolate->HasPendingException()) {
307 js.v8Isolate->ThrowError(js.str("Failed to instantiate module"_kj));
308 }
309 return {};
310 }
311 // If this synthetic module is marked with Flags::EVAL, and the evalCallback
312 // is specified, then we defer evaluation to the given callback.
313 if (isEval()) {
314 KJ_IF_SOME(result, maybeEvaluate(js, *this, module, observer)) {
315 v8::Local<v8::Value> val = result;
316 return val;
317 }
318 }
319 return module->Evaluate(js.v8Context());
320 }
321 
322 // Marked mutable because kj::Function::operator() is non-const, but evaluation
323 // callbacks are conceptually const — they produce new JS objects each time without
324 // modifying the module's logical state. The callback is only ever invoked while
325 // holding the isolate lock, so concurrent mutation is not a concern.
326 mutable ModuleBundle::BundleBuilder::EvaluateCallback callback;
327 kj::Array<kj::String> namedExports;
328};
329 
330#pragma clang diagnostic push
331#pragma clang diagnostic ignored "-Wunused-function"
332WD_STRONG_BOOL(SourcePhase);
333#pragma clang diagnostic pop
334 
335// Parses import attributes from V8's FixedArray format (key-value-location triples).
336// Returns the value of the "type" attribute if present, or kj::none if no attributes.
337// Throws TypeError for any unrecognized attribute keys or unsupported type values.
338kj::Maybe<kj::StringPtr> parseImportAttributes(
339 Lock& js, v8::Local<v8::FixedArray> import_attributes) {
340 if (import_attributes.IsEmpty() || import_attributes->Length() == 0) {
341 return kj::none;
342 }
343 // V8 encodes import attributes as a FixedArray of triples: [key, value, location, ...]
344 kj::Maybe<kj::StringPtr> typeValue;
345 for (int i = 0; i < import_attributes->Length(); i += 3) {
346 auto key = js.toString(import_attributes->Get(i).As<v8::String>());
347 if (key == "type"_kj) {
348 auto value = js.toString(import_attributes->Get(i + 1).As<v8::String>());
349 if (value == "json"_kj) {
350 typeValue = "json"_kjc;
351 } else if (value == "text"_kj) {
352 typeValue = "text"_kjc;
353 } else if (value == "bytes"_kj) {
354 typeValue = "bytes"_kjc;
355 } else {
356 js.throwException(
357 js.typeError(kj::str("Unsupported import attribute type: \"", value, "\"")));
358 }
359 } else {
360 js.throwException(js.typeError(kj::str("Unsupported import attribute: \"", key, "\"")));
361 }
362 }
363 return typeValue;
364}
365 
366// Validates that the resolved module's content type matches the import attribute "type" value.
367// Throws TypeError on mismatch. Does nothing if no type attribute was specified.
368void validateImportType(
369 Lock& js, kj::Maybe<kj::StringPtr> importType, const Module& module, kj::StringPtr specifier) {
370 KJ_IF_SOME(type, importType) {
371 // Import Text (TC39 Stage 3) and Import Bytes (TC39 Stage 2.7) are
372 // recognized but not yet supported. Text support is pending the proposal
373 // reaching Stage 4. Bytes support requires Uint8Array backed by an
374 // immutable ArrayBuffer, which is not yet implemented.
375 if (type == "text"_kj) {
376 js.throwException(js.typeError("Import attribute type \"text\" is not yet supported"_kj));
377 }
378 if (type == "bytes"_kj) {
379 js.throwException(js.typeError("Import attribute type \"bytes\" is not yet supported"_kj));
380 }
381 
382 Module::ContentType expected = Module::ContentType::NONE;
383 if (type == "json"_kj) {
384 expected = Module::ContentType::JSON;
385 }
386 // TODO(later): Enable when Import Text (TC39) reaches Stage 4.
387 // else if (type == "text"_kj) {
388 // expected = Module::ContentType::TEXT;
389 // }
390 // TODO(later): Enable when immutable ArrayBuffer is implemented.
391 // else if (type == "bytes"_kj) {
392 // expected = Module::ContentType::DATA;
393 // }
394 if (module.contentType() != expected) {
395 js.throwException(
396 js.typeError(kj::str("Module \"", specifier, "\" is not of type \"", type, "\"")));
397 }
398 }
399}
400 
401// Binds a ModuleRegistry to an Isolate.
402class IsolateModuleRegistry final {
403 public:
404 static IsolateModuleRegistry& from(v8::Isolate* isolate) {
405 return KJ_ASSERT_NONNULL(jsg::getAlignedPointerFromEmbedderData<IsolateModuleRegistry>(
406 isolate->GetCurrentContext(), jsg::ContextPointerSlot::MODULE_REGISTRY));
407 }
408 
409 struct SpecifierContext final {
410 ResolveContext::Type type;
411 Url id;
412 SpecifierContext(const ResolveContext& resolveContext)
413 : type(resolveContext.type),
414 id(resolveContext.normalizedSpecifier.clone()) {}
415 bool operator==(const SpecifierContext& other) const {
416 return type == other.type && id == other.id;
417 }
418 uint hashCode() const {
419 return kj::hashCode(type, id);
420 }
421 };
422 
423 struct Entry final {
424 HashableV8Ref<v8::Module> key;
425 SpecifierContext context;
426 const Module& module;
427 };
428 
429 IsolateModuleRegistry(
430 Lock& js, const ModuleRegistry& registry, const CompilationObserver& observer);
431 KJ_DISALLOW_COPY_AND_MOVE(IsolateModuleRegistry);
432 
433 // Used to implement the normal static import of modules (using `import ... from`).
434 // Returns the v8::Module descriptor. If an empty v8::MaybeLocal is returned, then
435 // an exception has been scheduled with the isolate.
436 v8::MaybeLocal<v8::Module> resolve(Lock& js, const ResolveContext& context) {
437 // Do we already have a cached module for this context?
438 KJ_IF_SOME(found, lookupCache.find<kj::HashIndex<ContextCallbacks>>(context)) {
439 return found.key.getHandle(js);
440 }
441 // No? That's OK, let's look it up.
442 KJ_IF_SOME(found, resolveWithCaching(js, context)) {
443 return found.key.getHandle(js);
444 }
445 
446 // Nothing found? Aw... fail!
447 JSG_FAIL_REQUIRE(Error, kj::str("Module not found: ", context.normalizedSpecifier.getHref()));
448 }
449 
450 // Used to implement the async dynamic import of modules (using `await import(...)`)
451 // Returns a promise that is resolved once the module is resolved. If any empty
452 // v8::MaybeLocal is returned, then an exception has been scheduled with the isolate.
453 v8::MaybeLocal<v8::Promise> dynamicResolve(Lock& js,
454 Url normalizedSpecifier,
455 Url referrer,
456 kj::StringPtr rawSpecifier,
457 SourcePhase sourcePhase,
458 kj::Maybe<kj::StringPtr> importType = kj::none) {
459 // Note: Takes v8::Local<v8::Module> and const Module& directly rather than
460 // Entry& for the same reason as require()'s evaluate lambda — the lookupCache
461 // table may rehash during evaluate(), invalidating Entry& references.
462 static constexpr auto evaluate =
463 [](Lock& js, v8::Local<v8::Module> module, const Module& moduleDef,
464 const CompilationObserver& observer, const Module::Evaluator& maybeEvaluate) {
465 return js
466 .toPromise(
467 check(moduleDef.evaluate(js, module, observer, maybeEvaluate)).As<v8::Promise>())
468 .then(js, [module = js.v8Ref(module)](Lock& js, Value) mutable -> Promise<Value> {
469 return js.resolvedPromise(js.v8Ref(module.getHandle(js)->GetModuleNamespace()));
470 });
471 };
472 
473 return js.wrapSimplePromise(js.tryCatch([&] -> Promise<Value> {
474 // The referrer should absolutely already be known to the registry
475 // or something bad happened.
476 auto& referring = JSG_REQUIRE_NONNULL(lookupCache.find<kj::HashIndex<UrlCallbacks>>(referrer),
477 TypeError, kj::str("Referring module not found in the registry: ", referrer.getHref()));
478 
479 // Now that we know the referrer module, we can set the context for the
480 // next resolve. In particular, the "type" of the context is determine
481 // by the type of the referring module.
482 ResolveContext context = {
483 .type = moduleTypeToResolveContextType(referring.module.type()),
484 .source = ResolveContext::Source::DYNAMIC_IMPORT,
485 .normalizedSpecifier = normalizedSpecifier,
486 .referrerNormalizedSpecifier = referrer,
487 .rawSpecifier = rawSpecifier,
488 };
489 
490 auto handleFoundModule = [&](Entry& found) -> Promise<Value> {
491 // Extract module handle and Module& before calling evaluate, since
492 // evaluate may trigger table rehashing that invalidates the Entry&.
493 auto v8Module = found.key.getHandle(js);
494 auto& moduleDef = found.module;
495 
496 // Validate import type attribute against the resolved module's content type.
497 validateImportType(js, importType, moduleDef, rawSpecifier);
498 
499 if (v8Module->GetStatus() == v8::Module::kErrored) {
500 return js.rejectedPromise<Value>(v8Module->GetException());
501 }
502 
503 auto evaluatePromise =
504 evaluate(js, v8Module, moduleDef, getObserver(), inner.getEvaluator());
505 auto isWasm = moduleDef.isWasm();
506 
507 if (!sourcePhase) {
508 return evaluatePromise;
509 } else {
510 // We only support source phase imports for Wasm modules.
511 // Source phase imports provide uninstantiated and unlinked representations for modules, as distinct
512 // from module instances. They effectively represent the compiled module without state.
513 // WebAssembly.Module is this representation for WebAssembly.
514 // JS source module handles as an instance of `ModuleSource` will be supported in due course,
515 // but are specified in a different spec ESM Phase Imports (https://github.com/tc39/proposal-esm-phase-imports).
516 // For builtins and other synthetic modules, there are currently no plans to make a source phase
517 // representation available, so that these would remain with the specified syntax error as
518 // implemented below.
519 if (isWasm) {
520 return evaluatePromise.then(js,
521 [normalizedSpecifier = normalizedSpecifier.clone()](
522 Lock& js, Value namespaceValue) -> Value {
523 auto moduleNamespace = namespaceValue.getHandle(js).As<v8::Object>();
524 v8::Local<v8::Value> defaultExport;
525 if (moduleNamespace->Get(js.v8Context(), js.strIntern("default"_kj))
526 .ToLocal(&defaultExport)) {
527 if (defaultExport->IsWasmModuleObject()) {
528 return js.v8Ref(defaultExport);
529 }
530 }
531 js.throwException(js.v8Ref(v8::Exception::SyntaxError(
532 js.str(kj::str("Source phase import not available for module: "_kj,
533 normalizedSpecifier.getHref())))));
534 });
535 }
536 return js.rejectedPromise<Value>(js.v8Ref(v8::Exception::SyntaxError(js.strIntern(kj::str(
537 "Source phase import not available for module: ", normalizedSpecifier.getHref())))));
538 }
539 };
540 
541 // Do we already have a cached module for this context?
542 KJ_IF_SOME(found, lookupCache.find<kj::HashIndex<ContextCallbacks>>(context)) {
543 return handleFoundModule(found);
544 }
545 
546 // No? That's OK, let's look it up.
547 KJ_IF_SOME(found, resolveWithCaching(js, context)) {
548 return handleFoundModule(found);
549 }
550 
551 // Nothing found? Aw... fail!
552 JSG_FAIL_REQUIRE(TypeError, kj::str("Module not found: ", normalizedSpecifier.getHref()));
553 }, [&](Value exception) -> Promise<Value> {
554 return js.rejectedPromise<Value>(kj::mv(exception));
555 }));
556 }
557 
558 enum class RequireOption {
559 DEFAULT = 0,
560 RETURN_EMPTY = 1 << 0,
561 NO_TOP_LEVEL_AWAIT = 1 << 1,
562 // When set, the default export is returned instead of the module namespace.
563 // This matches Node.js require() semantics where require() returns the
564 // default export (module.exports for CJS, default export for ESM builtins,
565 // parsed value for JSON, etc.).
566 UNWRAP_DEFAULT = 1 << 2,
567 };
568 
569 friend constexpr RequireOption operator|(RequireOption a, RequireOption b) {
570 return static_cast<RequireOption>(static_cast<int>(a) | static_cast<int>(b));
571 }
572 friend constexpr RequireOption operator&(RequireOption a, RequireOption b) {
573 return static_cast<RequireOption>(static_cast<int>(a) & static_cast<int>(b));
574 }
575 
576 // Used to implement the synchronous dynamic import of modules in support of APIs
577 // like the CommonJS require. Returns the instantiated/evaluated module namespace.
578 // If an empty v8::MaybeLocal is returned and the default option is given, then an
579 // exception has been scheduled.
580 v8::MaybeLocal<v8::Value> require(
581 Lock& js, const ResolveContext& context, RequireOption option = RequireOption::DEFAULT) {
582 // Returns either the module namespace or, when UNWRAP_DEFAULT is set and
583 // the module is not ESM, the default export from the namespace. This matches
584 // Node.js require() semantics: require('esm') returns the namespace,
585 // require('data.json') returns the parsed value.
586 // When UNWRAP_DEFAULT is set, returns the default export for all module types
587 // except user bundle ESM, which returns the namespace (matching Node.js require(esm)
588 // behavior). Builtin ESM returns default because workerd wraps CJS-style APIs in
589 // ESM default exports. Synthetic modules (CJS, JSON, Text, etc.) return default
590 // because that's where their value lives.
591 static constexpr auto maybeUnwrapDefault =
592 [](Lock& js, v8::Local<v8::Module> module, const Module& moduleDef,
593 RequireOption option) -> v8::MaybeLocal<v8::Value> {
594 auto ns = module->GetModuleNamespace().As<v8::Object>();
595 if ((option & RequireOption::UNWRAP_DEFAULT) == RequireOption::UNWRAP_DEFAULT) {
596 // User bundle ESM returns the full namespace, matching Node.js require(esm),
597 // unless the module has __cjsUnwrapDefault set (a convention used by bundlers
598 // like esbuild when transpiling CJS to ESM), in which case we return the
599 // default export.
600 if (moduleDef.type() == Module::Type::BUNDLE && moduleDef.isEsm()) {
601 auto unwrap = ns->Get(js.v8Context(), js.strIntern("__cjsUnwrapDefault"_kj));
602 v8::Local<v8::Value> unwrapValue;
603 if (unwrap.ToLocal(&unwrapValue) && unwrapValue->BooleanValue(js.v8Isolate)) {
604 return check(ns->Get(js.v8Context(), js.strIntern("default"_kj)));
605 }
606 return ns;
607 }
608 // Everything else (builtins, synthetic modules) returns the default export.
609 // Note: The default export may be a primitive (e.g. Text module returns a string).
610 // We cast to v8::Object here because require() returns MaybeLocal<Object>, but
611 // callers immediately convert to JsValue. The cast is safe because v8::Local is
612 // just a pointer wrapper.
613 return check(ns->Get(js.v8Context(), js.strIntern("default"_kj)));
614 }
615 return ns;
616 };
617 
618 // Note: This lambda takes v8::Local<v8::Module> and const Module& directly
619 // rather than Entry& because the lookupCache table may rehash during
620 // ensureInstantiated() or evaluate() (when V8 resolves static import
621 // dependencies via resolveModuleCallback -> resolveWithCaching -> upsert),
622 // which would invalidate any Entry& reference into the table.
623 static constexpr auto evaluate =
624 [](Lock& js, v8::Local<v8::Module> module, const Module& moduleDef, const Url& id,
625 const CompilationObserver& observer, const Module::Evaluator& maybeEvaluate,
626 RequireOption option) -> v8::MaybeLocal<v8::Value> {
627 auto status = module->GetStatus();
628 
629 // If status is kErrored, that means a prior attempt to evaluate the module
630 // failed. We simply propagate the same error here.
631 if (status == v8::Module::kErrored) {
632 js.throwException(JsValue(module->GetException()));
633 }
634 
635 // Circular dependencies should be fine when we are talking strictly
636 // about CJS/Node.js style modules. For ESM, it becomes more problematic
637 // because v8 will not allow us to grab the default export while the module
638 // is still evaluating.
639 
640 if (moduleDef.isEsm() && status == v8::Module::kEvaluating) {
641 JSG_FAIL_REQUIRE(Error, "Circular dependency when resolving module: ", id);
642 }
643 
644 // If the module has already been evaluated, or is in the process of being
645 // evaluated, return the module namespace object directly. Note that if the
646 // module is a synthetic module, and status is kEvaluating, it is possible
647 // and likely that the namespace has not yet been fully evaluated and will
648 // be incomplete here. This allows CJS circular dependencies to be supported
649 // to a degree. Just like in Node.js, however, such circular dependencies
650 // can still be problematic depending on how they are used.
651 if (status == v8::Module::kEvaluated || status == v8::Module::kEvaluating) {
652 return maybeUnwrapDefault(js, module, moduleDef, option);
653 }
654 
655 // Matches the require(esm) behavior implemented in Node.js, which is to
656 // throw if the module being imported uses top-level await.
657 if ((option & RequireOption::NO_TOP_LEVEL_AWAIT) == RequireOption::NO_TOP_LEVEL_AWAIT) {
658 // We have to ensure the module is instantiated before we can check for top-level await.
659 JSG_REQUIRE(ensureInstantiated(js, module, observer, moduleDef), Error,
660 "Failed to instantiate module: ", id);
661 JSG_REQUIRE(!module->IsGraphAsync(), Error,
662 "Top-level await is not supported in this context for module: ", id);
663 }
664 
665 // Evaluate the module and grab the default export from the module namespace.
666 auto promise =
667 check(moduleDef.evaluate(js, module, observer, maybeEvaluate)).As<v8::Promise>();
668 
669 // Run the microtasks to ensure that any promises that happen to be scheduled
670 // during the evaluation of the top-level scope have a chance to be settled.
671 // We only pump the microtasks queue if NO_TOP_LEVEL_AWAIT is not set.
672 if ((option & RequireOption::NO_TOP_LEVEL_AWAIT) != RequireOption::NO_TOP_LEVEL_AWAIT) {
673 js.runMicrotasks();
674 
675 static const auto kTopLevelAwaitError =
676 "Use of top-level await in a synchronously required module is restricted to "
677 "promises that are resolved synchronously. This includes any top-level awaits "
678 "in the entrypoint module for a worker."_kj;
679 
680 switch (promise->State()) {
681 case v8::Promise::kFulfilled: {
682 // This is what we want. The module namespace should be fully populated
683 // and evaluated at this point.
684 return maybeUnwrapDefault(js, module, moduleDef, option);
685 }
686 case v8::Promise::kRejected: {
687 // Oops, there was an error. We should throw it.
688 js.throwException(JsValue(promise->Result()));
689 break;
690 }
691 case v8::Promise::kPending: {
692 // The module evaluation could not complete in a single drain of the
693 // microtask queue. This means we've got a pending promise somewhere
694 // that is being awaited preventing the module from being ready to
695 // go. We can't have that! Throw! Throw!
696 JSG_FAIL_REQUIRE(Error, kTopLevelAwaitError, " Specifier: \"", id, "\".");
697 }
698 }
699 } else {
700 KJ_ASSERT(!module->IsGraphAsync() && promise->State() != v8::Promise::kPending,
701 "Top-level await is not supported in this context, so the module promise "
702 "should never be pending");
703 if (promise->State() == v8::Promise::kRejected) {
704 js.throwException(JsValue(promise->Result()));
705 }
706 return maybeUnwrapDefault(js, module, moduleDef, option);
707 }
708 KJ_UNREACHABLE;
709 };
710 
711 return js.tryCatch([&]() -> v8::MaybeLocal<v8::Value> {
712 KJ_IF_SOME(processUrl, maybeRedirectNodeProcess(js, context.normalizedSpecifier.getHref())) {
713 ResolveContext newContext{
714 .type = ResolveContext::Type::BUILTIN_ONLY,
715 .source = context.source,
716 .normalizedSpecifier = processUrl,
717 .referrerNormalizedSpecifier = context.referrerNormalizedSpecifier,
718 .rawSpecifier = context.rawSpecifier,
719 };
720 return require(js, newContext, option);
721 }
722 
723 // Do we already have a cached module for this context?
724 KJ_IF_SOME(found, lookupCache.find<kj::HashIndex<ContextCallbacks>>(context)) {
725 // Extract module handle and Module& before calling evaluate, since
726 // evaluate may trigger table rehashing that invalidates the Entry&.
727 auto foundModule = found.key.getHandle(js);
728 auto& foundModuleDef = found.module;
729 return evaluate(js, foundModule, foundModuleDef, context.normalizedSpecifier, getObserver(),
730 inner.getEvaluator(), option);
731 }
732 
733 KJ_IF_SOME(found, resolveWithCaching(js, context)) {
734 auto foundModule = found.key.getHandle(js);
735 auto& foundModuleDef = found.module;
736 return evaluate(js, foundModule, foundModuleDef, context.normalizedSpecifier, getObserver(),
737 inner.getEvaluator(), option);
738 }
739 
740 if ((option & RequireOption::RETURN_EMPTY) == RequireOption::RETURN_EMPTY) {
741 return {};
742 }
743 JSG_FAIL_REQUIRE(Error, kj::str("Module not found: ", context.normalizedSpecifier.getHref()));
744 }, [&](Value exception) -> v8::MaybeLocal<v8::Object> {
745 // Use the isolate to rethrow the exception here instead of using the lock.
746 js.v8Isolate->ThrowException(exception.getHandle(js));
747 return {};
748 });
749 }
750 
751 // Lookup a module that may have already been previously resolved and cached.
752 kj::Maybe<Entry&> lookup(Lock& js, v8::Local<v8::Module> module) {
753 return lookupCache
754 .find<kj::HashIndex<EntryCallbacks>>(HashableV8Ref<v8::Module>(js.v8Isolate, module))
755 .map([](Entry& entry) -> Entry& { return entry; });
756 }
757 
758 const jsg::Url& getBundleBase() const {
759 return inner.getBundleBase();
760 }
761 
762 private:
763 const ModuleRegistry& inner;
764 const CompilationObserver& observer;
765 
766 const CompilationObserver& getObserver() const {
767 return observer;
768 }
769 
770 struct EntryCallbacks final {
771 const HashableV8Ref<v8::Module>& keyForRow(const Entry& entry) const {
772 return entry.key;
773 }
774 bool matches(const Entry& entry, const HashableV8Ref<v8::Module>& key) const {
775 return entry.key == key;
776 }
777 uint hashCode(const HashableV8Ref<v8::Module>& ref) const {
778 return ref.hashCode();
779 }
780 };
781 
782 struct ContextCallbacks final {
783 const SpecifierContext& keyForRow(const Entry& entry) const {
784 return entry.context;
785 }
786 bool matches(const Entry& entry, const SpecifierContext& context) const {
787 return entry.context == context;
788 }
789 uint hashCode(const SpecifierContext& context) const {
790 return context.hashCode();
791 }
792 };
793 
794 struct UrlCallbacks final {
795 const Url& keyForRow(const Entry& entry) const {
796 return entry.context.id;
797 }
798 bool matches(const Entry& entry, const Url& id) const {
799 return entry.context.id == id;
800 }
801 uint hashCode(const Url& id) const {
802 return id.hashCode();
803 }
804 };
805 
806 // Resolves the module from the inner ModuleRegistry, caching the results.
807 kj::Maybe<Entry&> resolveWithCaching(
808 Lock& js, const ResolveContext& context) KJ_WARN_UNUSED_RESULT {
809 // Clone attributes so the fallback bundle callback can see them.
810 kj::HashMap<kj::StringPtr, kj::StringPtr> clonedAttrs;
811 for (const auto& [key, value]: context.attributes) {
812 clonedAttrs.insert(key, value);
813 }
814 ResolveContext innerContext{
815 // The type identifies the resolution context as a bundle, builtin, or builtin-only.
816 .type = context.type,
817 // The source identifies the method of resolution (static import, dynamic import, etc).
818 // This is passed along for informational purposes only.
819 .source = context.source,
820 // The inner registry should ignore all URL query parameters and fragments
821 .normalizedSpecifier = context.normalizedSpecifier.clone(
822 Url::EquivalenceOption::IGNORE_FRAGMENTS | Url::EquivalenceOption::IGNORE_SEARCH),
823 // The referrer is passed along for informational purposes only.
824 .referrerNormalizedSpecifier = context.referrerNormalizedSpecifier,
825 // The raw specifier and attributes are passed along for informational purposes
826 // (used by the fallback service protocol).
827 .rawSpecifier = context.rawSpecifier,
828 .attributes = kj::mv(clonedAttrs),
829 };
830 
831 KJ_IF_SOME(found, inner.lookup(innerContext)) {
832 return kj::Maybe<Entry&>(lookupCache.upsert(
833 Entry{
834 .key = HashableV8Ref<v8::Module>(
835 js.v8Isolate, check(found.getDescriptor(js, getObserver()))),
836 // Note that we cache specifically with the passed in context and not the
837 // innerContext that was created. This is because we want to use the original
838 // specifier URL (with query parameters and fragments) as part of the key for
839 // the lookup cache.
840 .context = context,
841 .module = found,
842 },
843 [](auto&, auto&&) {}));
844 }
845 return kj::none;
846 }
847 
848 kj::Table<Entry,
849 kj::HashIndex<EntryCallbacks>,
850 kj::HashIndex<ContextCallbacks>,
851 kj::HashIndex<UrlCallbacks>>
852 lookupCache;
853 friend class SyntheticModule;
854};
855 
856v8::MaybeLocal<v8::Value> SyntheticModule::evaluationSteps(
857 v8::Local<v8::Context> context, v8::Local<v8::Module> module) {
858 auto& js = Lock::current();
859 KJ_TRY {
860 auto& registry = IsolateModuleRegistry::from(js.v8Isolate);
861 KJ_IF_SOME(found, registry.lookup(js, module)) {
862 return found.module.actuallyEvaluate(js, module, registry.getObserver());
863 }
864 KJ_LOG(ERROR, "Synthetic module not found in registry for evaluation");
865 js.v8Isolate->ThrowError(js.str("Requested module does not exist"_kj));
866 return {};
867 }
868 KJ_CATCH(exception) {
869 auto ex = js.exceptionToJsValue(kj::mv(exception));
870 js.v8Isolate->ThrowException(ex.getHandle(js));
871 return {};
872 }
873}
874 
875// Set up the special `import.meta` property for the module.
876void importMeta(
877 v8::Local<v8::Context> context, v8::Local<v8::Module> module, v8::Local<v8::Object> meta) {
878 auto& js = Lock::current();
879 auto& registry = IsolateModuleRegistry::from(js.v8Isolate);
880 try {
881 js.tryCatch([&] {
882 KJ_IF_SOME(found, registry.lookup(js, module)) {
883 auto href = found.context.id.getHref();
884 
885 // V8's documentation says that the host should set the properties
886 // using CreateDataProperty.
887 
888 if (meta->CreateDataProperty(js.v8Context(), v8::Local<v8::String>(js.strIntern("main"_kj)),
889 js.boolean(found.module.isMain()))
890 .IsNothing()) {
891 // Notice that we do not use check here. There should be an exception
892 // scheduled with the isolate, it will take care of it at this point.
893 return;
894 }
895 
896 if (meta->CreateDataProperty(
897 js.v8Context(), v8::Local<v8::String>(js.strIntern("url"_kj)), js.str(href))
898 .IsNothing()) {
899 return;
900 }
901 
902 // The import.meta.resolve(...) function is effectively a shortcut for
903 // new URL(specifier, import.meta.url).href. The idea is that it allows
904 // resolving import specifiers relative to the current modules base URL.
905 // Note that we do not validate that the resolved URL actually matches
906 // anything in the registry.
907 auto resolve = js.wrapReturningFunction(js.v8Context(),
908 [href = kj::mv(href)](
909 Lock& js, const v8::FunctionCallbackInfo<v8::Value>& args) -> JsValue {
910 // Note that we intentionally use ToString here to coerce whatever value is given
911 // into a string or throw if it cannot be coerced.
912 auto specifier = js.toString(args[0]);
913 KJ_IF_SOME(resolved, Url::tryParse(specifier.asPtr(), href)) {
914 auto normalized = resolved.clone(Url::EquivalenceOption::NORMALIZE_PATH);
915 return js.str(normalized.getHref());
916 } else {
917 // If the specifier could not be parsed and resolved successfully,
918 // the spec says to return null.
919 return js.null();
920 }
921 });
922 
923 if (meta->CreateDataProperty(
924 js.v8Context(), v8::Local<v8::String>(js.strIntern("resolve"_kj)), resolve)
925 .IsNothing()) {
926 return;
927 }
928 }
929 }, [&](Value exception) {
930 // It would be exceedingly odd to end up here but we handle it anyway,
931 // just to ensure that we do not crash the isolate. The only thing we'll
932 // do is rethrow the error.
933 js.v8Isolate->ThrowException(exception.getHandle(js));
934 });
935 } catch (...) {
936 kj::throwFatalException(kj::getCaughtExceptionAsKj());
937 }
938}
939 
940// Templated implementation for both evaluation and source phase dynamic imports
941v8::MaybeLocal<v8::Promise> dynamicImportModuleCallback(v8::Local<v8::Context> context,
942 v8::Local<v8::Data> host_defined_options,
943 v8::Local<v8::Value> resource_name,
944 v8::Local<v8::String> specifier,
945 SourcePhase isSourcePhase,
946 v8::Local<v8::FixedArray> import_attributes) {
947 auto& js = Lock::current();
948 
949 // Since this method is called directly by V8, we don't want to use jsg::check
950 // or the js.rejectedPromise variants since those can throw JsExceptionThrown.
951 constexpr static auto rejected = [](jsg::Lock& js,
952 const jsg::JsValue& error) -> v8::MaybeLocal<v8::Promise> {
953 v8::Local<v8::Promise::Resolver> resolver;
954 if (!v8::Promise::Resolver::New(js.v8Context()).ToLocal(&resolver) ||
955 resolver->Reject(js.v8Context(), error).IsNothing()) {
956 return {};
957 }
958 return resolver->GetPromise();
959 };
960 
961 auto& registry = IsolateModuleRegistry::from(js.v8Isolate);
962 KJ_TRY {
963 return js.tryCatch([&]() -> v8::MaybeLocal<v8::Promise> {
964 auto spec = specifierToString(js, specifier);
965 
966 // Parse import attributes. Throws for unrecognized attribute keys.
967 // Returns the "type" value if specified, or kj::none.
968 auto importType = parseImportAttributes(js, import_attributes);
969 
970 Url referrer = ([&] {
971 if (resource_name.IsEmpty()) {
972 return registry.getBundleBase().clone();
973 }
974 auto str = js.toString(resource_name);
975 return KJ_ASSERT_NONNULL(Url::tryParse(str.asPtr()));
976 })();
977 
978 // If Node.js Compat v2 mode is enable, we have to check to see if the specifier
979 // is a bare node specifier and resolve it to a full node: URL.
980 if (isNodeJsCompatEnabled(js)) {
981 KJ_IF_SOME(nodeSpec, checkNodeSpecifier(spec)) {
982 spec = kj::mv(nodeSpec);
983 }
984 }
985 
986 // Handle process module redirection based on enable_nodejs_process_v2 flag
987 KJ_IF_SOME(processUrl, maybeRedirectNodeProcess(js, spec.asPtr())) {
988 auto processSpec = kj::str(processUrl.getHref());
989 return registry.dynamicResolve(
990 js, processUrl.clone(), kj::mv(referrer), processSpec, isSourcePhase, importType);
991 }
992 
993 KJ_IF_SOME(url, referrer.tryResolve(spec.asPtr())) {
994 return registry.dynamicResolve(js, url.clone(Url::EquivalenceOption::NORMALIZE_PATH),
995 kj::mv(referrer), spec, isSourcePhase, importType);
996 }
997 
998 // We were not able to parse the specifier. We'll return a rejected promise.
999 return rejected(js, js.typeError(kj::str("Invalid module specifier: ", spec)));
1000 }, [&](Value exception) -> v8::MaybeLocal<v8::Promise> {
1001 // If there are any synchronously thrown exceptions, we want to catch them
1002 // here and convert them into a rejected promise. The only exception are
1003 // fatal cases where the isolate is terminating which won't make it here
1004 // anyway.
1005 return rejected(js, jsg::JsValue(exception.getHandle(js)));
1006 });
1007 }
1008 KJ_CATCH(exception) {
1009 auto ex = js.exceptionToJsValue(kj::mv(exception));
1010 return rejected(js, ex.getHandle(js));
1011 }
1012}
1013 
1014// Wrapper functions to match the V8 callback signatures
1015v8::MaybeLocal<v8::Promise> dynamicImport(v8::Local<v8::Context> context,
1016 v8::Local<v8::Data> host_defined_options,
1017 v8::Local<v8::Value> resource_name,
1018 v8::Local<v8::String> specifier,
1019 v8::Local<v8::FixedArray> import_attributes) {
1020 return dynamicImportModuleCallback(
1021 context, host_defined_options, resource_name, specifier, SourcePhase::NO, import_attributes);
1022}
1023 
1024v8::MaybeLocal<v8::Promise> dynamicImportWithPhase(v8::Local<v8::Context> context,
1025 v8::Local<v8::Data> host_defined_options,
1026 v8::Local<v8::Value> resource_name,
1027 v8::Local<v8::String> specifier,
1028 v8::ModuleImportPhase phase,
1029 v8::Local<v8::FixedArray> import_attributes) {
1030 SourcePhase sourcePhase =
1031 (phase == v8::ModuleImportPhase::kSource) ? SourcePhase::YES : SourcePhase::NO;
1032 return dynamicImportModuleCallback(
1033 context, host_defined_options, resource_name, specifier, sourcePhase, import_attributes);
1034}
1035 
1036IsolateModuleRegistry::IsolateModuleRegistry(
1037 Lock& js, const ModuleRegistry& registry, const CompilationObserver& observer)
1038 : inner(registry),
1039 observer(observer),
1040 lookupCache(EntryCallbacks{}, ContextCallbacks{}, UrlCallbacks{}) {
1041 auto isolate = js.v8Isolate;
1042 auto context = isolate->GetCurrentContext();
1043 KJ_ASSERT(!context.IsEmpty());
1044 setAlignedPointerInEmbedderData(context, ContextPointerSlot::MODULE_REGISTRY, this);
1045 isolate->SetHostImportModuleDynamicallyCallback(&dynamicImport);
1046 isolate->SetHostImportModuleWithPhaseDynamicallyCallback(&dynamicImportWithPhase);
1047 isolate->SetHostInitializeImportMetaObjectCallback(&importMeta);
1048}
1049 
1050// Generalized module resolution callback that handles both evaluation and source phase imports
1051template <bool IsSourcePhase>
1052v8::MaybeLocal<std::conditional_t<IsSourcePhase, v8::Object, v8::Module>> resolveModuleCallback(
1053 v8::Local<v8::Context> context,
1054 v8::Local<v8::String> specifier,
1055 v8::Local<v8::FixedArray> import_attributes,
1056 v8::Local<v8::Module> referrer) {
1057 using ReturnType = std::conditional_t<IsSourcePhase, v8::Object, v8::Module>;
1058 auto& js = Lock::current();
1059 auto& registry = IsolateModuleRegistry::from(js.v8Isolate);
1060 
1061 return js.tryCatch([&]() -> v8::MaybeLocal<ReturnType> {
1062 auto spec = specifierToString(js, specifier);
1063 
1064 // The proposed specification for import attributes strongly recommends that
1065 // embedders reject import attributes and types they do not understand/implement.
1066 // This is because import attributes can alter the interpretation of a module.
1067 // Throwing an error for things we do not understand is the safest thing to do
1068 // for backwards compatibility.
1069 //
1070 // Parse import attributes. Throws for unrecognized attribute keys.
1071 auto importType = parseImportAttributes(js, import_attributes);
1072 
1073 ResolveContext::Type type = ResolveContext::Type::BUNDLE;
1074 
1075 // Clone the referrer URL out of the lookup cache entry rather than holding
1076 // a reference into it. The lookupCache table may rehash during resolve()
1077 // (via resolveWithCaching -> upsert), which would invalidate any reference
1078 // into the table's storage.
1079 Url referrerUrl = registry.lookup(js, referrer)
1080 .map([&](IsolateModuleRegistry::Entry& entry) -> Url {
1081 type = moduleTypeToResolveContextType(entry.module.type());
1082 return entry.context.id.clone();
1083 }).orDefault(registry.getBundleBase().clone());
1084 
1085 // If Node.js Compat v2 mode is enable, we have to check to see if the specifier
1086 // is a bare node specifier and resolve it to a full node: URL.
1087 if (isNodeJsCompatEnabled(js)) {
1088 KJ_IF_SOME(nodeSpec, checkNodeSpecifier(spec)) {
1089 spec = kj::mv(nodeSpec);
1090 }
1091 }
1092 
1093 // Handle process module redirection based on enable_nodejs_process_v2 flag
1094 if constexpr (!IsSourcePhase) {
1095 KJ_IF_SOME(processUrl, maybeRedirectNodeProcess(js, spec.asPtr())) {
1096 auto processSpec = kj::str(processUrl.getHref());
1097 ResolveContext resolveContext = {
1098 .type = ResolveContext::Type::BUILTIN_ONLY,
1099 .source = ResolveContext::Source::STATIC_IMPORT,
1100 .normalizedSpecifier = processUrl,
1101 .referrerNormalizedSpecifier = referrerUrl,
1102 .rawSpecifier = processSpec.asPtr(),
1103 };
1104 auto maybeResolved = registry.resolve(js, resolveContext);
1105 v8::Local<v8::Module> resolved;
1106 if (!maybeResolved.ToLocal(&resolved)) {
1107 return {};
1108 }
1109 if (resolved->GetStatus() == v8::Module::kErrored) {
1110 js.throwException(JsValue(resolved->GetException()));
1111 return {};
1112 }
1113 if (resolved->GetStatus() == v8::Module::kEvaluating) {
1114 js.throwException(
1115 js.typeError(kj::str("Circular dependency when resolving module: ", spec)));
1116 return {};
1117 }
1118 // Validate import type attribute against the resolved module's content type.
1119 KJ_IF_SOME(entry, registry.lookup(js, resolved)) {
1120 validateImportType(js, importType, entry.module, spec);
1121 }
1122 return resolved;
1123 }
1124 }
1125 
1126 KJ_IF_SOME(url, referrerUrl.tryResolve(spec)) {
1127 // Make sure that percent-encoding in the path is normalized so we can match correctly.
1128 auto normalized = url.clone(Url::EquivalenceOption::NORMALIZE_PATH);
1129 ResolveContext resolveContext = {
1130 .type = type,
1131 .source = ResolveContext::Source::STATIC_IMPORT,
1132 .normalizedSpecifier = normalized,
1133 .referrerNormalizedSpecifier = referrerUrl,
1134 .rawSpecifier = spec.asPtr(),
1135 };
1136 
1137 auto maybeResolved = registry.resolve(js, resolveContext);
1138 
1139 v8::Local<v8::Module> resolved;
1140 if (!maybeResolved.ToLocal(&resolved)) {
1141 return {};
1142 }
1143 
1144 // If the resolved module is in an errored state, we will rethrow the same exception here.
1145 if (resolved->GetStatus() == v8::Module::kErrored) {
1146 js.throwException(JsValue(resolved->GetException()));
1147 return {};
1148 }
1149 if (resolved->GetStatus() == v8::Module::kEvaluating) {
1150 js.throwException(
1151 js.typeError(kj::str("Circular dependency when resolving module: ", spec)));
1152 return v8::MaybeLocal<ReturnType>();
1153 }
1154 
1155 // Validate import type attribute against the resolved module's content type.
1156 KJ_IF_SOME(entry, registry.lookup(js, resolved)) {
1157 validateImportType(js, importType, entry.module, spec);
1158 }
1159 
1160 if constexpr (!IsSourcePhase) {
1161 return resolved;
1162 } else {
1163 KJ_IF_SOME(entry, registry.lookup(js, resolved)) {
1164 // We only support source phase imports for Wasm modules.
1165 // Since we do not have an async pre-instantiation phase which populates compilation,
1166 // and instead have compilation happening lazily in evaluate calls, we implement this
1167 // hack to synchronously obtain the compiled Wasm leaning into the require implementation
1168 // for the Wasm then plucking out the compiled Wasm module.
1169 // In future, the source phase should be eagerly populated during pre-innstantiation
1170 // with the compiled record, so that we can just directly read `sourceObject_` off of
1171 // entry.module instead.
1172 if (entry.module.isWasm()) {
1173 v8::Local<v8::Value> moduleNamespace;
1174 if (registry
1175 .require(js, resolveContext, IsolateModuleRegistry::RequireOption::RETURN_EMPTY)
1176 .ToLocal(&moduleNamespace)) {
1177 v8::Local<v8::Value> defaultExport;
1178 if (moduleNamespace.As<v8::Object>()
1179 ->Get(js.v8Context(), js.strIntern("default"_kj))
1180 .ToLocal(&defaultExport)) {
1181 if (defaultExport->IsWasmModuleObject()) {
1182 return defaultExport.As<v8::Object>();
1183 }
1184 }
1185 }
1186 // If require() failed with an exception (e.g. WASM compilation error),
1187 // propagate that instead of masking it with the generic message below.
1188 if (js.v8Isolate->HasPendingException()) {
1189 return {};
1190 }
1191 }
1192 }
1193 js.throwException(js.v8Ref(v8::Exception::SyntaxError(
1194 js.str(kj::str("Source phase import not available for module: "_kj, spec)))));
1195 return {};
1196 }
1197 KJ_UNREACHABLE;
1198 }
1199 
1200 js.throwException(js.error(kj::str("Invalid module specifier: "_kj, specifier)));
1201 return {};
1202 }, [&](Value exception) -> v8::MaybeLocal<ReturnType> {
1203 // If there are any synchronously thrown exceptions, we want to catch them
1204 // here and convert them into a rejected promise. The only exception are
1205 // fatal cases where the isolate is terminating which won't make it here
1206 // anyway.
1207 js.v8Isolate->ThrowException(exception.getHandle(js));
1208 return {};
1209 });
1210}
1211 
1212// The fallback module bundle calls a single resolve callback to resolve all modules
1213// it is asked to resolve. Thread safety is provided by the ModuleRegistry's
1214// MutexGuarded<Impl> exclusive lock — all calls to lookup() are made while
1215// holding that lock.
1216class FallbackModuleBundle final: public ModuleBundle {
1217 public:
1218 FallbackModuleBundle(Builder::ResolveCallback&& callback)
1219 : ModuleBundle(Type::FALLBACK),
1220 callback(kj::mv(callback)) {}
1221 
1222 kj::Maybe<Resolved> lookup(const ResolveContext& context) override {
1223 // Maybe it's an alias? If so, we just return the aliased specifier.
1224 // We don't resolve again because the alias might be to a specifier
1225 // in another bundle. We should start the resolution process over
1226 // from the start.
1227 KJ_IF_SOME(found, aliases.find(context.normalizedSpecifier)) {
1228 return Resolved{
1229 .specifier = kj::str(found),
1230 };
1231 }
1232 
1233 // Maybe it's already cached? If so, we just return the cached module.
1234 KJ_IF_SOME(found, storage.find(context.normalizedSpecifier)) {
1235 return Resolved{
1236 .module = *found,
1237 };
1238 }
1239 
1240 // Well, let's actually try to resolve it.
1241 KJ_IF_SOME(resolved, callback(context)) {
1242 KJ_SWITCH_ONEOF(resolved) {
1243 KJ_CASE_ONEOF(str, kj::String) {
1244 // We got an alias back. Store it and return it, unless it's an alias
1245 // to itself, in which case return kj::none. It's possible that a buggy
1246 // fallback resolver could end up in an infinite loop of aliasing.
1247 if (str == context.normalizedSpecifier.getHref()) {
1248 return kj::none;
1249 }
1250 aliases.insert(context.normalizedSpecifier.clone(), kj::str(str));
1251 return Resolved{
1252 .specifier = kj::mv(str),
1253 };
1254 }
1255 KJ_CASE_ONEOF(resolved, kj::Own<Module>) {
1256 auto& module = *resolved;
1257 // If the fallback service returned a module with a specifier that
1258 // already exists in storage, ignore it and return kj::none. We can't
1259 // have two different modules with the same specifier in the bundle.
1260 if (storage.find(module.id()) != kj::none) {
1261 return kj::none;
1262 }
1263 storage.insert(module.id().clone(), kj::mv(resolved));
1264 if (context.normalizedSpecifier != module.id()) {
1265 // We checked for the existence of the specifier alias above so this
1266 // insert should always succeed. In debug mode, let's check.
1267 KJ_DASSERT(aliases.find(context.normalizedSpecifier) == kj::none);
1268 aliases.insert(context.normalizedSpecifier.clone(), kj::str(module.id().getHref()));
1269 }
1270 return Resolved{
1271 .module = module,
1272 };
1273 }
1274 }
1275 KJ_UNREACHABLE;
1276 }
1277 
1278 return kj::none;
1279 }
1280 
1281 private:
1282 Builder::ResolveCallback callback;
1283 
1284 kj::HashMap<Url, kj::Own<Module>> storage;
1285 kj::HashMap<Url, kj::String> aliases;
1286};
1287 
1288// The static module bundle maintains an internal table of specifiers to resolve callbacks
1289// in memory. Thread safety is provided by the ModuleRegistry's MutexGuarded<Impl>
1290// exclusive lock — all calls to lookup() are made while holding that lock.
1291class StaticModuleBundle final: public ModuleBundle {
1292 public:
1293 StaticModuleBundle(Type type,
1294 kj::HashMap<Url, ModuleBundle::Builder::ResolveCallback> modules,
1295 kj::HashMap<Url, Url> aliases)
1296 : ModuleBundle(type),
1297 modules(kj::mv(modules)),
1298 aliases(kj::mv(aliases)) {}
1299 KJ_DISALLOW_COPY_AND_MOVE(StaticModuleBundle);
1300 
1301 kj::Maybe<Resolved> lookup(const ResolveContext& context) override {
1302 // Is it an alias? If so, we just return the aliased specifier.
1303 KJ_IF_SOME(aliased, aliases.find(context.normalizedSpecifier)) {
1304 return Resolved{
1305 .specifier = kj::str(aliased.getHref()),
1306 };
1307 }
1308 
1309 // It's not an alias, maybe it's already cached?
1310 KJ_IF_SOME(cached, cache.find(context.normalizedSpecifier)) {
1311 return Resolved{
1312 .module = checkModule(context, *cached),
1313 };
1314 }
1315 
1316 // Not aliased or cached, we need to look it up.
1317 KJ_IF_SOME(found, modules.find(context.normalizedSpecifier)) {
1318 KJ_IF_SOME(resolved, found(context)) {
1319 KJ_SWITCH_ONEOF(resolved) {
1320 KJ_CASE_ONEOF(str, kj::String) {
1321 return Resolved{
1322 .specifier = kj::mv(str),
1323 };
1324 }
1325 KJ_CASE_ONEOF(resolved, kj::Own<Module>) {
1326 const Module& module = *resolved;
1327 cache.insert(context.normalizedSpecifier.clone(), kj::mv(resolved));
1328 return Resolved{
1329 .module = checkModule(context, module),
1330 };
1331 }
1332 }
1333 KJ_UNREACHABLE;
1334 }
1335 }
1336 
1337 return kj::none;
1338 }
1339 
1340 private:
1341 kj::HashMap<Url, ModuleBundle::Builder::ResolveCallback> modules;
1342 kj::HashMap<Url, Url> aliases;
1343 kj::HashMap<Url, kj::Own<Module>> cache;
1344};
1345 
1346kj::HashSet<kj::StringPtr> toHashSet(kj::ArrayPtr<const kj::String> arr) {
1347 kj::HashSet<kj::StringPtr> set;
1348 set.insertAll(arr);
1349 // Make sure there is no "default" export listed explicitly in the set.
1350 set.eraseMatch("default"_kj);
1351 return kj::mv(set);
1352}
1353 
1354} // namespace
1355 
1356// ======================================================================================
1357kj::Own<ModuleBundle> ModuleBundle::newFallbackBundle(Builder::ResolveCallback callback) {
1358 return kj::heap<FallbackModuleBundle>(kj::mv(callback));
1359}
1360 
1361void ModuleBundle::getBuiltInBundleFromCapnp(BuiltinBuilder& builder, Bundle::Reader bundle) {
1362 getBuiltInBundleFromCapnp(builder, bundle, [](workerd::jsg::Module::Reader) { return true; });
1363}
1364 
1365void ModuleBundle::getBuiltInBundleFromCapnp(BuiltinBuilder& builder,
1366 Bundle::Reader bundle,
1367 kj::Function<bool(workerd::jsg::Module::Reader)> filter) {
1368 auto typeFilter = ([&] {
1369 switch (builder.type()) {
1370 case Module::Type::BUILTIN:
1371 return ModuleType::BUILTIN;
1372 case Module::Type::BUILTIN_ONLY:
1373 return ModuleType::INTERNAL;
1374 case Module::Type::BUNDLE:
1375 break;
1376 case Module::Type::FALLBACK:
1377 break;
1378 }
1379 KJ_UNREACHABLE;
1380 })();
1381 
1382 for (auto module: bundle.getModules()) {
1383 if (module.getType() == typeFilter && filter(module)) {
1384 auto id = KJ_ASSERT_NONNULL(Url::tryParse(module.getName()));
1385 switch (module.which()) {
1386 case workerd::jsg::Module::SRC: {
1387 builder.addEsm(id, module.getSrc().asChars());
1388 continue;
1389 }
1390 case workerd::jsg::Module::WASM: {
1391 builder.addSynthetic(id, Module::newWasmModuleHandler(module.getWasm().asBytes()));
1392 continue;
1393 }
1394 case workerd::jsg::Module::DATA: {
1395 builder.addSynthetic(id, Module::newDataModuleHandler(module.getData().asBytes()));
1396 continue;
1397 }
1398 case workerd::jsg::Module::JSON: {
1399 builder.addSynthetic(id, Module::newJsonModuleHandler(module.getJson().asArray()));
1400 continue;
1401 }
1402 }
1403 KJ_UNREACHABLE;
1404 }
1405 }
1406}
1407 
1408ModuleBundle::ModuleBundle(Type type): type_(type) {}
1409 
1410ModuleBundle::Builder::Builder(Type type): type_(type) {}
1411 
1412ModuleBundle::Builder& ModuleBundle::Builder::alias(const Url& alias, const Url& id) {
1413 auto aliasNormed = alias.clone(Url::EquivalenceOption::NORMALIZE_PATH);
1414 if (modules_.find(aliasNormed) != kj::none || aliases_.find(aliasNormed) != kj::none) {
1415 KJ_FAIL_REQUIRE(kj::str("Module \"", aliasNormed.getHref(), "\" already added to bundle"));
1416 }
1417 aliases_.insert(kj::mv(aliasNormed), id.clone(Url::EquivalenceOption::NORMALIZE_PATH));
1418 return *this;
1419}
1420 
1421ModuleBundle::Builder& ModuleBundle::Builder::add(
1422 const Url& id, Builder::ResolveCallback callback) {
1423 if (modules_.find(id) != kj::none || aliases_.find(id) != kj::none) {
1424 KJ_FAIL_REQUIRE(kj::str("Module \"", id.getHref(), "\" already added to bundle"));
1425 }
1426 modules_.insert(id.clone(), kj::mv(callback));
1427 return *this;
1428}
1429 
1430kj::Own<ModuleBundle> ModuleBundle::Builder::finish() {
1431 return kj::heap<StaticModuleBundle>(type_, kj::mv(modules_), kj::mv(aliases_));
1432}
1433 
1434void ModuleBundle::Builder::ensureIsNotBundleSpecifier(const Url& id) {
1435 // The file: protocol is reserved for bundle type modules.
1436 KJ_REQUIRE(
1437 id.getProtocol() != "file:"_kjc, "The file: protocol is reserved for bundle type modules");
1438}
1439 
1440// ======================================================================================
1441 
1442ModuleBundle::BundleBuilder::BundleBuilder(const jsg::Url& bundleBase)
1443 : ModuleBundle::Builder(Type::BUNDLE),
1444 bundleBase(bundleBase) {}
1445 
1446namespace {
1447static constexpr auto BUNDLE_CLONE_OPTIONS = jsg::Url::EquivalenceOption::IGNORE_FRAGMENTS |
1448 jsg::Url::EquivalenceOption::IGNORE_SEARCH | jsg::Url::EquivalenceOption::NORMALIZE_PATH;
1449 
1450// Takes the user-provided module name and normalizes it to a form that can be
1451// resolved relative to the bundle base. This involves pre-parsing the name as a URL
1452// relative to a dummy base URL in order to normalize out dot and double-dot segments,
1453// then stripping off any leading slashes so that the name is always relative and cannot
1454// be interpreted as an absolute path.
1455jsg::Url normalizeModuleName(kj::StringPtr name, const jsg::Url& base) {
1456 // This first step normalizes out path segments like "." and "..", drops query
1457 // strings and fragments, and normalizes percent-encoding in the path.
1458 auto url = KJ_ASSERT_NONNULL(base.tryResolve(name)).clone(BUNDLE_CLONE_OPTIONS);
1459 
1460 // If the protocol is not file:, then we don't need to do any more processing
1461 // here. We will check the validity of the result as a module URL in the next
1462 // step.
1463 if (url.getProtocol() != "file:"_kj) {
1464 return kj::mv(url);
1465 }
1466 
1467 auto urlPath = url.getPathname();
1468 auto basePath = base.getPathname();
1469 
1470 // The url path must not be identical to the base...
1471 KJ_REQUIRE(urlPath != basePath, "Invalid empty module name");
1472 
1473 // If the url path starts with the base path, then we're good!
1474 if (urlPath.startsWith(basePath)) {
1475 return kj::mv(url);
1476 }
1477 
1478 // Otherwise, let's make sure that the url path is processed as
1479 // relative to the base path. We do this by stripping off any
1480 // leading slashes from the front of the URL then re-resolve
1481 // against the base. This should be an exceedingly rare edge
1482 // case if the worker bundle is being constructed properly. It's
1483 // meant only to handle cases where silliness like "///foo" is
1484 // given as a module name.
1485 while (urlPath.startsWith("/"_kj) && urlPath.size() > 0) {
1486 urlPath = urlPath.slice(1);
1487 }
1488 KJ_REQUIRE(urlPath.size() > 0, "Invalid empty module name");
1489 
1490 return KJ_ASSERT_NONNULL(base.tryResolve(urlPath));
1491}
1492 
1493bool isValidBundleModuleUrl(const jsg::Url& url, const jsg::Url& base) {
1494 KJ_DASSERT(base.getProtocol() == "file:"_kj);
1495 KJ_DASSERT(base.getPathname().endsWith("/"_kj));
1496 
1497 // Let's forbid users from using cloudflare: and workerd: URLs in bundles so that
1498 // we can protect those namespaces for our own future use. Specifically, these
1499 // should only be used by the runtime to refer to built-in modules. We don't
1500 // restrict other non-standard protocols like node:
1501 KJ_REQUIRE(url.getProtocol() != "cloudflare:"_kj,
1502 "The cloudflare: protocol is reserved and cannot be used in module bundles");
1503 KJ_REQUIRE(url.getProtocol() != "workerd:"_kj,
1504 "The workerd: protocol is reserved and cannot be used in module bundles");
1505 
1506 // Let's forbid data: URL use from module bundles. They are not yet supported
1507 // by the runtime due to dynamic eval restrictions. Even when we do support them
1508 // eventually, we want to be able to reserve the data: URL namespace for that
1509 // use. If we allowed worker bundles to use data: URLs that we could end up
1510 // requiring a compat flag later to actually properly enable them.
1511 KJ_REQUIRE(
1512 url.getProtocol() != "data:"_kj, "The data: protocol cannot be used in module bundles");
1513 
1514 if (url.getProtocol() != "file:"_kj) {
1515 // Different protocols are always OK
1516 return true;
1517 }
1518 
1519 // Module file: URLs must not have a host component.
1520 // We already know the protocol is "file:" here because of the check above.
1521 if (url.getHost() != ""_kj) {
1522 return false;
1523 }
1524 
1525 // Check if url is subordinate to the base.
1526 // This means url's path should start with base's path as a prefix
1527 auto aPath = url.getPathname();
1528 auto bPath = base.getPathname();
1529 
1530 return aPath.startsWith(bPath);
1531}
1532 
1533// Converts the name given for a user-bundle module into a fully qualified module url.
1534// This involves normalizing the name such that it is relative to the bundle base, removes
1535// any query parameters or fragments, removes dot and double-dot path segments, normalizes
1536// percent-encoding, and otherwise validates that the resulting URL is a valid URL.
1537const jsg::Url processModuleName(kj::StringPtr name, const jsg::Url& base) {
1538 auto url = normalizeModuleName(name, base);
1539 KJ_REQUIRE(isValidBundleModuleUrl(url, base), "Invalid module name: ", name);
1540 return url;
1541}
1542 
1543} // namespace
1544 
1545ModuleBundle::BundleBuilder& ModuleBundle::BundleBuilder::addSyntheticModule(kj::StringPtr name,
1546 EvaluateCallback callback,
1547 kj::Array<kj::String> namedExports,
1548 Module::ContentType contentType) {
1549 const auto url = processModuleName(name, bundleBase);
1550 add(url,
1551 [url = url.clone(), callback = kj::mv(callback), namedExports = kj::mv(namedExports),
1552 type = type(), contentType](const ResolveContext& context) mutable
1553 -> kj::Maybe<kj::OneOf<kj::String, kj::Own<Module>>> {
1554 kj::Own<Module> mod = Module::newSynthetic(kj::mv(url), type, kj::mv(callback),
1555 kj::mv(namedExports), Module::Flags::NONE, contentType);
1556 return kj::Maybe<kj::OneOf<kj::String, kj::Own<Module>>>(kj::mv(mod));
1557 });
1558 return *this;
1559}
1560 
1561ModuleBundle::BundleBuilder& ModuleBundle::BundleBuilder::addEsmModule(
1562 kj::StringPtr name, kj::ArrayPtr<const char> source, Module::Flags flags) {
1563 const auto url = processModuleName(name, bundleBase);
1564 add(url,
1565 [url = url.clone(), source, flags, type = type()](const ResolveContext& context) mutable
1566 -> kj::Maybe<kj::OneOf<kj::String, kj::Own<Module>>> {
1567 kj::Own<Module> mod = kj::heap<EsModule>(kj::mv(url), type, flags, source);
1568 return kj::Maybe<kj::OneOf<kj::String, kj::Own<Module>>>(kj::mv(mod));
1569 });
1570 return *this;
1571}
1572 
1573ModuleBundle::BundleBuilder& ModuleBundle::BundleBuilder::addEsmModule(
1574 kj::StringPtr name, kj::Array<const char> source, Module::Flags flags) {
1575 const auto url = processModuleName(name, bundleBase);
1576 add(url,
1577 [url = url.clone(), source = kj::mv(source), flags, type = type()](
1578 const ResolveContext& context) mutable
1579 -> kj::Maybe<kj::OneOf<kj::String, kj::Own<Module>>> {
1580 kj::Own<Module> mod = Module::newEsm(kj::mv(url), type, kj::mv(source), flags);
1581 return kj::Maybe<kj::OneOf<kj::String, kj::Own<Module>>>(kj::mv(mod));
1582 });
1583 return *this;
1584}
1585 
1586ModuleBundle::BundleBuilder& ModuleBundle::BundleBuilder::addWasmModule(
1587 kj::StringPtr name, kj::ArrayPtr<const kj::byte> data) {
1588 const auto url = processModuleName(name, bundleBase);
1589 auto callback = jsg::modules::Module::newWasmModuleHandler(data);
1590 add(url,
1591 [url = url.clone(), callback = kj::mv(callback), type = type()](
1592 const ResolveContext& context) mutable
1593 -> kj::Maybe<kj::OneOf<kj::String, kj::Own<Module>>> {
1594 kj::Own<Module> mod = Module::newSynthetic(kj::mv(url), type, kj::mv(callback), nullptr,
1595 EsModule::Flags::WASM, Module::ContentType::WASM);
1596 return kj::Maybe<kj::OneOf<kj::String, kj::Own<Module>>>(kj::mv(mod));
1597 });
1598 return *this;
1599}
1600 
1601ModuleBundle::BundleBuilder& ModuleBundle::BundleBuilder::alias(
1602 kj::StringPtr alias, kj::StringPtr name) {
1603 const auto id = processModuleName(name, bundleBase);
1604 const auto aliasUrl = processModuleName(alias, bundleBase);
1605 Builder::alias(aliasUrl, id);
1606 return *this;
1607}
1608 
1609// ======================================================================================
1610 
1611ModuleBundle::BuiltinBuilder::BuiltinBuilder(Type type)
1612 : ModuleBundle::Builder(toModuleBuilderType(type)) {}
1613 
1614ModuleBundle::BuiltinBuilder& ModuleBundle::BuiltinBuilder::addSynthetic(
1615 const Url& id, ModuleBundle::BundleBuilder::EvaluateCallback callback) {
1616 ensureIsNotBundleSpecifier(id);
1617 Builder::add(id,
1618 [url = id.clone(), callback = kj::mv(callback), type = type()](
1619 const ResolveContext& context) mutable
1620 -> kj::Maybe<kj::OneOf<kj::String, kj::Own<Module>>> {
1621 kj::Own<Module> mod = Module::newSynthetic(kj::mv(url), type, kj::mv(callback));
1622 return kj::Maybe<kj::OneOf<kj::String, kj::Own<Module>>>(kj::mv(mod));
1623 });
1624 return *this;
1625}
1626 
1627ModuleBundle::BuiltinBuilder& ModuleBundle::BuiltinBuilder::addEsm(
1628 const Url& id, kj::ArrayPtr<const char> source) {
1629 ensureIsNotBundleSpecifier(id);
1630 Builder::add(id,
1631 [url = id.clone(), source, type = type()](const ResolveContext& context) mutable
1632 -> kj::Maybe<kj::OneOf<kj::String, kj::Own<Module>>> {
1633 kj::Own<Module> mod = Module::newEsm(kj::mv(url), type, source);
1634 return kj::Maybe<kj::OneOf<kj::String, kj::Own<Module>>>(kj::mv(mod));
1635 });
1636 return *this;
1637}
1638 
1639// ======================================================================================
1640ModuleRegistry::Impl::Impl(kj::ArrayPtr<kj::Vector<kj::Own<ModuleBundle>>> vectors) {
1641 bundles[kBundle] = vectors[kBundle].releaseAsArray();
1642 bundles[kBuiltin] = vectors[kBuiltin].releaseAsArray();
1643 bundles[kBuiltinOnly] = vectors[kBuiltinOnly].releaseAsArray();
1644 bundles[kFallback] = vectors[kFallback].releaseAsArray();
1645}
1646 
1647ModuleRegistry::Builder::Builder(
1648 const ResolveObserver& observer, const jsg::Url& bundleBase, Options options)
1649 : observer(observer),
1650 bundleBase(bundleBase),
1651 options(options),
1652 schemaLoader(kj::heap<capnp::SchemaLoader>()) {}
1653 
1654bool ModuleRegistry::Builder::allowsFallback() const {
1655 return (options & Options::ALLOW_FALLBACK) == Options::ALLOW_FALLBACK;
1656}
1657 
1658ModuleRegistry::Builder& ModuleRegistry::Builder::add(kj::Own<ModuleBundle> bundle) {
1659 if (!allowsFallback()) {
1660 KJ_REQUIRE(bundle->type() != ModuleBundle::Type::FALLBACK,
1661 "Fallback bundle types are not allowed for this registry");
1662 }
1663 bundles_[static_cast<int>(bundle->type())].add(kj::mv(bundle));
1664 return *this;
1665}
1666 
1667ModuleRegistry::Builder& ModuleRegistry::Builder::setEvalCallback(EvalCallback callback) {
1668 maybeEvalCallback = kj::mv(callback);
1669 return *this;
1670}
1671 
1672kj::Arc<ModuleRegistry> ModuleRegistry::Builder::finish() {
1673 return kj::arc<ModuleRegistry>(this);
1674}
1675 
1676ModuleRegistry::ModuleRegistry(ModuleRegistry::Builder* builder)
1677 : observer(builder->observer),
1678 bundleBase(builder->bundleBase),
1679 impl(Impl(builder->bundles_.asPtr())),
1680 maybeEvalCallback(kj::mv(builder->maybeEvalCallback)),
1681 schemaLoader(kj::mv(builder->schemaLoader)) {}
1682 
1683kj::Maybe<jsg::JsPromise> ModuleRegistry::evaluateImpl(jsg::Lock& js,
1684 const Module& module,
1685 v8::Local<v8::Module> v8Module,
1686 const CompilationObserver& observer) const {
1687 KJ_IF_SOME(callback, maybeEvalCallback) {
1688 return callback(js, module, v8Module, observer);
1689 }
1690 return kj::none;
1691}
1692 
1693kj::Own<void> ModuleRegistry::attachToIsolate(Lock& js, const CompilationObserver& observer) const {
1694 // The IsolateModuleRegistry is attached to the isolate as an embedder data slot.
1695 // We have to keep it alive for the duration of the v8::Context so we return a
1696 // kj::Own and store that in the jsg::JsContext
1697 return kj::heap<IsolateModuleRegistry>(js, *this, observer);
1698}
1699 
1700kj::Maybe<ModuleRegistry::ModuleOrRedirect> ModuleRegistry::tryFindInBundle(
1701 const ResolveContext& context, ModuleBundle& bundle, const Url& bundleBase) {
1702 KJ_IF_SOME(found, bundle.lookup(context)) {
1703 KJ_IF_SOME(str, found.specifier) {
1704 // We received a redirect to another module specifier. Let's
1705 // start resolution over again with the new specifier... but only
1706 // if we can successfully parse the specifier as a URL.
1707 KJ_IF_SOME(id, jsg::Url::tryParse(str.asPtr(), bundleBase.getHref())) {
1708 return kj::Maybe(kj::mv(id));
1709 }
1710 }
1711 KJ_IF_SOME(module, found.module) {
1712 return kj::Maybe(ModuleRef{
1713 .module = module,
1714 });
1715 }
1716 }
1717 return kj::none;
1718}
1719 
1720kj::Maybe<ModuleRegistry::ModuleOrRedirect> ModuleRegistry::tryFindInBundleGroup(
1721 const ResolveContext& context, kj::ArrayPtr<kj::Own<ModuleBundle>> bundles) const {
1722 for (auto& bundle: bundles) {
1723 KJ_IF_SOME(found, tryFindInBundle(context, *bundle, bundleBase)) {
1724 return kj::mv(found);
1725 }
1726 }
1727 return kj::none;
1728}
1729 
1730kj::Maybe<const Module&> ModuleRegistry::lookupImpl(
1731 Impl& impl, const ResolveContext& context, bool recursed) const {
1732#define MODULE_LOOKUP(context, bundle) \
1733 KJ_IF_SOME(found, tryFindInBundleGroup(context, impl.bundles[bundle])) { \
1734 KJ_SWITCH_ONEOF(found) { \
1735 KJ_CASE_ONEOF(url, Url) { \
1736 if (recursed) { /* avoid recursing indefinitely */ \
1737 return kj::none; \
1738 } \
1739 kj::HashMap<kj::StringPtr, kj::StringPtr> clonedAttrs; \
1740 for (const auto& [key, value]: context.attributes) { \
1741 clonedAttrs.insert(key, value); \
1742 } \
1743 ResolveContext ctx{ \
1744 .type = context.type, \
1745 .source = context.source, \
1746 .normalizedSpecifier = url, \
1747 .referrerNormalizedSpecifier = context.referrerNormalizedSpecifier, \
1748 .rawSpecifier = \
1749 context.rawSpecifier.map([](auto& str) -> kj::StringPtr { return str; }), \
1750 .attributes = kj::mv(clonedAttrs), \
1751 }; \
1752 return lookupImpl(impl, ctx, true); \
1753 } \
1754 KJ_CASE_ONEOF(mod, ModuleRef) { \
1755 return mod.module; \
1756 } \
1757 } \
1758 KJ_UNREACHABLE; \
1759 }
1760 
1761 switch (context.type) {
1762 case ResolveContext::Type::BUNDLE: {
1763 // For bundle resolution, we only use Bundle, Builtin, and Fallback bundles,
1764 // in that order.
1765 MODULE_LOOKUP(context, kBundle);
1766 MODULE_LOOKUP(context, kBuiltin);
1767 MODULE_LOOKUP(context, kFallback);
1768 break;
1769 }
1770 case ResolveContext::Type::BUILTIN: {
1771 // For built-in resolution, we only use builtin and builtin-only bundles.
1772 MODULE_LOOKUP(context, kBuiltin);
1773 MODULE_LOOKUP(context, kBuiltinOnly);
1774 break;
1775 }
1776 case ResolveContext::Type::BUILTIN_ONLY: {
1777 // For built-in only resolution, we only use builtin-only bundles.
1778 MODULE_LOOKUP(context, kBuiltinOnly);
1779 break;
1780 }
1781 case ResolveContext::Type::PUBLIC_BUILTIN: {
1782 // For public built-in resolution, we only use builtin bundles.
1783 // This excludes both worker bundle modules and internal-only modules,
1784 // returning only built-ins that are normally importable by user code.
1785 MODULE_LOOKUP(context, kBuiltin);
1786 break;
1787 }
1788 }
1789 
1790#undef MODULE_LOOKUP
1791 
1792 return kj::none;
1793}
1794 
1795kj::Maybe<const Module&> ModuleRegistry::lookup(const ResolveContext& context) const {
1796 // If the embedder supports it, collect metrics on what modules were resolved.
1797 auto metrics =
1798 observer.onResolveModule(context.normalizedSpecifier, context.type, context.source);
1799 
1800 // While multiple threads may be holding references to the registry, only one thread
1801 // at a time may resolve a module. Resolving a module may involve mutating internal
1802 // state (e.g. caching) so we lock here. Fortunately, module resolution should be
1803 // fast, especially with caching, so this lock should be held only briefly.
1804 auto lock = impl.lockExclusive();
1805 KJ_IF_SOME(found, lookupImpl(*lock, context, false)) {
1806 metrics->found();
1807 return found;
1808 }
1809 
1810 metrics->notFound();
1811 return kj::none;
1812}
1813 
1814kj::Maybe<JsValue> ModuleRegistry::tryResolveModuleNamespace(Lock& js,
1815 kj::StringPtr specifier,
1816 ResolveContext::Type type,
1817 ResolveContext::Source source,
1818 kj::Maybe<const Url&> maybeReferrer,
1819 UnwrapDefault unwrapDefault) {
1820 auto& bound = IsolateModuleRegistry::from(js.v8Isolate);
1821 auto url = ([&] {
1822 KJ_IF_SOME(referrer, maybeReferrer) {
1823 return KJ_ASSERT_NONNULL(referrer.tryResolve(specifier));
1824 }
1825 return KJ_ASSERT_NONNULL(bound.getBundleBase().tryResolve(specifier));
1826 })();
1827 auto normalized = url.clone(Url::EquivalenceOption::NORMALIZE_PATH);
1828 ResolveContext context{
1829 .type = type,
1830 .source = source,
1831 .normalizedSpecifier = normalized,
1832 .referrerNormalizedSpecifier = maybeReferrer.orDefault(bound.getBundleBase()),
1833 .rawSpecifier = specifier,
1834 };
1835 v8::TryCatch tryCatch(js.v8Isolate);
1836 auto option = IsolateModuleRegistry::RequireOption::RETURN_EMPTY;
1837 // Following the behavior of Node.js' require(esm) implementation, we disallow top-level await
1838 // in synchronously required modules.
1839 if (source == ResolveContext::Source::REQUIRE) {
1840 option = option | IsolateModuleRegistry::RequireOption::NO_TOP_LEVEL_AWAIT;
1841 }
1842 if (unwrapDefault == UnwrapDefault::YES) {
1843 option = option | IsolateModuleRegistry::RequireOption::UNWRAP_DEFAULT;
1844 }
1845 
1846 auto ns = bound.require(js, context, option);
1847 if (tryCatch.HasCaught()) {
1848 tryCatch.ReThrow();
1849 throw JsExceptionThrown();
1850 }
1851 if (ns.IsEmpty()) return kj::none;
1852 return JsValue(check(ns));
1853}
1854 
1855JsValue ModuleRegistry::resolve(Lock& js,
1856 kj::StringPtr specifier,
1857 kj::StringPtr exportName,
1858 ResolveContext::Type type,
1859 ResolveContext::Source source,
1860 kj::Maybe<const Url&> maybeReferrer) {
1861 KJ_IF_SOME(val, tryResolveModuleNamespace(js, specifier, type, source, maybeReferrer)) {
1862 auto ns = KJ_ASSERT_NONNULL(val.tryCast<JsObject>());
1863 return ns.get(js, exportName);
1864 }
1865 JSG_FAIL_REQUIRE(Error, kj::str("Module not found: ", specifier));
1866}
1867 
1868// ======================================================================================
1869 
1870Module::Module(Url id, Type type, Flags flags, ContentType contentType)
1871 : id_(kj::mv(id)),
1872 type_(type),
1873 flags_(flags),
1874 contentType_(contentType) {}
1875 
1876kj::Maybe<jsg::JsPromise> Module::Evaluator::operator()(jsg::Lock& js,
1877 const Module& module,
1878 v8::Local<v8::Module> v8Module,
1879 const CompilationObserver& observer) const {
1880 return registry.evaluateImpl(js, module, v8Module, observer);
1881}
1882 
1883bool Module::instantiate(
1884 Lock& js, v8::Local<v8::Module> module, const CompilationObserver& observer) const {
1885 if (module->GetStatus() != v8::Module::kUninstantiated) {
1886 return true;
1887 }
1888 // InstantiateModule is one of those methods that returns a Maybe<bool> but
1889 // never returns Just(false). It either returns Just(true) or an empty Maybe
1890 // to signal that the instantiation failed. Eventually I would expect V8 to
1891 // replace the return value with a Maybe<void>.
1892 return module
1893 ->InstantiateModule(js.v8Context(), resolveModuleCallback<false>, resolveModuleCallback<true>)
1894 .IsJust();
1895}
1896 
1897bool Module::isEval() const {
1898 return (flags_ & Flags::EVAL) == Flags::EVAL;
1899}
1900 
1901bool Module::isEsm() const {
1902 return (flags_ & Flags::ESM) == Flags::ESM;
1903}
1904 
1905bool Module::isMain() const {
1906 return (flags_ & Flags::MAIN) == Flags::MAIN;
1907}
1908 
1909bool Module::isWasm() const {
1910 return (flags_ & Flags::WASM) == Flags::WASM;
1911}
1912 
1913bool Module::evaluateContext(const ResolveContext& context) const {
1914 if (context.normalizedSpecifier != id()) return false;
1915 // TODO(soon): Check the import attributes in the context.
1916 return true;
1917}
1918 
1919kj::Own<Module> Module::newSynthetic(Url id,
1920 Type type,
1921 EvaluateCallback callback,
1922 kj::Array<kj::String> namedExports,
1923 Flags flags,
1924 ContentType contentType) {
1925 return kj::heap<SyntheticModule>(
1926 kj::mv(id), type, kj::mv(callback), kj::mv(namedExports), flags, contentType);
1927}
1928 
1929kj::Own<Module> Module::newEsm(Url id, Type type, kj::Array<const char> code, Flags flags) {
1930 return kj::heap<EsModule>(kj::mv(id), type, flags, code).attach(kj::mv(code));
1931}
1932 
1933kj::Own<Module> Module::newEsm(Url id, Type type, kj::ArrayPtr<const char> code) {
1934 return kj::heap<EsModule>(kj::mv(id), type, Flags::ESM, code);
1935}
1936 
1937Module::ModuleNamespace::ModuleNamespace(
1938 v8::Local<v8::Module> inner, kj::ArrayPtr<const kj::String> namedExports)
1939 : inner(inner),
1940 namedExports(toHashSet(namedExports)) {}
1941 
1942bool Module::ModuleNamespace::set(Lock& js, kj::StringPtr name, JsValue value) const {
1943 if (name != "default"_kj) {
1944 KJ_REQUIRE(namedExports.find(name) != kj::none, kj::str("Module does not export ", name));
1945 }
1946 
1947 bool result;
1948 if (!inner->SetSyntheticModuleExport(js.v8Isolate, js.strIntern(name), value).To(&result)) {
1949 return false;
1950 }
1951 if (!result) {
1952 js.v8Isolate->ThrowError(js.str(kj::str("Failed to set synthetic module export ", name)));
1953 }
1954 return result;
1955}
1956 
1957bool Module::ModuleNamespace::setDefault(Lock& js, JsValue value) const {
1958 return set(js, SyntheticModule::DEFAULT, value);
1959}
1960 
1961kj::ArrayPtr<const kj::StringPtr> Module::ModuleNamespace::getNamedExports() const {
1962 return kj::ArrayPtr<const kj::StringPtr>(namedExports.begin(), namedExports.size());
1963}
1964 
1965// ======================================================================================
1966// Methods to create evaluation callbacks for common synthetic module types. It is
1967// important to remember that evaluation callbacks can be called multiple times and
1968// from multiple threads. The callbacks must be thread-safe and idempotent.
1969 
1970Module::EvaluateCallback Module::newTextModuleHandler(kj::ArrayPtr<const char> data) {
1971 return [data](Lock& js, const Url& id, const ModuleNamespace& ns,
1972 const CompilationObserver&) -> bool {
1973 JSG_TRY(js) {
1974 return ns.setDefault(js, js.str(data));
1975 }
1976 JSG_CATCH(exception) {
1977 js.v8Isolate->ThrowException(exception.getHandle(js));
1978 return false;
1979 }
1980 };
1981}
1982 
1983Module::EvaluateCallback Module::newDataModuleHandler(kj::ArrayPtr<const kj::byte> data) {
1984 return [data](Lock& js, const Url& id, const ModuleNamespace& ns,
1985 const CompilationObserver&) -> bool {
1986 JSG_TRY(js) {
1987 auto backing = jsg::BackingStore::alloc<v8::ArrayBuffer>(js, data.size());
1988 backing.asArrayPtr().copyFrom(data);
1989 auto buffer = jsg::BufferSource(js, kj::mv(backing));
1990 return ns.setDefault(js, JsValue(buffer.getHandle(js)));
1991 }
1992 JSG_CATCH(exception) {
1993 js.v8Isolate->ThrowException(exception.getHandle(js));
1994 return false;
1995 }
1996 };
1997}
1998 
1999Module::EvaluateCallback Module::newJsonModuleHandler(kj::ArrayPtr<const char> data) {
2000 return [data](Lock& js, const Url& id, const ModuleNamespace& ns,
2001 const CompilationObserver& observer) -> bool {
2002 return js.tryCatch([&] {
2003 auto metrics = observer.onJsonCompilationStart(js.v8Isolate, data.size());
2004 return ns.setDefault(js, JsValue(js.parseJson(data).getHandle(js)));
2005 }, [&](Value exception) {
2006 js.v8Isolate->ThrowException(exception.getHandle(js));
2007 return false;
2008 });
2009 };
2010}
2011 
2012Module::EvaluateCallback Module::newWasmModuleHandler(kj::ArrayPtr<const kj::byte> data) {
2013 struct Cache final {
2014 kj::MutexGuarded<kj::Maybe<v8::CompiledWasmModule>> mutex;
2015 };
2016 return [data, cache = kj::heap<Cache>()](Lock& js, const Url& id, const ModuleNamespace& ns,
2017 const CompilationObserver& observer) mutable -> bool {
2018 return js.tryCatch([&]() -> bool {
2019 js.setAllowEval(true);
2020 KJ_DEFER(js.setAllowEval(false));
2021 
2022 // Allow Wasm compilation to spawn a background thread for tier-up, i.e. recompiling
2023 // Wasm with optimizations in the background. Otherwise Wasm startup is way too slow.
2024 // Until tier-up finishes, requests will be handled using Liftoff-generated code, which
2025 // compiles fast but runs slower.
2026 AllowV8BackgroundThreadsScope scope;
2027 
2028 {
2029 // See if we can use a cached compiled module to speed things up.
2030 auto lock = cache->mutex.lockShared();
2031 KJ_IF_SOME(compiled, *lock) {
2032 auto metrics = observer.onWasmCompilationFromCacheStart(js.v8Isolate);
2033 auto result =
2034 JsValue(check(v8::WasmModuleObject::FromCompiledModule(js.v8Isolate, compiled)));
2035 return ns.setDefault(js, result);
2036 }
2037 }
2038 
2039 auto module = jsg::compileWasmModule(js, data, observer);
2040 auto lock = cache->mutex.lockExclusive();
2041 *lock = module->GetCompiledModule();
2042 auto result = JsValue(module);
2043 return ns.setDefault(js, result);
2044 }, [&](Value exception) {
2045 js.v8Isolate->ThrowException(exception.getHandle(js));
2046 return false;
2047 });
2048 };
2049}
2050 
2051Function<void()> Module::compileEvalFunction(Lock& js,
2052 kj::StringPtr code,
2053 kj::StringPtr name,
2054 kj::Maybe<JsObject> compileExtensions,
2055 const CompilationObserver& observer) {
2056 auto metrics = observer.onScriptCompilationStart(js.v8Isolate, name);
2057 v8::ScriptOrigin origin(js.str(name));
2058 v8::ScriptCompiler::Source source(js.str(code), origin);
2059 auto fn = ([&] {
2060 KJ_IF_SOME(ext, compileExtensions) {
2061 v8::Local<v8::Object> obj = ext;
2062 return check(
2063 v8::ScriptCompiler::CompileFunction(js.v8Context(), &source, 0, nullptr, 1, &obj));
2064 } else {
2065 return check(
2066 v8::ScriptCompiler::CompileFunction(js.v8Context(), &source, 0, nullptr, 0, nullptr));
2067 }
2068 })();
2069 
2070 return [ref = js.v8Ref(fn)](Lock& js) mutable {
2071 js.withinHandleScope([&] {
2072 // Any return value is explicitly ignored.
2073 JsValue(check(ref.getHandle(js)->Call(js.v8Context(), js.v8Context()->Global(), 0, nullptr)));
2074 });
2075 };
2076}
2077 
2078} // namespace workerd::jsg::modules