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