Skip to content
File

Blob: src/workerd/jsg/modules-new.h

cpp814 lines
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#pragma once
6 
7#include <workerd/jsg/jsg.h>
8#include <workerd/jsg/jsvalue.h>
9#include <workerd/jsg/modules.capnp.h>
10#include <workerd/jsg/modules.h>
11#include <workerd/jsg/observer.h>
12#include <workerd/jsg/setup.h>
13#include <workerd/jsg/url.h>
14#include <workerd/jsg/util.h>
15 
16#include <v8.h>
17 
18#include <capnp/schema-loader.h>
19#include <kj/common.h>
20#include <kj/function.h>
21#include <kj/map.h>
22#include <kj/refcount.h>
23#include <kj/table.h>
24 
25namespace workerd::jsg::modules {
26 
27// Defines and implements workerd's (new) module loader subsystem.
28//
29// This new implementation of ModuleRegistry is designed to be more flexible,
30// modular, and extensible than the previous implementation. It is also designed
31// to support handling import specifiers as URLs, implement import.meta, properly
32// handle import attributes, sharing of modules across isolate replicas, and more.
33//
34// Every Worker has exactly one ModuleRegistry associated with composed of
35// of or more ModuleBundles (e.g. a ModuleBundle with modules from the worker
36// bundle, a ModuleBundle representing built-in modules, a ModuleBundle that is
37// a fallback service, etc). When multiple replicas of a Worker are created, they
38// may share the same ModuleRegistry instance, since multiple workers may be
39// acting across multiple threads, the ModuleRegistry instance must be thread-safe.
40//
41// The ModuleRegistry is the collection of individual Modules that can be
42// imported (i.e. `import * from '...'` and `await import('...')`) or required
43// (i.e. `require('...')`).
44//
45// The ModuleRegistry is conceptually immutable once created (individual
46// instances may support dynamic resolution using, for instance, a fallback
47// service but there is no API to manipulate the ModuleRegistry once it has
48// been created). There is internal state that may change, such as caching of
49// resolved modules, and compile cache data, but the set of modules that are
50// available within the ModuleRegistry does not change. The only exception to
51// this is the fallback service, which may dynamically resolved modules that
52// are not otherwise available in the ModuleRegistry, but that is only used
53// for local development in workerd and is never used in production.
54//
55// All access to the ModuleRegistry is thread-safe by way of (a) requiring callers
56// to hold and pass the jsg::Lock& when resolving modules and (b) having the
57// ModuleRegistry instance by AtomicRefcounted with MutexGuarded locks. With
58// exception to the fallback service, all operations within the ModuleRegistry
59// are either synchronous or expected to use JavaScript promises to perform
60// asynchronous operations.
61//
62// When a Worker is created, a ModuleBundle is created to contain Module
63// definitions declared by the worker source bundle configuration. One or more
64// are also provided that provides modules from within the runtime.
65//
66// When a v8::Isolate* is created for a particular worker, the ModuleRegistry
67// instance will be bound to the isolate in the form of a new IsolateModuleRegistry
68// instance. This is the interface that is actually used to resolve specifiers into
69// imported modules. The ModuleRegistry instance itself is owned by the Worker::Script
70// and the IsolateModuleRegistry instance can be viewed as a client.
71//
72// When there are multiple IsolateModuleRegistry instances pointing to the same
73// ModuleRegistry instance, only one can peform resolution at a time, controlled
74// by an exclusive lock on a MutexGuarded within the ModuleRegistry.
75//
76// A note on language use: Resolving a module vs. Loading a module. We use
77// the term "resolving" to refer to the overall process of taking a specifier
78// and turning it into a Module instance. This includes locating the module
79// within the registry, loading the module source code (if necessary), compiling
80// the module (if necessary), instantiating the module (i.e. creating the v8::Module)
81// instance, and evaluating the module (i.e. running the code). The part the
82// ModuleRegistry is responsible for is locating the module within the registry
83// and maintaining the cache of loaded modules along with additional metadata
84// like the compile cache. The actual compilation, instantiation, and evaluation
85// is performed by the IsolateModuleRegistry instance associated with the v8
86// isolate/context.
87//
88// The ModuleRegistry, ModuleBundle, and individual Module instances do not /
89// must not store any state that is specific to an isolate.
90//
91// Specifiers are always handled as URLs.
92//
93// Built-in modules are always identified using a prefixed-specifier that
94// can be parsed as an absolute URL. For instance, `node:buffer` is a fully
95// qualified, absolute URL whose protocol is `node:` and whose pathname is
96// `buffer`.
97//
98// Specifiers for modules that come from the worker bundle config are always
99// relative to `file:///bundle` (by default). Note that the `/bundle` part
100// will be configurable in the future and is shared with the virtual file
101// system used by the worker.
102//
103// For ESM modules, the specifier is accessible using import.meta.url.
104// All ESM modules support import.meta.resolve(...)
105//
106// If the first module in the module worker bundle is an ESM it will specify
107// import.meta.main = true.
108//
109// All modules are evaluated lazily when they are first imported. That is,
110// the ModuleRegistry will not actually generate the v8::Local<v8::Module>,
111// compile scripts, or evaluate it until the module is actually imported.
112// This means that any modules that are never actually imported by a worker
113// will never actually be compiled or evaluated. Keep in mind, that this does
114// not mean everything is dynamically imported. Static imports will still
115// be statically resolved and evaluated when the module that imports them
116// is resolved/evaluated. The key difference is that if a module is never
117// imported, it will never be resolved/compiled/evaluated.
118//
119// A ModuleBundle will have one of three basic types: Bundle, Builtin,
120// or Builtin-Only.
121//
122// * A Bundle ModuleBundle provides access to modules that are defined in the
123// worker bundle configuration. These may always be imported by the worker
124// bundle scripts and may override modules that are defined in the runtime.
125// * A Builtin ModuleBundle provides access to modules that are compiled into
126// the runtime and are importable by bundle scripts. These may always be
127// imported by the worker bundle scripts.
128// * A Builtin-Only ModuleBundle provides access to built-in modules that can
129// only be imported by other built-ins. These are invisible to bundle scripts.
130//
131// A special fourth type of ModuleBundle that is available only for local
132// dev in workerd is the Fallback type. This will use dynamic resolution
133// using a configurable fallback service. This will not be available in
134// production.
135//
136// ModuleBundle instances will also have a resolution priority. That is,
137// when a ModuleRegistry is using multiple ModuleBundle instances, the order
138// in which they are searched is significant. The search order will vary based
139// on resolve context.
140//
141// * If import is called from a bundle script, the search order is:
142//
143// 1. Bundle ModuleBundle
144// 2. Builtin ModuleBundle
145// 3. Fallback ModuleBundle (if available)
146//
147// * If import is called from a builtin script, the search order is:
148//
149// 1. Builtin ModuleBundle
150// 2. Builtin-Only ModuleBundle
151//
152// * If import is called from a builtin-only script, the search order is:
153//
154// 1. Builtin-Only ModuleBundle
155//
156// When the Fallback ModuleBundle is used, modules loaded from the fallback
157// are handled as if they are bundle scripts. The key difference, however, is
158// that the fallback service is not limited to a specific URL root.
159//
160// Notice that for built-ins, the Bundle ModuleBundle is not used. This
161// means it will not be possible to import worker bundle modules from a
162// built-in.
163//
164// The ModuleRegistry evaluates modules synchronously and all modules are
165// evaluated outside of the current IoContext (if any). This means the
166// evaluation of any module cannot perform any i/o and therefore is expected
167// to resolve synchronously. This allows for both ESM and CommonJS style
168// imports/requires. However, this also means that module bundles that do need
169// to be resolved, loaded, and evaluated asynchronously (like the fallback
170// service) must make appropriate arrangements to be able to do so.
171//
172// Metrics can be collected for module loading and resolution.
173//
174// Other details:
175//
176// * Module specifiers can be aliases for other specifiers, but only one
177// level of aliasing is supported. That is, an alias cannot point to another
178// alias. When a specifier resolves to an alias, the resolution starts over
179// and the aliased module can be located in any ModuleBundle. This is really
180// closer to a symbolic link or a redirect than a true alias but "alias" is
181// the term we've used historically with the fallback service in the original
182// implementation so we're sticking with it for now.
183// * Import attributes are not currently implemented but will be in a future
184// iteration. For now, if any import attributes are specified an error will
185// be thrown.
186// * ES modules all support the compile cache. When the ModuleRegistry is
187// shared across multiple replicas of a Worker, the compile cache will speed
188// up module compilation since the same compile cache can be used across all
189// replicas.
190 
191// The ResolveContext identifies the module that is being resolved along with
192// other key bits of information that may be used to resolve the module.
193struct ResolveContext final {
194 using Source = ResolveObserver::Source;
195 using Type = ResolveObserver::Context;
196 
197 // The type of module being resolved (one of BUNDLE, BUILTIN, or BUILTIN_ONLY)
198 Type type;
199 
200 // The source of the module resolution (e.g. import, dynamic import, require, etc);
201 Source source;
202 
203 // The fully resolved absolute import specifier URL for the module being resolved.
204 const Url& normalizedSpecifier;
205 
206 // The normalized specifier of the module that is importing this module.
207 const Url& referrerNormalizedSpecifier;
208 
209 // The raw specifier is the original specifier passed in, if any,
210 // before it was normalized into the specifier URL.
211 kj::Maybe<kj::StringPtr> rawSpecifier = kj::none;
212 
213 // Per the standard, import attributes are considered to be part of the
214 // specifier key when the module is resolved and cached.
215 kj::HashMap<kj::StringPtr, kj::StringPtr> attributes;
216};
217 
218class ModuleRegistry;
219 
220// The abstraction of a module within the ModuleRegistry.
221// Importantly, a Module is immutable once created and must be thread-safe.
222// The Module class itself represents the definition of a module and not
223// its actual instantiation.
224// The Module class is a virtual base class that is specialized for the
225// different types of modules being supported. There are essentially two
226// types of modules: ESM and Synthetic. ESM modules are the standard backed
227// by an ESM module script. Synthetic modules are any other type of module.
228class Module {
229 public:
230 // The types here echo the types in ResolveContext::Type but also include
231 // the FALLBACK, which is used to identify modules that are loaded from the
232 // fallback service.
233 enum class Type : uint8_t {
234 BUNDLE,
235 BUILTIN,
236 BUILTIN_ONLY,
237 FALLBACK,
238 };
239 
240 // The content type of the module, used to validate import attributes.
241 // For instance, `import data from './data.json' with { type: 'json' }`
242 // will only succeed if the module's content type is JSON.
243 enum class ContentType : uint8_t {
244 NONE, // No specific content type (ESM, CJS, builtin objects, etc.)
245 JSON, // JSON module
246 TEXT, // Text module
247 DATA, // Data/binary module
248 WASM, // WebAssembly module
249 };
250 
251 // The flags are set internally and are used to identify various properties
252 // of the module.
253 enum class Flags : uint8_t {
254 NONE = 0,
255 // A Module with the MAIN flag set would specify import.meta.main = true.
256 // This is generally only suitable for worker-bundle entry point modules,
257 // but could in theory be applied to any module. Typically only one module
258 // in the ModuleRegistry should have this flag set. We do not verify/enforce
259 // that however.
260 MAIN = 1 << 0,
261 // A Module with the ESM flag set is interpreted as an ECMAScript module.
262 ESM = 1 << 1,
263 // A Module with the EVAL flag set is interpreted as a module that requires
264 // code evaluation to complete. This is generally used for synthetic modules
265 // that require JavaScript evaluation outside of the current request context.
266 // The eval callback must be set or the flag is ignored.
267 EVAL = 1 << 2,
268 // A Module with the WASM flag set is a WebAssembly module.
269 WASM = 1 << 3,
270 };
271 
272 // The Evaluator is used to to ensure evaluation of a module outside of an
273 // IoContext, when necessary.
274 class Evaluator final {
275 public:
276 KJ_DISALLOW_COPY_AND_MOVE(Evaluator);
277 kj::Maybe<jsg::JsPromise> operator()(jsg::Lock& js,
278 const Module& module,
279 v8::Local<v8::Module> v8Module,
280 const CompilationObserver& observer) const;
281 
282 private:
283 Evaluator(const ModuleRegistry& registry): registry(registry) {}
284 const ModuleRegistry& registry;
285 friend class ModuleRegistry;
286 };
287 
288 KJ_DISALLOW_COPY_AND_MOVE(Module);
289 virtual ~Module() noexcept(false) = default;
290 
291 // The fully resolved absolute import specifier URL for the module.
292 inline const Url& id() const KJ_LIFETIMEBOUND {
293 return id_;
294 }
295 
296 // The module type.
297 inline Type type() const {
298 return type_;
299 }
300 
301 // If isEsm() returns false the implication is that the module is a synthetic module
302 bool isEsm() const;
303 
304 // If isMain() returns true, then import.meta.main will be true for this module
305 bool isMain() const;
306 
307 // If isEval() returns true, then the module requires code evaluation to complete
308 // outside of a request context (that is, it cannot perform certain I/O tasks).
309 bool isEval() const;
310 
311 // If isWasm() returns true, then the module is a WebAssembly module.
312 bool isWasm() const;
313 
314 // Returns the content type of the module.
315 inline ContentType contentType() const {
316 return contentType_;
317 }
318 
319 // Returns a v8::Module representing this Module definition for the given isolate.
320 // The return value follows the established v8 rules for Maybe. If the returned
321 // maybe is empty, then an exception should have been scheduled on the isolate
322 // via the lock. Do not throw C++ exceptions from this method unless they are fatal.
323 // The returned v8::Module is not yet instantiated.
324 virtual v8::MaybeLocal<v8::Module> getDescriptor(
325 Lock& js, const CompilationObserver& observer) const KJ_WARN_UNUSED_RESULT = 0;
326 
327 // Determines if this module can be resolved in the given context.
328 virtual bool evaluateContext(const ResolveContext& context) const KJ_WARN_UNUSED_RESULT;
329 
330 // Instantiates the given module. If false is returned, then an exception should
331 // have been scheduled on the isolate via the lock. Do not throw C++ exceptions
332 // from this method unless they are fatal.
333 bool instantiate(Lock& js,
334 v8::Local<v8::Module> module,
335 const CompilationObserver& observer) const KJ_WARN_UNUSED_RESULT;
336 
337 // Evaluates the given module, returning the result of the evaluation in the form
338 // of a JS value. This is the value that is actually returned by the import or
339 // require. The return value follows the established v8 rules for Maybe. If the
340 // returned maybe is empty, then an exception should have been scheduled on the
341 // isolate via the lock. If the module has not yet been instantiated, it will
342 // be instantiated first. Do not throw C++ exceptions from this method unless they
343 // are fatal.
344 virtual v8::MaybeLocal<v8::Value> evaluate(Lock& js,
345 v8::Local<v8::Module> module,
346 const CompilationObserver& observer,
347 const Evaluator& maybeEvaluate) const KJ_WARN_UNUSED_RESULT = 0;
348 
349 virtual v8::MaybeLocal<v8::Value> actuallyEvaluate(Lock& js,
350 v8::Local<v8::Module> module,
351 const CompilationObserver& observer) const KJ_WARN_UNUSED_RESULT = 0;
352 
353 // A helper interface that is used to make it easier for a synthetic module
354 // evaluation callback to set the exports of the module.
355 class ModuleNamespace final {
356 public:
357 explicit ModuleNamespace(
358 v8::Local<v8::Module> inner, kj::ArrayPtr<const kj::String> namedExports);
359 KJ_DISALLOW_COPY_AND_MOVE(ModuleNamespace);
360 
361 bool set(Lock& js, kj::StringPtr name, JsValue value) const;
362 bool setDefault(Lock& js, JsValue value) const;
363 
364 // Returns the list of the named exports expected for the module
365 // (should not include the "default")
366 kj::ArrayPtr<const kj::StringPtr> getNamedExports() const;
367 
368 private:
369 v8::Local<v8::Module> inner;
370 kj::HashSet<kj::StringPtr> namedExports;
371 };
372 
373 // The EvaluateCallback is used to evaluate a synthetic module. The callback
374 // is called after the module is resolved and instantiated. Note that this
375 // is different from the Module::Evaluator, which is used to ensure that
376 // evaluation of a module occurs outside of an IoContext. This callback
377 // is always called to actually perform the evaluation of a synthetic module.
378 // If false is returned, then an exception should have been scheduled on the isolate.
379 using EvaluateCallback =
380 Function<bool(const Url&, const ModuleNamespace&, const CompilationObserver&)>;
381 
382 // Returns a new synthetic module. The callback is invoked to evaluate the module. Due to
383 // the nature of synthetic modules, the callback is expected to perform all necessary evaluation
384 // synchronously and return a boolean indicating whether the evaluation succeeded or not. The
385 // evaluation cannot be async because V8 does not wait for the synthetic module evaluation
386 // promises to resolve before it considers the module to be evaluated. The most it will do is
387 // track errors thrown synchronously from the callback to determine whether evaluation failed.
388 static kj::Own<Module> newSynthetic(Url id,
389 Type type,
390 EvaluateCallback callback,
391 kj::Array<kj::String> namedExports = nullptr,
392 Flags flags = Flags::NONE,
393 ContentType contentType = ContentType::NONE);
394 
395 // Creates a new ESM module that takes ownership of the given code array.
396 // This is generally used to construct ESM modules from a worker bundle.
397 static kj::Own<Module> newEsm(
398 Url id, Type type, kj::Array<const char> code, Flags flags = Flags::NONE);
399 
400 // Creates a new ESM module that does not take ownership of the given code
401 // array. This is used to construct ESM modules from compiled-in built-in
402 // modules.
403 // This variation of newEsm does not take Flags as none of the existing
404 // Flags are relevant other than the ESM flag which will be set automatically.
405 static kj::Own<Module> newEsm(Url id, Type type, kj::ArrayPtr<const char> code);
406 
407 // The following methods are used to create the evaluation callbacks for various
408 // kinds of common simple synthetic module types. The module registry is not
409 // limited to just these kinds of modules, however. These are just the most
410 // common.
411 
412 static EvaluateCallback newTextModuleHandler(kj::ArrayPtr<const char> data) KJ_WARN_UNUSED_RESULT;
413 static EvaluateCallback newDataModuleHandler(
414 kj::ArrayPtr<const kj::byte> data) KJ_WARN_UNUSED_RESULT;
415 static EvaluateCallback newJsonModuleHandler(kj::ArrayPtr<const char> data) KJ_WARN_UNUSED_RESULT;
416 static EvaluateCallback newWasmModuleHandler(
417 kj::ArrayPtr<const kj::byte> data) KJ_WARN_UNUSED_RESULT;
418 
419 // An eval function is used for CommonJS style modules (including Node.js compat
420 // modules. The expectation is that this method will be called when the CommonJS
421 // style module is evaluated (e.g. within the EvaluationCallback).
422 static Function<void()> compileEvalFunction(Lock& js,
423 kj::StringPtr code,
424 kj::StringPtr name,
425 kj::Maybe<JsObject> compileExtensions,
426 const CompilationObserver& observer) KJ_WARN_UNUSED_RESULT;
427 
428 // A CjsStyleModuleHandler is used for CommonJS style modules (including
429 // The template type T must be a jsg::Object that implements a getExports(Lock&)
430 // method returning a JsValue. This is set as the default export of the
431 // synthetic module. All methods and properties exposed by the template
432 // type T are exposed as additional globals within the executed scope.
433 template <typename T, typename TypeWrapper>
434 static EvaluateCallback newCjsStyleModuleHandler(
435 kj::StringPtr source, kj::StringPtr name) KJ_WARN_UNUSED_RESULT {
436 return [source, name](Lock& js, const Url& id, const Module::ModuleNamespace& ns,
437 const CompilationObserver& observer) mutable -> bool {
438 return js.tryCatch([&] {
439 auto& wrapper = TypeWrapper::from(js.v8Isolate);
440 auto ext = js.alloc<T>(js, id);
441 ns.setDefault(js, ext->getExports(js));
442 auto fn = Module::compileEvalFunction(js, source, name,
443 JsObject(wrapper.wrap(js, js.v8Context(), kj::none, ext.addRef())), observer);
444 fn(js);
445 // If there are named exports specified for the module namespace,
446 // then we want to examine the ext->getExports() to extract those.
447 JsValue exports = ext->getModuleExports(js);
448 KJ_IF_SOME(obj, exports.template tryCast<JsObject>()) {
449 for (auto& name: ns.getNamedExports()) {
450 ns.set(js, name, obj.get(js, name));
451 }
452 }
453 return ns.setDefault(js, exports);
454 }, [&](Value exception) {
455 js.v8Isolate->ThrowException(exception.getHandle(js));
456 return false;
457 });
458 };
459 }
460 
461 // A ModuleHandler used to create a synthetic module that is backed by a jsg::Object.
462 template <typename T, typename TypeWrapper, typename Func>
463 static EvaluateCallback newJsgObjectModuleHandler(Func factory) KJ_WARN_UNUSED_RESULT {
464 return [factory = kj::mv(factory)](Lock& js, const Url& id, const Module::ModuleNamespace& ns,
465 const CompilationObserver& observer) mutable -> bool {
466 Ref<T> instance = factory(js);
467 auto value =
468 TypeWrapper::from(js.v8Isolate).wrap(js, js.v8Context(), kj::none, kj::mv(instance));
469 return ns.setDefault(js, JsValue(value));
470 };
471 }
472 
473 protected:
474 Module(Url id, Type type, Flags flags = Flags::NONE, ContentType contentType = ContentType::NONE);
475 
476 private:
477 const Url id_;
478 Type type_;
479 Flags flags_;
480 ContentType contentType_;
481 
482 // TODO: Support source objects as optional instantiation-hook creations and move
483 // Wasm compilation to start at instantiation-time instead of evaluation-time.
484 // kj::Maybe<HashableV8Ref<v8::Object>> sourceObject_;
485};
486 
487constexpr Module::Flags operator&(const Module::Flags& a, const Module::Flags& b) {
488 return static_cast<Module::Flags>(static_cast<uint8_t>(a) & static_cast<uint8_t>(b));
489}
490constexpr Module::Flags operator|(const Module::Flags& a, const Module::Flags& b) {
491 return static_cast<Module::Flags>(static_cast<uint8_t>(a) | static_cast<uint8_t>(b));
492}
493 
494// A ModuleBundle is a source of modules that can be imported or required.
495// A ModuleRegistry is a collection of ModuleBundles.
496// Importantly, a ModuleBundle is immutable once created with exception to
497// any internal caching it may use to optimize resolution. Accesses to the
498// bundle must be thread-safe.
499class ModuleBundle {
500 public:
501 using Type = Module::Type;
502 
503 // A Builder is used to construct a ModuleBundle.
504 class Builder {
505 public:
506 KJ_DISALLOW_COPY_AND_MOVE(Builder);
507 
508 // The resolve callback is used to perform resolution of a module context.
509 // If the callback returns a string, then resolution will start over with
510 // the new specifier. If the callback returns a Module, then that module
511 // will be used as the resolved module. If the callback returns kj::none,
512 // then the module is not resolved.
513 using ResolveCallback =
514 kj::Function<kj::Maybe<kj::OneOf<kj::String, kj::Own<Module>>>(const ResolveContext&)>;
515 
516 Builder& add(const Url& id, ResolveCallback callback) KJ_LIFETIMEBOUND;
517 
518 Builder& alias(const Url& alias, const Url& id) KJ_LIFETIMEBOUND;
519 
520 kj::Own<ModuleBundle> finish() KJ_WARN_UNUSED_RESULT;
521 
522 inline Type type() const {
523 return type_;
524 }
525 
526 protected:
527 Builder(Type type);
528 
529 void ensureIsNotBundleSpecifier(const Url& id);
530 
531 Type type_;
532 kj::HashMap<Url, ResolveCallback> modules_;
533 kj::HashMap<Url, Url> aliases_;
534 };
535 
536 // Used to build a ModuleBundle representing modules sourced from a worker bundle.
537 class BundleBuilder final: public Builder {
538 public:
539 BundleBuilder(const jsg::Url& bundleBase);
540 KJ_DISALLOW_COPY_AND_MOVE(BundleBuilder);
541 
542 using EvaluateCallback = Module::EvaluateCallback;
543 
544 BundleBuilder& addSyntheticModule(kj::StringPtr name,
545 EvaluateCallback callback,
546 kj::Array<kj::String> namedExports = nullptr,
547 Module::ContentType contentType = Module::ContentType::NONE) KJ_LIFETIMEBOUND;
548 
549 BundleBuilder& addEsmModule(kj::StringPtr name,
550 kj::ArrayPtr<const char> code,
551 Module::Flags flags = Module::Flags::ESM) KJ_LIFETIMEBOUND;
552 
553 // Overload that takes ownership of the source data. Use this when the
554 // source buffer may not outlive the module registry (e.g. transpiled
555 // TypeScript where the backing rust::String has shorter lifetime).
556 BundleBuilder& addEsmModule(kj::StringPtr name,
557 kj::Array<const char> code,
558 Module::Flags flags = Module::Flags::ESM) KJ_LIFETIMEBOUND;
559 
560 BundleBuilder& addWasmModule(
561 kj::StringPtr name, kj::ArrayPtr<const kj::byte> data) KJ_LIFETIMEBOUND;
562 
563 BundleBuilder& alias(kj::StringPtr alias, kj::StringPtr name) KJ_LIFETIMEBOUND;
564 
565 private:
566 const jsg::Url& bundleBase;
567 };
568 
569 // Used to build a ModuleBundle representing modules sources from the runtime.
570 class BuiltinBuilder final: public Builder {
571 public:
572 enum class Type {
573 BUILTIN,
574 BUILTIN_ONLY,
575 };
576 BuiltinBuilder(Type type = Type::BUILTIN);
577 KJ_DISALLOW_COPY_AND_MOVE(BuiltinBuilder);
578 
579 BuiltinBuilder& addSynthetic(
580 const Url& id, BundleBuilder::EvaluateCallback callback) KJ_LIFETIMEBOUND;
581 
582 BuiltinBuilder& addEsm(const Url& id, kj::ArrayPtr<const char> source) KJ_LIFETIMEBOUND;
583 
584 // Adds a module that is implemented in C++ as a jsg::Object
585 template <typename T, typename TypeWrapper>
586 BuiltinBuilder& addObject(const Url& id) KJ_LIFETIMEBOUND {
587 ensureIsNotBundleSpecifier(id);
588 add(id,
589 [id = id.clone(), type = type()](const ResolveContext& context) mutable
590 -> kj::Maybe<kj::OneOf<kj::String, kj::Own<Module>>> {
591 if (context.normalizedSpecifier != id) return kj::none;
592 kj::Own<Module> mod = Module::newSynthetic(kj::mv(id), type,
593 [](Lock& js, const Url& id, const Module::ModuleNamespace& ns,
594 const CompilationObserver&) {
595 auto value = TypeWrapper::from(js.v8Isolate)
596 .wrap(js, js.v8Context(), kj::none, js.alloc<T>(js, id));
597 ns.setDefault(js, JsValue(value));
598 return true;
599 });
600 return kj::Maybe<kj::OneOf<kj::String, kj::Own<Module>>>(kj::mv(mod));
601 });
602 return *this;
603 }
604 };
605 
606 static kj::Own<ModuleBundle> newFallbackBundle(
607 Builder::ResolveCallback callback) KJ_WARN_UNUSED_RESULT;
608 
609 static void getBuiltInBundleFromCapnp(BuiltinBuilder& builder, Bundle::Reader bundle);
610 
611 // Overload that accepts a per-module filter predicate. Only modules for which
612 // the filter returns true are added to the builder. This is used for per-module
613 // feature flag gating (e.g., individual node:* modules behind compat flags).
614 static void getBuiltInBundleFromCapnp(BuiltinBuilder& builder,
615 Bundle::Reader bundle,
616 kj::Function<bool(::workerd::jsg::Module::Reader)> filter);
617 
618 KJ_DISALLOW_COPY_AND_MOVE(ModuleBundle);
619 
620 inline Type type() const {
621 return type_;
622 }
623 
624 virtual ~ModuleBundle() noexcept(false) = default;
625 
626 struct Resolved {
627 // This struct exists only to work around the limitation that a kj::OneOf
628 // cannot be used with a const reference or we get a compiler error.
629 kj::Maybe<const Module&> module;
630 kj::Maybe<kj::String> specifier;
631 };
632 
633 // Load a module context. If a string is returned, then it must be a
634 // module specifier. The resolution will start over with the new specifier.
635 // If a Module is returned, then that is the loaded module. If kj::none
636 // is returned, then the module is not known by this module.
637 virtual kj::Maybe<Resolved> lookup(
638 const ResolveContext& context) KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT = 0;
639 
640 protected:
641 ModuleBundle(Type type);
642 
643 private:
644 Type type_;
645};
646 
647// A ModuleRegistry is a collection of zero or more ModuleBundles.
648// Importantly, the ModuleRegistry is immutable once created and
649// must be thread-safe. In workerd, the module registry is created
650// and owned by a single Worker instance. In production, however, a
651// single ModuleRegistry instance may be shared by multiple replicas
652// of a Worker and therefore must be AtomicRefcounted.
653 
654// When passed to tryResolveModuleNamespace, controls whether non-ESM
655// (synthetic) modules return the default export instead of the full
656// module namespace. Matches Node.js require() semantics.
657WD_STRONG_BOOL(UnwrapDefault);
658 
659class ModuleRegistry final: public kj::AtomicRefcounted, public ModuleRegistryBase {
660 private:
661 enum BundleIndices { kBundle, kBuiltin, kBuiltinOnly, kFallback, kBundleCount };
662 
663 public:
664 // The EvalCallback is used to to ensure evaluation of a module outside of an
665 // IoContext, when necessary. If the EvalCallback is not set, then the
666 // Flag::EVAL on a module is ignored. If the EvalCallback is set, then any
667 // Modules that have the Flag::EVAL set will have their evaluation deferred
668 // to this callback.
669 using EvalCallback = Function<jsg::JsPromise(
670 const Module& module, v8::Local<v8::Module> v8Module, const CompilationObserver& observer)>;
671 
672 class Builder final {
673 public:
674 enum class Options {
675 NONE = 0,
676 // When set, allows the ModuleRegistry to use a fallback ModuleBundle to
677 // dynamically resolve a module that cannot be resolved by any other
678 // registered bundles. The fallback service is only used when using the
679 // ResolveContext::Type::BUNDLE context and is always the last bundle
680 // checked. The fallback service should only be used for local dev.
681 ALLOW_FALLBACK = 1 << 0,
682 };
683 Builder(const ResolveObserver& observer,
684 const jsg::Url& bundleBase,
685 Options options = Options::NONE);
686 KJ_DISALLOW_COPY_AND_MOVE(Builder);
687 
688 Builder& add(kj::Own<ModuleBundle> bundle) KJ_LIFETIMEBOUND;
689 
690 kj::Arc<ModuleRegistry> finish() KJ_WARN_UNUSED_RESULT;
691 
692 Builder& setEvalCallback(EvalCallback callback) KJ_LIFETIMEBOUND;
693 
694 capnp::SchemaLoader& getSchemaLoader() {
695 return *schemaLoader;
696 }
697 
698 private:
699 bool allowsFallback() const;
700 
701 // One slot for each of ModuleBundle::Type
702 const ResolveObserver& observer;
703 const jsg::Url& bundleBase;
704 const Options options;
705 kj::FixedArray<kj::Vector<kj::Own<ModuleBundle>>, ModuleRegistry::kBundleCount> bundles_;
706 kj::Maybe<EvalCallback> maybeEvalCallback = kj::none;
707 kj::Own<capnp::SchemaLoader> schemaLoader;
708 friend class ModuleRegistry;
709 };
710 
711 kj::Maybe<const Module&> lookup(
712 const ResolveContext& context) const KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT;
713 
714 // Attaches the ModuleRegistry to the given isolate by creating an IsolateModuleRegistry
715 // and linking that to the isolate.
716 kj::Own<void> attachToIsolate(Lock& js, const CompilationObserver& observer) const override;
717 
718 // Synchronously resolve the specified module from the registry bound to the given lock.
719 // This will throw a JsExceptionThrown exception if the module cannot be found or an
720 // error occurs while the module is being evaluated. Modules resolved with this method
721 // must be capable of fully evaluating within one drain of the microtask queue.
722 static JsValue resolve(Lock& js,
723 kj::StringPtr specifier,
724 kj::StringPtr exportName = "default"_kjc,
725 ResolveContext::Type type = ResolveContext::Type::BUNDLE,
726 ResolveContext::Source source = ResolveContext::Source::INTERNAL,
727 kj::Maybe<const Url&> maybeReferrer = kj::none);
728 
729 // Synchronously resolve the specified module from the registry bound to the given lock.
730 // This variant will return kj::none if the module cannot be found but will throw a
731 // JsExceptionThrown exception if an error occurs while the module is being evaluated.
732 // Modules resolved with this method must be capable of fully evaluating within one
733 // drain of the microtask queue.
734 static kj::Maybe<JsValue> tryResolveModuleNamespace(Lock& js,
735 kj::StringPtr specifier,
736 ResolveContext::Type type = ResolveContext::Type::BUNDLE,
737 ResolveContext::Source source = ResolveContext::Source::INTERNAL,
738 kj::Maybe<const Url&> maybeReferrer = kj::none,
739 UnwrapDefault unwrapDefault = UnwrapDefault::NO);
740 
741 // The constructor is public because kj::heap requires is to be. Do not
742 // use the constructor directly. Use the ModuleRegistry::Builder
743 ModuleRegistry(ModuleRegistry::Builder* builder);
744 KJ_DISALLOW_COPY_AND_MOVE(ModuleRegistry);
745 
746 const jsg::Url& getBundleBase() const {
747 return bundleBase;
748 }
749 
750 const capnp::SchemaLoader& getSchemaLoader() const override {
751 return *schemaLoader;
752 }
753 
754 const Module::Evaluator getEvaluator() const {
755 return Module::Evaluator(*this);
756 }
757 
758 private:
759 struct Impl {
760 // One slot for each of ModuleBundle::Type, within each slot is
761 // an array of bundles of that type in registration order.
762 kj::FixedArray<kj::Array<kj::Own<ModuleBundle>>, kBundleCount> bundles;
763 Impl(kj::ArrayPtr<kj::Vector<kj::Own<ModuleBundle>>> bundles);
764 };
765 
766 const ResolveObserver& observer;
767 const jsg::Url& bundleBase;
768 kj::MutexGuarded<Impl> impl;
769 // Marked mutable because kj::Function::operator() is non-const, but the eval
770 // callback is conceptually const — it is only ever invoked while holding the
771 // isolate lock, so concurrent mutation is not a concern.
772 mutable kj::Maybe<EvalCallback> maybeEvalCallback = kj::none;
773 kj::Own<capnp::SchemaLoader> schemaLoader;
774 
775 struct ModuleRef {
776 const Module& module;
777 };
778 using ModuleOrRedirect = kj::OneOf<ModuleRef, Url>;
779 
780 kj::Maybe<const Module&> lookupImpl(Impl& impl,
781 const ResolveContext& context,
782 bool recursed) const KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT;
783 
784 kj::Maybe<ModuleOrRedirect> tryFindInBundleGroup(const ResolveContext& context,
785 kj::ArrayPtr<kj::Own<ModuleBundle>> bundles) const KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT;
786 
787 // Attempts to find the module in the given bundle. If found, returns
788 // const Module&, if an alias is found, returns the new ResolveContext
789 // to try again with. If not found, returns kj::none.
790 static kj::Maybe<ModuleOrRedirect> tryFindInBundle(const ResolveContext& context,
791 ModuleBundle& bundle,
792 const Url& bundleBase) KJ_WARN_UNUSED_RESULT;
793 
794 kj::Maybe<jsg::JsPromise> evaluateImpl(jsg::Lock& js,
795 const Module& module,
796 v8::Local<v8::Module> v8Module,
797 const CompilationObserver& observer) const;
798 
799 friend class Module::Evaluator;
800};
801 
802constexpr ModuleRegistry::Builder::Options operator|(
803 const ModuleRegistry::Builder::Options& a, const ModuleRegistry::Builder::Options& b) {
804 return static_cast<ModuleRegistry::Builder::Options>(
805 static_cast<uint8_t>(a) | static_cast<uint8_t>(b));
806}
807constexpr ModuleRegistry::Builder::Options operator&(
808 const ModuleRegistry::Builder::Options& a, const ModuleRegistry::Builder::Options& b) {
809 return static_cast<ModuleRegistry::Builder::Options>(
810 static_cast<uint8_t>(a) & static_cast<uint8_t>(b));
811}
812 
813} // namespace workerd::jsg::modules