// Copyright (c) 2017-2026 Cloudflare, Inc. // Licensed under the Apache 2.0 license found in the LICENSE file or at: // https://opensource.org/licenses/Apache-2.0 #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace workerd::jsg::modules { // Defines and implements workerd's (new) module loader subsystem. // // This new implementation of ModuleRegistry is designed to be more flexible, // modular, and extensible than the previous implementation. It is also designed // to support handling import specifiers as URLs, implement import.meta, properly // handle import attributes, sharing of modules across isolate replicas, and more. // // Every Worker has exactly one ModuleRegistry associated with composed of // of or more ModuleBundles (e.g. a ModuleBundle with modules from the worker // bundle, a ModuleBundle representing built-in modules, a ModuleBundle that is // a fallback service, etc). When multiple replicas of a Worker are created, they // may share the same ModuleRegistry instance, since multiple workers may be // acting across multiple threads, the ModuleRegistry instance must be thread-safe. // // The ModuleRegistry is the collection of individual Modules that can be // imported (i.e. `import * from '...'` and `await import('...')`) or required // (i.e. `require('...')`). // // The ModuleRegistry is conceptually immutable once created (individual // instances may support dynamic resolution using, for instance, a fallback // service but there is no API to manipulate the ModuleRegistry once it has // been created). There is internal state that may change, such as caching of // resolved modules, and compile cache data, but the set of modules that are // available within the ModuleRegistry does not change. The only exception to // this is the fallback service, which may dynamically resolved modules that // are not otherwise available in the ModuleRegistry, but that is only used // for local development in workerd and is never used in production. // // All access to the ModuleRegistry is thread-safe by way of (a) requiring callers // to hold and pass the jsg::Lock& when resolving modules and (b) having the // ModuleRegistry instance by AtomicRefcounted with MutexGuarded locks. With // exception to the fallback service, all operations within the ModuleRegistry // are either synchronous or expected to use JavaScript promises to perform // asynchronous operations. // // When a Worker is created, a ModuleBundle is created to contain Module // definitions declared by the worker source bundle configuration. One or more // are also provided that provides modules from within the runtime. // // When a v8::Isolate* is created for a particular worker, the ModuleRegistry // instance will be bound to the isolate in the form of a new IsolateModuleRegistry // instance. This is the interface that is actually used to resolve specifiers into // imported modules. The ModuleRegistry instance itself is owned by the Worker::Script // and the IsolateModuleRegistry instance can be viewed as a client. // // When there are multiple IsolateModuleRegistry instances pointing to the same // ModuleRegistry instance, only one can peform resolution at a time, controlled // by an exclusive lock on a MutexGuarded within the ModuleRegistry. // // A note on language use: Resolving a module vs. Loading a module. We use // the term "resolving" to refer to the overall process of taking a specifier // and turning it into a Module instance. This includes locating the module // within the registry, loading the module source code (if necessary), compiling // the module (if necessary), instantiating the module (i.e. creating the v8::Module) // instance, and evaluating the module (i.e. running the code). The part the // ModuleRegistry is responsible for is locating the module within the registry // and maintaining the cache of loaded modules along with additional metadata // like the compile cache. The actual compilation, instantiation, and evaluation // is performed by the IsolateModuleRegistry instance associated with the v8 // isolate/context. // // The ModuleRegistry, ModuleBundle, and individual Module instances do not / // must not store any state that is specific to an isolate. // // Specifiers are always handled as URLs. // // Built-in modules are always identified using a prefixed-specifier that // can be parsed as an absolute URL. For instance, `node:buffer` is a fully // qualified, absolute URL whose protocol is `node:` and whose pathname is // `buffer`. // // Specifiers for modules that come from the worker bundle config are always // relative to `file:///bundle` (by default). Note that the `/bundle` part // will be configurable in the future and is shared with the virtual file // system used by the worker. // // For ESM modules, the specifier is accessible using import.meta.url. // All ESM modules support import.meta.resolve(...) // // If the first module in the module worker bundle is an ESM it will specify // import.meta.main = true. // // All modules are evaluated lazily when they are first imported. That is, // the ModuleRegistry will not actually generate the v8::Local, // compile scripts, or evaluate it until the module is actually imported. // This means that any modules that are never actually imported by a worker // will never actually be compiled or evaluated. Keep in mind, that this does // not mean everything is dynamically imported. Static imports will still // be statically resolved and evaluated when the module that imports them // is resolved/evaluated. The key difference is that if a module is never // imported, it will never be resolved/compiled/evaluated. // // A ModuleBundle will have one of three basic types: Bundle, Builtin, // or Builtin-Only. // // * A Bundle ModuleBundle provides access to modules that are defined in the // worker bundle configuration. These may always be imported by the worker // bundle scripts and may override modules that are defined in the runtime. // * A Builtin ModuleBundle provides access to modules that are compiled into // the runtime and are importable by bundle scripts. These may always be // imported by the worker bundle scripts. // * A Builtin-Only ModuleBundle provides access to built-in modules that can // only be imported by other built-ins. These are invisible to bundle scripts. // // A special fourth type of ModuleBundle that is available only for local // dev in workerd is the Fallback type. This will use dynamic resolution // using a configurable fallback service. This will not be available in // production. // // ModuleBundle instances will also have a resolution priority. That is, // when a ModuleRegistry is using multiple ModuleBundle instances, the order // in which they are searched is significant. The search order will vary based // on resolve context. // // * If import is called from a bundle script, the search order is: // // 1. Bundle ModuleBundle // 2. Builtin ModuleBundle // 3. Fallback ModuleBundle (if available) // // * If import is called from a builtin script, the search order is: // // 1. Builtin ModuleBundle // 2. Builtin-Only ModuleBundle // // * If import is called from a builtin-only script, the search order is: // // 1. Builtin-Only ModuleBundle // // When the Fallback ModuleBundle is used, modules loaded from the fallback // are handled as if they are bundle scripts. The key difference, however, is // that the fallback service is not limited to a specific URL root. // // Notice that for built-ins, the Bundle ModuleBundle is not used. This // means it will not be possible to import worker bundle modules from a // built-in. // // The ModuleRegistry evaluates modules synchronously and all modules are // evaluated outside of the current IoContext (if any). This means the // evaluation of any module cannot perform any i/o and therefore is expected // to resolve synchronously. This allows for both ESM and CommonJS style // imports/requires. However, this also means that module bundles that do need // to be resolved, loaded, and evaluated asynchronously (like the fallback // service) must make appropriate arrangements to be able to do so. // // Metrics can be collected for module loading and resolution. // // Other details: // // * Module specifiers can be aliases for other specifiers, but only one // level of aliasing is supported. That is, an alias cannot point to another // alias. When a specifier resolves to an alias, the resolution starts over // and the aliased module can be located in any ModuleBundle. This is really // closer to a symbolic link or a redirect than a true alias but "alias" is // the term we've used historically with the fallback service in the original // implementation so we're sticking with it for now. // * Import attributes are not currently implemented but will be in a future // iteration. For now, if any import attributes are specified an error will // be thrown. // * ES modules all support the compile cache. When the ModuleRegistry is // shared across multiple replicas of a Worker, the compile cache will speed // up module compilation since the same compile cache can be used across all // replicas. // The ResolveContext identifies the module that is being resolved along with // other key bits of information that may be used to resolve the module. struct ResolveContext final { using Source = ResolveObserver::Source; using Type = ResolveObserver::Context; // The type of module being resolved (one of BUNDLE, BUILTIN, or BUILTIN_ONLY) Type type; // The source of the module resolution (e.g. import, dynamic import, require, etc); Source source; // The fully resolved absolute import specifier URL for the module being resolved. const Url& normalizedSpecifier; // The normalized specifier of the module that is importing this module. const Url& referrerNormalizedSpecifier; // The raw specifier is the original specifier passed in, if any, // before it was normalized into the specifier URL. kj::Maybe rawSpecifier = kj::none; // Per the standard, import attributes are considered to be part of the // specifier key when the module is resolved and cached. kj::HashMap attributes; }; class ModuleRegistry; // The abstraction of a module within the ModuleRegistry. // Importantly, a Module is immutable once created and must be thread-safe. // The Module class itself represents the definition of a module and not // its actual instantiation. // The Module class is a virtual base class that is specialized for the // different types of modules being supported. There are essentially two // types of modules: ESM and Synthetic. ESM modules are the standard backed // by an ESM module script. Synthetic modules are any other type of module. class Module { public: // The types here echo the types in ResolveContext::Type but also include // the FALLBACK, which is used to identify modules that are loaded from the // fallback service. enum class Type : uint8_t { BUNDLE, BUILTIN, BUILTIN_ONLY, FALLBACK, }; // The content type of the module, used to validate import attributes. // For instance, `import data from './data.json' with { type: 'json' }` // will only succeed if the module's content type is JSON. enum class ContentType : uint8_t { NONE, // No specific content type (ESM, CJS, builtin objects, etc.) JSON, // JSON module TEXT, // Text module DATA, // Data/binary module WASM, // WebAssembly module }; // The flags are set internally and are used to identify various properties // of the module. enum class Flags : uint8_t { NONE = 0, // A Module with the MAIN flag set would specify import.meta.main = true. // This is generally only suitable for worker-bundle entry point modules, // but could in theory be applied to any module. Typically only one module // in the ModuleRegistry should have this flag set. We do not verify/enforce // that however. MAIN = 1 << 0, // A Module with the ESM flag set is interpreted as an ECMAScript module. ESM = 1 << 1, // A Module with the EVAL flag set is interpreted as a module that requires // code evaluation to complete. This is generally used for synthetic modules // that require JavaScript evaluation outside of the current request context. // The eval callback must be set or the flag is ignored. EVAL = 1 << 2, // A Module with the WASM flag set is a WebAssembly module. WASM = 1 << 3, }; // The Evaluator is used to to ensure evaluation of a module outside of an // IoContext, when necessary. class Evaluator final { public: KJ_DISALLOW_COPY_AND_MOVE(Evaluator); kj::Maybe operator()(jsg::Lock& js, const Module& module, v8::Local v8Module, const CompilationObserver& observer) const; private: Evaluator(const ModuleRegistry& registry): registry(registry) {} const ModuleRegistry& registry; friend class ModuleRegistry; }; KJ_DISALLOW_COPY_AND_MOVE(Module); virtual ~Module() noexcept(false) = default; // The fully resolved absolute import specifier URL for the module. inline const Url& id() const KJ_LIFETIMEBOUND { return id_; } // The module type. inline Type type() const { return type_; } // If isEsm() returns false the implication is that the module is a synthetic module bool isEsm() const; // If isMain() returns true, then import.meta.main will be true for this module bool isMain() const; // If isEval() returns true, then the module requires code evaluation to complete // outside of a request context (that is, it cannot perform certain I/O tasks). bool isEval() const; // If isWasm() returns true, then the module is a WebAssembly module. bool isWasm() const; // Returns the content type of the module. inline ContentType contentType() const { return contentType_; } // Returns a v8::Module representing this Module definition for the given isolate. // The return value follows the established v8 rules for Maybe. If the returned // maybe is empty, then an exception should have been scheduled on the isolate // via the lock. Do not throw C++ exceptions from this method unless they are fatal. // The returned v8::Module is not yet instantiated. virtual v8::MaybeLocal getDescriptor( Lock& js, const CompilationObserver& observer) const KJ_WARN_UNUSED_RESULT = 0; // Determines if this module can be resolved in the given context. virtual bool evaluateContext(const ResolveContext& context) const KJ_WARN_UNUSED_RESULT; // Instantiates the given module. If false is returned, then an exception should // have been scheduled on the isolate via the lock. Do not throw C++ exceptions // from this method unless they are fatal. bool instantiate(Lock& js, v8::Local module, const CompilationObserver& observer) const KJ_WARN_UNUSED_RESULT; // Evaluates the given module, returning the result of the evaluation in the form // of a JS value. This is the value that is actually returned by the import or // require. The return value follows the established v8 rules for Maybe. If the // returned maybe is empty, then an exception should have been scheduled on the // isolate via the lock. If the module has not yet been instantiated, it will // be instantiated first. Do not throw C++ exceptions from this method unless they // are fatal. virtual v8::MaybeLocal evaluate(Lock& js, v8::Local module, const CompilationObserver& observer, const Evaluator& maybeEvaluate) const KJ_WARN_UNUSED_RESULT = 0; virtual v8::MaybeLocal actuallyEvaluate(Lock& js, v8::Local module, const CompilationObserver& observer) const KJ_WARN_UNUSED_RESULT = 0; // A helper interface that is used to make it easier for a synthetic module // evaluation callback to set the exports of the module. class ModuleNamespace final { public: explicit ModuleNamespace( v8::Local inner, kj::ArrayPtr namedExports); KJ_DISALLOW_COPY_AND_MOVE(ModuleNamespace); bool set(Lock& js, kj::StringPtr name, JsValue value) const; bool setDefault(Lock& js, JsValue value) const; // Returns the list of the named exports expected for the module // (should not include the "default") kj::ArrayPtr getNamedExports() const; private: v8::Local inner; kj::HashSet namedExports; }; // The EvaluateCallback is used to evaluate a synthetic module. The callback // is called after the module is resolved and instantiated. Note that this // is different from the Module::Evaluator, which is used to ensure that // evaluation of a module occurs outside of an IoContext. This callback // is always called to actually perform the evaluation of a synthetic module. // If false is returned, then an exception should have been scheduled on the isolate. using EvaluateCallback = Function; // Returns a new synthetic module. The callback is invoked to evaluate the module. Due to // the nature of synthetic modules, the callback is expected to perform all necessary evaluation // synchronously and return a boolean indicating whether the evaluation succeeded or not. The // evaluation cannot be async because V8 does not wait for the synthetic module evaluation // promises to resolve before it considers the module to be evaluated. The most it will do is // track errors thrown synchronously from the callback to determine whether evaluation failed. static kj::Own newSynthetic(Url id, Type type, EvaluateCallback callback, kj::Array namedExports = nullptr, Flags flags = Flags::NONE, ContentType contentType = ContentType::NONE); // Creates a new ESM module that takes ownership of the given code array. // This is generally used to construct ESM modules from a worker bundle. static kj::Own newEsm( Url id, Type type, kj::Array code, Flags flags = Flags::NONE); // Creates a new ESM module that does not take ownership of the given code // array. This is used to construct ESM modules from compiled-in built-in // modules. // This variation of newEsm does not take Flags as none of the existing // Flags are relevant other than the ESM flag which will be set automatically. static kj::Own newEsm(Url id, Type type, kj::ArrayPtr code); // The following methods are used to create the evaluation callbacks for various // kinds of common simple synthetic module types. The module registry is not // limited to just these kinds of modules, however. These are just the most // common. static EvaluateCallback newTextModuleHandler(kj::ArrayPtr data) KJ_WARN_UNUSED_RESULT; static EvaluateCallback newDataModuleHandler( kj::ArrayPtr data) KJ_WARN_UNUSED_RESULT; static EvaluateCallback newJsonModuleHandler(kj::ArrayPtr data) KJ_WARN_UNUSED_RESULT; static EvaluateCallback newWasmModuleHandler( kj::ArrayPtr data) KJ_WARN_UNUSED_RESULT; // An eval function is used for CommonJS style modules (including Node.js compat // modules. The expectation is that this method will be called when the CommonJS // style module is evaluated (e.g. within the EvaluationCallback). static Function compileEvalFunction(Lock& js, kj::StringPtr code, kj::StringPtr name, kj::Maybe compileExtensions, const CompilationObserver& observer) KJ_WARN_UNUSED_RESULT; // A CjsStyleModuleHandler is used for CommonJS style modules (including // The template type T must be a jsg::Object that implements a getExports(Lock&) // method returning a JsValue. This is set as the default export of the // synthetic module. All methods and properties exposed by the template // type T are exposed as additional globals within the executed scope. template static EvaluateCallback newCjsStyleModuleHandler( kj::StringPtr source, kj::StringPtr name) KJ_WARN_UNUSED_RESULT { return [source, name](Lock& js, const Url& id, const Module::ModuleNamespace& ns, const CompilationObserver& observer) mutable -> bool { return js.tryCatch([&] { auto& wrapper = TypeWrapper::from(js.v8Isolate); auto ext = js.alloc(js, id); ns.setDefault(js, ext->getExports(js)); auto fn = Module::compileEvalFunction(js, source, name, JsObject(wrapper.wrap(js, js.v8Context(), kj::none, ext.addRef())), observer); fn(js); // If there are named exports specified for the module namespace, // then we want to examine the ext->getExports() to extract those. JsValue exports = ext->getModuleExports(js); KJ_IF_SOME(obj, exports.template tryCast()) { for (auto& name: ns.getNamedExports()) { ns.set(js, name, obj.get(js, name)); } } return ns.setDefault(js, exports); }, [&](Value exception) { js.v8Isolate->ThrowException(exception.getHandle(js)); return false; }); }; } // A ModuleHandler used to create a synthetic module that is backed by a jsg::Object. template static EvaluateCallback newJsgObjectModuleHandler(Func factory) KJ_WARN_UNUSED_RESULT { return [factory = kj::mv(factory)](Lock& js, const Url& id, const Module::ModuleNamespace& ns, const CompilationObserver& observer) mutable -> bool { Ref instance = factory(js); auto value = TypeWrapper::from(js.v8Isolate).wrap(js, js.v8Context(), kj::none, kj::mv(instance)); return ns.setDefault(js, JsValue(value)); }; } protected: Module(Url id, Type type, Flags flags = Flags::NONE, ContentType contentType = ContentType::NONE); private: const Url id_; Type type_; Flags flags_; ContentType contentType_; // TODO: Support source objects as optional instantiation-hook creations and move // Wasm compilation to start at instantiation-time instead of evaluation-time. // kj::Maybe> sourceObject_; }; constexpr Module::Flags operator&(const Module::Flags& a, const Module::Flags& b) { return static_cast(static_cast(a) & static_cast(b)); } constexpr Module::Flags operator|(const Module::Flags& a, const Module::Flags& b) { return static_cast(static_cast(a) | static_cast(b)); } // A ModuleBundle is a source of modules that can be imported or required. // A ModuleRegistry is a collection of ModuleBundles. // Importantly, a ModuleBundle is immutable once created with exception to // any internal caching it may use to optimize resolution. Accesses to the // bundle must be thread-safe. class ModuleBundle { public: using Type = Module::Type; // A Builder is used to construct a ModuleBundle. class Builder { public: KJ_DISALLOW_COPY_AND_MOVE(Builder); // The resolve callback is used to perform resolution of a module context. // If the callback returns a string, then resolution will start over with // the new specifier. If the callback returns a Module, then that module // will be used as the resolved module. If the callback returns kj::none, // then the module is not resolved. using ResolveCallback = kj::Function>>(const ResolveContext&)>; Builder& add(const Url& id, ResolveCallback callback) KJ_LIFETIMEBOUND; Builder& alias(const Url& alias, const Url& id) KJ_LIFETIMEBOUND; kj::Own finish() KJ_WARN_UNUSED_RESULT; inline Type type() const { return type_; } protected: Builder(Type type); void ensureIsNotBundleSpecifier(const Url& id); Type type_; kj::HashMap modules_; kj::HashMap aliases_; }; // Used to build a ModuleBundle representing modules sourced from a worker bundle. class BundleBuilder final: public Builder { public: BundleBuilder(const jsg::Url& bundleBase); KJ_DISALLOW_COPY_AND_MOVE(BundleBuilder); using EvaluateCallback = Module::EvaluateCallback; BundleBuilder& addSyntheticModule(kj::StringPtr name, EvaluateCallback callback, kj::Array namedExports = nullptr, Module::ContentType contentType = Module::ContentType::NONE) KJ_LIFETIMEBOUND; BundleBuilder& addEsmModule(kj::StringPtr name, kj::ArrayPtr code, Module::Flags flags = Module::Flags::ESM) KJ_LIFETIMEBOUND; // Overload that takes ownership of the source data. Use this when the // source buffer may not outlive the module registry (e.g. transpiled // TypeScript where the backing rust::String has shorter lifetime). BundleBuilder& addEsmModule(kj::StringPtr name, kj::Array code, Module::Flags flags = Module::Flags::ESM) KJ_LIFETIMEBOUND; BundleBuilder& addWasmModule( kj::StringPtr name, kj::ArrayPtr data) KJ_LIFETIMEBOUND; BundleBuilder& alias(kj::StringPtr alias, kj::StringPtr name) KJ_LIFETIMEBOUND; private: const jsg::Url& bundleBase; }; // Used to build a ModuleBundle representing modules sources from the runtime. class BuiltinBuilder final: public Builder { public: enum class Type { BUILTIN, BUILTIN_ONLY, }; BuiltinBuilder(Type type = Type::BUILTIN); KJ_DISALLOW_COPY_AND_MOVE(BuiltinBuilder); BuiltinBuilder& addSynthetic( const Url& id, BundleBuilder::EvaluateCallback callback) KJ_LIFETIMEBOUND; BuiltinBuilder& addEsm(const Url& id, kj::ArrayPtr source) KJ_LIFETIMEBOUND; // Adds a module that is implemented in C++ as a jsg::Object template BuiltinBuilder& addObject(const Url& id) KJ_LIFETIMEBOUND { ensureIsNotBundleSpecifier(id); add(id, [id = id.clone(), type = type()](const ResolveContext& context) mutable -> kj::Maybe>> { if (context.normalizedSpecifier != id) return kj::none; kj::Own mod = Module::newSynthetic(kj::mv(id), type, [](Lock& js, const Url& id, const Module::ModuleNamespace& ns, const CompilationObserver&) { auto value = TypeWrapper::from(js.v8Isolate) .wrap(js, js.v8Context(), kj::none, js.alloc(js, id)); ns.setDefault(js, JsValue(value)); return true; }); return kj::Maybe>>(kj::mv(mod)); }); return *this; } }; static kj::Own newFallbackBundle( Builder::ResolveCallback callback) KJ_WARN_UNUSED_RESULT; static void getBuiltInBundleFromCapnp(BuiltinBuilder& builder, Bundle::Reader bundle); // Overload that accepts a per-module filter predicate. Only modules for which // the filter returns true are added to the builder. This is used for per-module // feature flag gating (e.g., individual node:* modules behind compat flags). static void getBuiltInBundleFromCapnp(BuiltinBuilder& builder, Bundle::Reader bundle, kj::Function filter); KJ_DISALLOW_COPY_AND_MOVE(ModuleBundle); inline Type type() const { return type_; } virtual ~ModuleBundle() noexcept(false) = default; struct Resolved { // This struct exists only to work around the limitation that a kj::OneOf // cannot be used with a const reference or we get a compiler error. kj::Maybe module; kj::Maybe specifier; }; // Load a module context. If a string is returned, then it must be a // module specifier. The resolution will start over with the new specifier. // If a Module is returned, then that is the loaded module. If kj::none // is returned, then the module is not known by this module. virtual kj::Maybe lookup( const ResolveContext& context) KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT = 0; protected: ModuleBundle(Type type); private: Type type_; }; // A ModuleRegistry is a collection of zero or more ModuleBundles. // Importantly, the ModuleRegistry is immutable once created and // must be thread-safe. In workerd, the module registry is created // and owned by a single Worker instance. In production, however, a // single ModuleRegistry instance may be shared by multiple replicas // of a Worker and therefore must be AtomicRefcounted. // When passed to tryResolveModuleNamespace, controls whether non-ESM // (synthetic) modules return the default export instead of the full // module namespace. Matches Node.js require() semantics. WD_STRONG_BOOL(UnwrapDefault); class ModuleRegistry final: public kj::AtomicRefcounted, public ModuleRegistryBase { private: enum BundleIndices { kBundle, kBuiltin, kBuiltinOnly, kFallback, kBundleCount }; public: // The EvalCallback is used to to ensure evaluation of a module outside of an // IoContext, when necessary. If the EvalCallback is not set, then the // Flag::EVAL on a module is ignored. If the EvalCallback is set, then any // Modules that have the Flag::EVAL set will have their evaluation deferred // to this callback. using EvalCallback = Function v8Module, const CompilationObserver& observer)>; class Builder final { public: enum class Options { NONE = 0, // When set, allows the ModuleRegistry to use a fallback ModuleBundle to // dynamically resolve a module that cannot be resolved by any other // registered bundles. The fallback service is only used when using the // ResolveContext::Type::BUNDLE context and is always the last bundle // checked. The fallback service should only be used for local dev. ALLOW_FALLBACK = 1 << 0, }; Builder(const ResolveObserver& observer, const jsg::Url& bundleBase, Options options = Options::NONE); KJ_DISALLOW_COPY_AND_MOVE(Builder); Builder& add(kj::Own bundle) KJ_LIFETIMEBOUND; kj::Arc finish() KJ_WARN_UNUSED_RESULT; Builder& setEvalCallback(EvalCallback callback) KJ_LIFETIMEBOUND; capnp::SchemaLoader& getSchemaLoader() { return *schemaLoader; } private: bool allowsFallback() const; // One slot for each of ModuleBundle::Type const ResolveObserver& observer; const jsg::Url& bundleBase; const Options options; kj::FixedArray>, ModuleRegistry::kBundleCount> bundles_; kj::Maybe maybeEvalCallback = kj::none; kj::Own schemaLoader; friend class ModuleRegistry; }; kj::Maybe lookup( const ResolveContext& context) const KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT; // Attaches the ModuleRegistry to the given isolate by creating an IsolateModuleRegistry // and linking that to the isolate. kj::Own attachToIsolate(Lock& js, const CompilationObserver& observer) const override; // Synchronously resolve the specified module from the registry bound to the given lock. // This will throw a JsExceptionThrown exception if the module cannot be found or an // error occurs while the module is being evaluated. Modules resolved with this method // must be capable of fully evaluating within one drain of the microtask queue. static JsValue resolve(Lock& js, kj::StringPtr specifier, kj::StringPtr exportName = "default"_kjc, ResolveContext::Type type = ResolveContext::Type::BUNDLE, ResolveContext::Source source = ResolveContext::Source::INTERNAL, kj::Maybe maybeReferrer = kj::none); // Synchronously resolve the specified module from the registry bound to the given lock. // This variant will return kj::none if the module cannot be found but will throw a // JsExceptionThrown exception if an error occurs while the module is being evaluated. // Modules resolved with this method must be capable of fully evaluating within one // drain of the microtask queue. static kj::Maybe tryResolveModuleNamespace(Lock& js, kj::StringPtr specifier, ResolveContext::Type type = ResolveContext::Type::BUNDLE, ResolveContext::Source source = ResolveContext::Source::INTERNAL, kj::Maybe maybeReferrer = kj::none, UnwrapDefault unwrapDefault = UnwrapDefault::NO); // The constructor is public because kj::heap requires is to be. Do not // use the constructor directly. Use the ModuleRegistry::Builder ModuleRegistry(ModuleRegistry::Builder* builder); KJ_DISALLOW_COPY_AND_MOVE(ModuleRegistry); const jsg::Url& getBundleBase() const { return bundleBase; } const capnp::SchemaLoader& getSchemaLoader() const override { return *schemaLoader; } const Module::Evaluator getEvaluator() const { return Module::Evaluator(*this); } private: struct Impl { // One slot for each of ModuleBundle::Type, within each slot is // an array of bundles of that type in registration order. kj::FixedArray>, kBundleCount> bundles; Impl(kj::ArrayPtr>> bundles); }; const ResolveObserver& observer; const jsg::Url& bundleBase; kj::MutexGuarded impl; // Marked mutable because kj::Function::operator() is non-const, but the eval // callback is conceptually const — it is only ever invoked while holding the // isolate lock, so concurrent mutation is not a concern. mutable kj::Maybe maybeEvalCallback = kj::none; kj::Own schemaLoader; struct ModuleRef { const Module& module; }; using ModuleOrRedirect = kj::OneOf; kj::Maybe lookupImpl(Impl& impl, const ResolveContext& context, bool recursed) const KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT; kj::Maybe tryFindInBundleGroup(const ResolveContext& context, kj::ArrayPtr> bundles) const KJ_LIFETIMEBOUND KJ_WARN_UNUSED_RESULT; // Attempts to find the module in the given bundle. If found, returns // const Module&, if an alias is found, returns the new ResolveContext // to try again with. If not found, returns kj::none. static kj::Maybe tryFindInBundle(const ResolveContext& context, ModuleBundle& bundle, const Url& bundleBase) KJ_WARN_UNUSED_RESULT; kj::Maybe evaluateImpl(jsg::Lock& js, const Module& module, v8::Local v8Module, const CompilationObserver& observer) const; friend class Module::Evaluator; }; constexpr ModuleRegistry::Builder::Options operator|( const ModuleRegistry::Builder::Options& a, const ModuleRegistry::Builder::Options& b) { return static_cast( static_cast(a) | static_cast(b)); } constexpr ModuleRegistry::Builder::Options operator&( const ModuleRegistry::Builder::Options& a, const ModuleRegistry::Builder::Options& b) { return static_cast( static_cast(a) & static_cast(b)); } } // namespace workerd::jsg::modules