File
Blob: src/workerd/jsg/modules-new.h
| 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 | |
| 25 | namespace 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. |
| 193 | struct 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 | |
| 218 | class 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. |
| 228 | class 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 | |
| 487 | constexpr 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 | } |
| 490 | constexpr 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. |
| 499 | class 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. |
| 657 | WD_STRONG_BOOL(UnwrapDefault); |
| 658 | |
| 659 | class 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 | |
| 802 | constexpr 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 | } |
| 807 | constexpr 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 |