// Copyright (c) 2017-2022 Cloudflare, Inc. // Licensed under the Apache 2.0 license found in the LICENSE file or at: // https://opensource.org/licenses/Apache-2.0 #include "observer.h" #include "type-wrapper.h" #include "url.h" #include #include #include #include #include #include #include #include namespace workerd::jsg::test { namespace { using workerd::jsg::modules::Module; using workerd::jsg::modules::ModuleBundle; using workerd::jsg::modules::ModuleRegistry; using workerd::jsg::modules::ResolveContext; V8System v8System; const jsg::Url BASE = "file:///"_url; struct ResolveObserverImpl: public ResolveObserver { struct Request { Url id; ResolveObserver::Context context; ResolveObserver::Source source; bool found = false; }; mutable kj::Vector modules; struct MyResolveStatus: public ResolveObserver::ResolveStatus { Request& request; MyResolveStatus(Request& request): request(request) {} void found() override { request.found = true; } void notFound() override { request.found = false; } }; kj::Own onResolveModule( const Url& id, Context context, Source source) const override { modules.add(Request{ .id = id.clone(), .context = context, .source = source, }); return kj::heap(modules.back()); } }; struct TestType: public jsg::Object { bool barCalled = false; kj::Maybe> exports; TestType(Lock&, const jsg::Url&) {} void bar() { barCalled = true; } JsObject getExports(Lock& js) { KJ_IF_SOME(exp, exports) { return exp.getHandle(js); } return exports.emplace(JsRef(js, js.obj())).getHandle(js); } void setExports(Lock& js, JsObject obj) { exports = JsRef(js, obj); } JsValue require(Lock& js, kj::String specifier) { return js.tryCatch([&] { return ModuleRegistry::resolve(js, specifier); }, [&](Value exception) -> JsValue { js.throwException(kj::mv(exception)); }); } jsg::JsValue getModuleExports(jsg::Lock& js) { return getExports(js); } JSG_RESOURCE_TYPE(TestType) { JSG_METHOD(bar); JSG_METHOD(require); JSG_PROTOTYPE_PROPERTY(exports, getExports, setExports); } }; struct TestTypeWrapper { static TestTypeWrapper& from(v8::Isolate*) { KJ_UNIMPLEMENTED("not implemented"); } v8::Local wrap(jsg::Lock& lock, v8::Local, kj::Maybe>, jsg::Ref) { KJ_UNIMPLEMENTED("not implemented"); } }; struct TestContext: public Object, public ContextGlobal { JSG_RESOURCE_TYPE(TestContext) {} }; JSG_DECLARE_ISOLATE_TYPE(TestIsolate, TestContext, TestType); #define PREAMBLE(fn) \ TestIsolate isolate(v8System, v8::IsolateGroup::GetDefault(), 123, kj::heap()); \ isolate.runInLockScope([&](auto& lock) { \ IsolateBase::from(lock.v8Isolate).setUsingNewModuleRegistry(); \ JSG_WITHIN_CONTEXT_SCOPE(lock, lock.template newContext().getHandle(lock), \ [&](jsg::Lock& js) { fn(lock); }); \ }); // ====================================================================================== KJ_TEST("An empty registry") { // We should be able to create an empty registry that returns nothing. // Basic resolution of this kind does not require an isolate lock. ResolveObserverImpl observer; ModuleRegistry::Builder registryBuilder(observer, BASE); auto registry = registryBuilder.finish(); KJ_ASSERT(registry.get() != nullptr); ResolveContext context = { .type = ResolveContext::Type::BUNDLE, .source = ResolveContext::Source::INTERNAL, .normalizedSpecifier = "file:///foo"_url, .referrerNormalizedSpecifier = BASE, }; KJ_ASSERT(registry->lookup(context) == kj::none); KJ_ASSERT(observer.modules.size() == 1); KJ_ASSERT(observer.modules[0].found == false); } // ====================================================================================== KJ_TEST("A empty fallback bundle") { // We should be able to create an empty fallback bundle that returns nothing. // Basic resolution of this kind does not require an isolate lock. bool called = false; auto fallback = ModuleBundle::newFallbackBundle([&called](const ResolveContext& context) { called = true; return kj::none; }); ResolveContext context = { .type = ResolveContext::Type::BUNDLE, .source = ResolveContext::Source::INTERNAL, .normalizedSpecifier = "file:///foo"_url, .referrerNormalizedSpecifier = BASE, }; KJ_ASSERT(fallback->lookup(context) == kj::none); KJ_ASSERT(called); } // ====================================================================================== KJ_TEST("An empty user bundle") { // We should be able to create an empty user bundle that returns nothing. // Basic resolution of this kind does not require an isolate lock. ModuleBundle::BundleBuilder builder(BASE); auto bundle = builder.finish(); ResolveContext context = { .type = ResolveContext::Type::BUNDLE, .source = ResolveContext::Source::INTERNAL, .normalizedSpecifier = "file:///foo"_url, .referrerNormalizedSpecifier = BASE, }; KJ_ASSERT(bundle->lookup(context) == kj::none); } // ====================================================================================== KJ_TEST("An empty built-in bundle") { // We should be able to create an empty built-in bundle that returns nothing. // Basic resolution of this kind does not require an isolate lock. ModuleBundle::BuiltinBuilder builder; auto bundle = builder.finish(); ResolveContext context = { .type = ResolveContext::Type::BUNDLE, .source = ResolveContext::Source::INTERNAL, .normalizedSpecifier = "file:///foo"_url, .referrerNormalizedSpecifier = BASE, }; KJ_ASSERT(bundle->lookup(context) == kj::none); } // ====================================================================================== KJ_TEST("A registry with empty bundles") { // We should be able to create a registry with empty bundles that return nothing. // Basic resolution of this kind does not require an isolate lock. ResolveObserverImpl observer; ModuleRegistry::Builder registryBuilder( observer, BASE, ModuleRegistry::Builder::Options::ALLOW_FALLBACK); registryBuilder.add( ModuleBundle::newFallbackBundle([](const ResolveContext& context) { return kj::none; })); ModuleBundle::BundleBuilder bundleBuilder(BASE); registryBuilder.add(bundleBuilder.finish()); ModuleBundle::BuiltinBuilder builtinBuilder; registryBuilder.add(builtinBuilder.finish()); auto registry = registryBuilder.finish(); ResolveContext context = { .type = ResolveContext::Type::BUNDLE, .source = ResolveContext::Source::INTERNAL, .normalizedSpecifier = "file:///foo"_url, .referrerNormalizedSpecifier = BASE, }; KJ_ASSERT(registry->lookup(context) == kj::none); KJ_ASSERT(observer.modules.size() == 1); KJ_ASSERT(observer.modules[0].found == false); } // ====================================================================================== KJ_TEST("A user bundle with a single ESM module") { ModuleBundle::BundleBuilder builder(BASE); auto source = kj::str("export const foo = 123;"); builder.addEsmModule("foo", source, Module::Flags::MAIN); auto bundle = builder.finish(); const auto id = "file:///foo"_url; ResolveContext context = { .type = ResolveContext::Type::BUNDLE, .source = ResolveContext::Source::INTERNAL, .normalizedSpecifier = id, .referrerNormalizedSpecifier = BASE, }; auto resolved = KJ_ASSERT_NONNULL(bundle->lookup(context)); auto& module = KJ_ASSERT_NONNULL(resolved.module); KJ_ASSERT(module.id() == id); KJ_ASSERT(module.isEsm()); KJ_ASSERT(module.isMain()); KJ_ASSERT(module.type() == Module::Type::BUNDLE); } // ====================================================================================== KJ_TEST("A user bundle with an ESM module and a Synthetic module") { ModuleBundle::BundleBuilder builder(BASE); auto source = kj::str("export const foo = 123;"); builder.addEsmModule("foo", source, Module::Flags::MAIN); builder.addSyntheticModule( "foo/bar", [](Lock&, const Url&, const Module::ModuleNamespace&, const CompilationObserver&) { return true; }); const auto foo = "file:///foo"_url; const auto bar = "file:///foo/bar"_url; auto bundle = builder.finish(); { ResolveContext context = { .type = ResolveContext::Type::BUNDLE, .source = ResolveContext::Source::INTERNAL, .normalizedSpecifier = foo, .referrerNormalizedSpecifier = BASE, }; auto resolved = KJ_ASSERT_NONNULL(bundle->lookup(context)); auto& module = KJ_ASSERT_NONNULL(resolved.module); KJ_ASSERT(module.id() == foo); KJ_ASSERT(module.isEsm()); KJ_ASSERT(module.isMain()); KJ_ASSERT(module.type() == Module::Type::BUNDLE); } { ResolveContext context = { .type = ResolveContext::Type::BUNDLE, .source = ResolveContext::Source::INTERNAL, .normalizedSpecifier = bar, .referrerNormalizedSpecifier = BASE, }; auto resolved = KJ_ASSERT_NONNULL(bundle->lookup(context)); auto& module = KJ_ASSERT_NONNULL(resolved.module); KJ_ASSERT(module.id() == bar); KJ_ASSERT(!module.isEsm()); KJ_ASSERT(!module.isMain()); KJ_ASSERT(module.type() == Module::Type::BUNDLE); } } // ====================================================================================== KJ_TEST("A built-in bundle with two modules") { ResolveObserverImpl observer; ModuleRegistry::Builder registryBuilder(observer, BASE); ModuleBundle::BuiltinBuilder builder; const auto foo = "foo:bar"_url; const auto bar = "bar:baz"_url; auto source = "export const foo = 123;"_kjc; builder.addEsm(foo, source.asArray()); struct W { static W& from(v8::Isolate*) { static W w; return w; } v8::Local wrap(jsg::Lock& lock, v8::Local, kj::Maybe>, jsg::Ref) { return v8::Local(); } }; builder.addObject(bar); auto registry = registryBuilder.add(builder.finish()).finish(); { ResolveContext context = { .type = ResolveContext::Type::BUNDLE, .source = ResolveContext::Source::INTERNAL, .normalizedSpecifier = foo, .referrerNormalizedSpecifier = foo, }; auto& module = KJ_ASSERT_NONNULL(registry->lookup(context)); KJ_ASSERT(module.id() == foo); KJ_ASSERT(module.isEsm()); KJ_ASSERT(!module.isMain()); KJ_ASSERT(module.type() == Module::Type::BUILTIN); } { ResolveContext context = { .type = ResolveContext::Type::BUNDLE, .source = ResolveContext::Source::INTERNAL, .normalizedSpecifier = bar, .referrerNormalizedSpecifier = bar, }; auto& module = KJ_ASSERT_NONNULL(registry->lookup(context)); KJ_ASSERT(module.id() == bar); KJ_ASSERT(!module.isEsm()); KJ_ASSERT(!module.isMain()); KJ_ASSERT(module.type() == Module::Type::BUILTIN); } KJ_ASSERT(observer.modules.size() == 2); KJ_ASSERT(observer.modules[0].id == foo); KJ_ASSERT(observer.modules[1].id == bar); } // ====================================================================================== KJ_TEST("Built-in and Built-in only bundles") { ResolveObserverImpl observer; ModuleRegistry::Builder registryBuilder(observer, BASE); ModuleBundle::BuiltinBuilder builtinBuilder; ModuleBundle::BuiltinBuilder builtinOnlyBuilder(ModuleBundle::BuiltinBuilder::Type::BUILTIN_ONLY); const auto foo = "foo:bar"_url; const auto bar = "bar:baz"_url; auto source = "export const foo = 123;"_kjc; builtinBuilder.addEsm(foo, source.asArray()); builtinOnlyBuilder.addObject(bar); auto registry = registryBuilder.add(builtinBuilder.finish()).add(builtinOnlyBuilder.finish()).finish(); { ResolveContext context = { .type = ResolveContext::Type::BUNDLE, .source = ResolveContext::Source::INTERNAL, .normalizedSpecifier = foo, .referrerNormalizedSpecifier = foo, }; auto& module = KJ_ASSERT_NONNULL(registry->lookup(context)); KJ_ASSERT(module.id() == foo); KJ_ASSERT(module.isEsm()); KJ_ASSERT(!module.isMain()); KJ_ASSERT(module.type() == Module::Type::BUILTIN); } { ResolveContext context = { .type = ResolveContext::Type::BUNDLE, .source = ResolveContext::Source::INTERNAL, .normalizedSpecifier = bar, .referrerNormalizedSpecifier = bar, }; // Built-in only modules cannot be resolved from a bundle context. KJ_ASSERT(registry->lookup(context) == kj::none); } { ResolveContext context = { .type = ResolveContext::Type::BUILTIN, .source = ResolveContext::Source::INTERNAL, .normalizedSpecifier = bar, .referrerNormalizedSpecifier = bar, }; auto& module = KJ_ASSERT_NONNULL(registry->lookup(context)); KJ_ASSERT(module.id() == bar); KJ_ASSERT(!module.isEsm()); KJ_ASSERT(!module.isMain()); KJ_ASSERT(module.type() == Module::Type::BUILTIN_ONLY); } { ResolveContext context = { .type = ResolveContext::Type::BUILTIN_ONLY, .source = ResolveContext::Source::INTERNAL, .normalizedSpecifier = bar, .referrerNormalizedSpecifier = bar, }; auto& module = KJ_ASSERT_NONNULL(registry->lookup(context)); KJ_ASSERT(module.id() == bar); KJ_ASSERT(!module.isEsm()); KJ_ASSERT(!module.isMain()); KJ_ASSERT(module.type() == Module::Type::BUILTIN_ONLY); } } // ====================================================================================== KJ_TEST("Built-in modules cannot use file:") { ModuleBundle::BuiltinBuilder builder; const auto foo = "file:///foo"_url; auto source = "export const foo = 123;"_kjc; try { builder.addEsm(foo, source.asArray()); KJ_FAIL_ASSERT("Expected an exception"); } catch (kj::Exception& exception) { KJ_ASSERT(exception.getDescription().endsWith( "The file: protocol is reserved for bundle type modules"_kjc)); } } // ====================================================================================== KJ_TEST("Fallback bundle that returns something") { auto fallback = ModuleBundle::newFallbackBundle([](const ResolveContext& context) { kj::Own mod = Module::newSynthetic("file:///foo"_url, Module::Type::FALLBACK, [](Lock&, const Url&, const Module::ModuleNamespace&, const CompilationObserver&) -> bool { KJ_FAIL_ASSERT("Should not be called"); }); return kj::Maybe>>(kj::mv(mod)); }); ResolveObserverImpl observer; ModuleRegistry::Builder registryBuilder( observer, BASE, ModuleRegistry::Builder::Options::ALLOW_FALLBACK); auto registry = registryBuilder.add(kj::mv(fallback)).finish(); const auto id = "file:///foo"_url; { ResolveContext context{ .type = ResolveContext::Type::BUNDLE, .source = ResolveContext::Source::INTERNAL, .normalizedSpecifier = id, .referrerNormalizedSpecifier = BASE, }; auto& module = KJ_ASSERT_NONNULL(registry->lookup(context)); KJ_ASSERT(module.id() == id); KJ_ASSERT(module.type() == Module::Type::FALLBACK); KJ_ASSERT(!module.isEsm()); } // Built-in and built-in only contexts do not use the fallback { ResolveContext context{ .type = ResolveContext::Type::BUILTIN, .source = ResolveContext::Source::INTERNAL, .normalizedSpecifier = id, .referrerNormalizedSpecifier = BASE, }; KJ_ASSERT(registry->lookup(context) == kj::none); } { ResolveContext context{ .type = ResolveContext::Type::BUILTIN_ONLY, .source = ResolveContext::Source::INTERNAL, .normalizedSpecifier = id, .referrerNormalizedSpecifier = BASE, }; KJ_ASSERT(registry->lookup(context) == kj::none); } } // ====================================================================================== KJ_TEST("Duplicate module names in a single are caught and throw properly") { ModuleBundle::BundleBuilder builder(BASE); builder.addSyntheticModule( "foo", [](Lock&, const Url&, const Module::ModuleNamespace&, const CompilationObserver&) { return true; }); try { builder.addSyntheticModule( "foo", [](Lock&, const Url&, const Module::ModuleNamespace&, const CompilationObserver&) { return true; }); KJ_FAIL_ASSERT("Expected an exception"); } catch (kj::Exception& exception) { KJ_ASSERT(exception.getDescription() == "Module \"file:///foo\" already added to bundle"_kjc); } } // ====================================================================================== KJ_TEST("Fallback bundles are not permitted in production") { ResolveObserverImpl observer; ModuleRegistry::Builder registryBuilder(observer, BASE); try { registryBuilder.add(ModuleBundle::newFallbackBundle([](const ResolveContext& context) { kj::Own mod = Module::newSynthetic(context.normalizedSpecifier.clone(), Module::Type::FALLBACK, [](Lock&, const Url&, const Module::ModuleNamespace&, const CompilationObserver&) -> bool { KJ_FAIL_ASSERT("Should not be called"); }); return kj::Maybe>>(kj::mv(mod)); })); KJ_FAIL_ASSERT("Expected an exception"); } catch (kj::Exception& exception) { KJ_ASSERT(exception.getDescription().endsWith( "Fallback bundle types are not allowed for this registry"_kjc)); } } // ====================================================================================== KJ_TEST("Compound Registry") { ResolveObserverImpl observer; ModuleRegistry::Builder registryBuilder( observer, BASE, ModuleRegistry::Builder::Options::ALLOW_FALLBACK); const auto foo = "foo:bar"_url; // Fallback const auto bar = "bar:baz"_url; // Built-in const auto baz = "abc:xyz"_url; // Built-in only const auto qux = "file:///qux"_url; // Bundle registryBuilder.add(ModuleBundle::newFallbackBundle( [&](const ResolveContext& context) -> kj::Maybe>> { if (context.normalizedSpecifier != foo) return kj::none; kj::Own mod = Module::newSynthetic(foo.clone(), Module::Type::FALLBACK, [](Lock&, const Url&, const Module::ModuleNamespace&, const CompilationObserver&) -> bool { KJ_FAIL_ASSERT("should not have been called"); }); return kj::Maybe>>(kj::mv(mod)); })); ModuleBundle::BuiltinBuilder builtinBuilder; auto barSource = "export const foo = 123;"_kjc; builtinBuilder.addEsm(bar, barSource.asArray()); registryBuilder.add(builtinBuilder.finish()); ModuleBundle::BuiltinBuilder builtinOnlyBuilder(ModuleBundle::BuiltinBuilder::Type::BUILTIN_ONLY); builtinOnlyBuilder.addObject(baz); registryBuilder.add(builtinOnlyBuilder.finish()); ModuleBundle::BundleBuilder bundleBuilder(BASE); auto quxSource = kj::str("export const foo = 123;"); bundleBuilder.addEsmModule("qux", quxSource, Module::Flags::MAIN); registryBuilder.add(bundleBuilder.finish()); auto registry = registryBuilder.finish(); constexpr auto resolve = [](const auto& registry, ResolveContext::Type type, const Url& id) { ResolveContext context{ .type = type, .source = ResolveContext::Source::INTERNAL, .normalizedSpecifier = id, .referrerNormalizedSpecifier = BASE, }; return registry->lookup(context); }; { // The fallback module is resolved when using a bundle context auto& module = KJ_ASSERT_NONNULL(resolve(registry, ResolveContext::Type::BUNDLE, foo)); KJ_ASSERT(module.id() == foo); KJ_ASSERT(module.type() == Module::Type::FALLBACK); KJ_ASSERT(!module.isEsm()); KJ_ASSERT(!module.isMain()); } { // A built-in module is resolved when using a bundle context auto& module = KJ_ASSERT_NONNULL(resolve(registry, ResolveContext::Type::BUNDLE, bar)); KJ_ASSERT(module.id() == bar); KJ_ASSERT(module.type() == Module::Type::BUILTIN); KJ_ASSERT(module.isEsm()); KJ_ASSERT(!module.isMain()); } { // A bundle module is resolved when using a bundle context auto& module = KJ_ASSERT_NONNULL(resolve(registry, ResolveContext::Type::BUNDLE, qux)); KJ_ASSERT(module.id() == qux); KJ_ASSERT(module.type() == Module::Type::BUNDLE); KJ_ASSERT(module.isEsm()); KJ_ASSERT(module.isMain()); } { // A built-in module is resolved when using a builtin context auto& module = KJ_ASSERT_NONNULL(resolve(registry, ResolveContext::Type::BUILTIN, bar)); KJ_ASSERT(module.id() == bar); KJ_ASSERT(module.type() == Module::Type::BUILTIN); KJ_ASSERT(module.isEsm()); KJ_ASSERT(!module.isMain()); } { // A built-in only module is resolved when using a built-in context auto& module = KJ_ASSERT_NONNULL(resolve(registry, ResolveContext::Type::BUILTIN, baz)); KJ_ASSERT(module.id() == baz); KJ_ASSERT(module.type() == Module::Type::BUILTIN_ONLY); KJ_ASSERT(!module.isEsm()); KJ_ASSERT(!module.isMain()); } { // A built-in only module is resolved when using a built-in only context auto& module = KJ_ASSERT_NONNULL(resolve(registry, ResolveContext::Type::BUILTIN_ONLY, baz)); KJ_ASSERT(module.id() == baz); KJ_ASSERT(module.type() == Module::Type::BUILTIN_ONLY); KJ_ASSERT(!module.isEsm()); KJ_ASSERT(!module.isMain()); } // A built-in only module cannot be resolved from a bundle context KJ_ASSERT(resolve(registry, ResolveContext::Type::BUNDLE, baz) == kj::none); // Fallback modules cannot be resolved from a built-in context KJ_ASSERT(resolve(registry, ResolveContext::Type::BUILTIN, foo) == kj::none); KJ_ASSERT(resolve(registry, ResolveContext::Type::BUILTIN_ONLY, foo) == kj::none); // Bundle modules cannot be resolved from a built-in or built-in only context KJ_ASSERT(resolve(registry, ResolveContext::Type::BUILTIN, qux) == kj::none); KJ_ASSERT(resolve(registry, ResolveContext::Type::BUILTIN_ONLY, qux) == kj::none); // We should have seen eleven distinct resolution events. KJ_ASSERT(observer.modules.size() == 11); } // ====================================================================================== KJ_TEST("Bundle shadows built-in") { // A bundle module can shadow a built-in ResolveObserverImpl observer; ModuleRegistry::Builder registryBuilder(observer, BASE); const auto foo = "foo:bar"_url; ModuleBundle::BuiltinBuilder builtinBuilder; auto source = "export const foo = 123;"_kjc; builtinBuilder.addEsm(foo, source.asArray()); registryBuilder.add(builtinBuilder.finish()); ModuleBundle::BundleBuilder bundleBuilder(BASE); auto bundleSource = kj::str("export const foo = 456;"); bundleBuilder.addEsmModule("foo:bar", bundleSource, Module::Flags::MAIN); registryBuilder.add(bundleBuilder.finish()); auto registry = registryBuilder.finish(); ResolveContext context{ .type = ResolveContext::Type::BUNDLE, .source = ResolveContext::Source::INTERNAL, .normalizedSpecifier = foo, .referrerNormalizedSpecifier = BASE, }; auto& module = KJ_ASSERT_NONNULL(registry->lookup(context)); KJ_ASSERT(module.id() == foo); KJ_ASSERT(module.type() == Module::Type::BUNDLE); KJ_ASSERT(module.isEsm()); KJ_ASSERT(module.isMain()); } // ====================================================================================== KJ_TEST("Attaching a module registry works") { PREAMBLE(([&](Lock& js) { ResolveObserver resolveObserver; CompilationObserver compilationObserver; ModuleRegistry::Builder registryBuilder(resolveObserver, BASE); ModuleBundle::BundleBuilder bundleBuilder(BASE); auto source = kj::str("export default 123; export const m = 'abc';"); // Done this way to avoid including the nullptr at the end... bundleBuilder.addEsmModule("main", source); auto mainSource = kj::str("import foo from 'main'; export default foo;"); bundleBuilder.addEsmModule("worker1", mainSource.first(mainSource.size()), Module::Flags::MAIN); registryBuilder.add(bundleBuilder.finish()); auto registry = registryBuilder.finish(); const auto id = "file:///main"_url; ResolveContext resolveContext{ .type = ResolveContext::Type::BUNDLE, .source = ResolveContext::Source::INTERNAL, .normalizedSpecifier = id, .referrerNormalizedSpecifier = BASE, }; KJ_ASSERT(registry->lookup(resolveContext) != kj::none); auto attached = registry->attachToIsolate(js, compilationObserver); js.tryCatch([&] { auto val = ModuleRegistry::resolve(js, "file:///worker1"); KJ_ASSERT(val.isNumber()); }, [&](Value exception) { js.throwException(kj::mv(exception)); }); js.tryCatch([&] { auto val = ModuleRegistry::resolve(js, "worker1"); KJ_ASSERT(val.isNumber()); }, [&](Value exception) { js.throwException(kj::mv(exception)); }); js.tryCatch([&] { auto val = ModuleRegistry::resolve(js, "./.././../worker1"); KJ_ASSERT(val.isNumber()); }, [&](Value exception) { js.throwException(kj::mv(exception)); }); js.tryCatch([&] { auto val = ModuleRegistry::resolve(js, "file:///main", "m"_kjc); KJ_ASSERT(val.isString()); }, [&](Value exception) { js.throwException(kj::mv(exception)); }); })); } // ====================================================================================== KJ_TEST("Basic types of modules work (text, data, json, wasm)") { PREAMBLE(([&](Lock& js) { ResolveObserver resolveObserver; CompilationObserver compilationObserver; ModuleRegistry::Builder registryBuilder(resolveObserver, BASE); ModuleBundle::BundleBuilder bundleBuilder(BASE); auto abcSource = kj::str("hello"); auto xyzData = kj::heapArray({1, 2, 3}); bundleBuilder.addSyntheticModule("abc", Module::newTextModuleHandler(abcSource)); bundleBuilder.addSyntheticModule("xyz", Module::newDataModuleHandler(xyzData)); auto json = kj::str("{\"foo\":123}"); bundleBuilder.addSyntheticModule("json", Module::newJsonModuleHandler(json.first(json.size()))); auto wasm = kj::heapArray({ 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x07, 0x01, 0x60, 0x02, 0x7f, 0x7f, 0x01, 0x7f, 0x03, 0x02, 0x01, 0x00, 0x07, 0x07, 0x01, 0x03, 0x61, 0x64, 0x64, 0x00, 0x00, 0x0a, 0x09, 0x01, 0x07, 0x00, 0x20, 0x00, 0x20, 0x01, 0x6a, 0x0b, }); bundleBuilder.addSyntheticModule("wasm", Module::newWasmModuleHandler(wasm)); auto mainSource2 = kj::str("export { default as abc } from 'abc';" "export { default as xyz } from 'xyz';" "export { default as json } from 'json';" "export { default as wasm } from 'wasm';" "export { default as wasm2 } from 'wasm?a';"); bundleBuilder.addEsmModule("worker", mainSource2, Module::Flags::MAIN); registryBuilder.add(bundleBuilder.finish()); auto registry = registryBuilder.finish(); const auto id = "file:///worker"_url; ResolveContext resolveContext{ .type = ResolveContext::Type::BUNDLE, .source = ResolveContext::Source::INTERNAL, .normalizedSpecifier = id, .referrerNormalizedSpecifier = BASE, }; auto& resolved KJ_UNUSED = KJ_ASSERT_NONNULL(registry->lookup(resolveContext)); auto attached = registry->attachToIsolate(js, compilationObserver); js.tryCatch([&] { auto val = ModuleRegistry::resolve(js, "file:///worker", "abc"_kjc); KJ_ASSERT(val.isString()); KJ_ASSERT(kj::str(val) == "hello"_kjc); }, [&](Value exception) { js.throwException(kj::mv(exception)); }); js.tryCatch([&] { auto val = ModuleRegistry::resolve(js, "file:///worker", "xyz"_kjc); KJ_ASSERT(val.isArrayBuffer()); }, [&](Value exception) { js.throwException(kj::mv(exception)); }); js.tryCatch([&] { auto val1 = ModuleRegistry::resolve(js, "file:///worker", "json"_kjc); auto val2 = ModuleRegistry::resolve(js, "file:///json", "default"_kjc); KJ_ASSERT(val1.isObject()); KJ_ASSERT(val2.isObject()); KJ_ASSERT(val1.strictEquals(val2)); auto obj = KJ_ASSERT_NONNULL(val1.tryCast()); KJ_ASSERT(obj.get(js, "foo").isNumber()); }, [&](Value exception) { js.throwException(kj::mv(exception)); }); js.tryCatch([&] { auto wasm1 = ModuleRegistry::resolve(js, "file:///worker", "wasm"_kjc); auto wasm2 = ModuleRegistry::resolve(js, "file:///wasm", "default"_kjc); auto wasm3 = ModuleRegistry::resolve(js, "file:///worker", "wasm2"_kjc); KJ_ASSERT(wasm1.isWasmModuleObject()); KJ_ASSERT(wasm2.isWasmModuleObject()); KJ_ASSERT(wasm3.isWasmModuleObject()); KJ_ASSERT(wasm1.strictEquals(wasm2)); KJ_ASSERT(!wasm1.strictEquals(wasm3)); }, [&](Value exception) { js.throwException(kj::mv(exception)); }); })); } // ====================================================================================== KJ_TEST("compileEvalFunction in synthetic module works") { PREAMBLE([&](Lock& js) { CompilationObserver compilationObserver; ResolveObserver resolveObserver; ModuleBundle::BundleBuilder bundleBuilder(BASE); bundleBuilder.addSyntheticModule("abc", [](Lock& js, const Url& id, const Module::ModuleNamespace& ns, const CompilationObserver& observer) mutable -> bool { // The compileEvalFunction is used in CommonJs/Node.js compat modules to // evaluate the module as a function rather than as an ESM. This test just // verifies that compileEvalFunction works as expected. auto ext = js.alloc(js, id); auto& wrapper = TestIsolate_TypeWrapper::from(js.v8Isolate); auto fn = Module::compileEvalFunction(js, "bar(123);"_kj, "foo"_kj, JsObject(wrapper.wrap(js, js.v8Context(), kj::none, ext.addRef())), observer); return js.tryCatch([&] { fn(js); KJ_ASSERT(ext->barCalled); return ns.setDefault(js, js.num(123)); }, [&](Value exception) { js.v8Isolate->ThrowException(exception.getHandle(js)); return false; }); }); auto source = kj::str("import 'abc'"); bundleBuilder.addEsmModule("main", source, Module::Flags::MAIN); auto registry = ModuleRegistry::Builder(resolveObserver, BASE).add(bundleBuilder.finish()).finish(); auto attached = registry->attachToIsolate(js, compilationObserver); js.tryCatch([&] { auto val = ModuleRegistry::resolve(js, "file:///main"); KJ_ASSERT(val.isUndefined()); }, [&](Value exception) { js.throwException(kj::mv(exception)); }); }); } // ====================================================================================== KJ_TEST("import.meta works as expected") { PREAMBLE([&](Lock& js) { ResolveObserver ResolveObserver; CompilationObserver compilationObserver; ModuleBundle::BundleBuilder bundleBuilder(BASE); auto foo = kj::str("export default import.meta"); bundleBuilder.addEsmModule("foo", foo); auto bar = kj::str("export default import.meta"); bundleBuilder.addEsmModule("foo/././././bar", bar, Module::Flags::MAIN); auto registry = ModuleRegistry::Builder(ResolveObserver, BASE).add(bundleBuilder.finish()).finish(); auto attached = registry->attachToIsolate(js, compilationObserver); js.tryCatch([&] { auto val = ModuleRegistry::resolve(js, "file:///foo"); KJ_ASSERT(val.isObject()); auto obj = KJ_ASSERT_NONNULL(val.tryCast()); JsValue url = obj.get(js, "url"); JsValue main = obj.get(js, "main"); JsValue res = obj.get(js, "resolve"); KJ_ASSERT(url.isString()); KJ_ASSERT(main.isBoolean()); KJ_ASSERT(res.isFunction()); KJ_ASSERT(url.toString(js) == "file:///foo"_kj); auto mainVal = KJ_ASSERT_NONNULL(main.tryCast()); KJ_ASSERT(!mainVal.value(js)); auto& wrapper = TestIsolate_TypeWrapper::from(js.v8Isolate); KJ_IF_SOME(fn, wrapper.tryUnwrap( js, js.v8Context(), res, (Function*)nullptr, kj::none)) { KJ_ASSERT(fn(js, kj::str("foo/bar")) == "file:///foo/bar"_kj); } else { } }, [&](Value exception) { js.throwException(kj::mv(exception)); }); js.tryCatch([&] { auto val = ModuleRegistry::resolve(js, "file:///foo/bar"); KJ_ASSERT(val.isObject()); auto obj = KJ_ASSERT_NONNULL(val.tryCast()); JsValue url = obj.get(js, "url"); JsValue main = obj.get(js, "main"); JsValue res = obj.get(js, "resolve"); KJ_ASSERT(url.isString()); KJ_ASSERT(main.isBoolean()); KJ_ASSERT(res.isFunction()); KJ_ASSERT(url.toString(js) == "file:///foo/bar"_kj); auto mainVal = KJ_ASSERT_NONNULL(main.tryCast()); KJ_ASSERT(mainVal.value(js)); }, [&](Value exception) { js.throwException(kj::mv(exception)); }); }); } // ====================================================================================== KJ_TEST("import specifiers with query params and hash fragments work") { // If we have two imports with the same base specifier URL // but different query params or hash fragments, they should // resolve to the same underlying Module but get evaluated // separately. This means the EvaluationCallback can be called // multiple times. PREAMBLE([&](jsg::Lock& js) { ResolveObserver ResolveObserver; CompilationObserver compilationObserver; ModuleBundle::BundleBuilder bundleBuilder(BASE); auto foo = kj::str("export default import.meta"); bundleBuilder.addEsmModule("foo", foo); auto registry = ModuleRegistry::Builder(ResolveObserver, BASE).add(bundleBuilder.finish()).finish(); auto attached = registry->attachToIsolate(js, compilationObserver); js.tryCatch([&] { auto val1 = ModuleRegistry::resolve(js, "file:///foo?1"); auto val2 = ModuleRegistry::resolve(js, "file:///foo?2"); auto val3 = ModuleRegistry::resolve(js, "file:///foo#1"); auto val4 = ModuleRegistry::resolve(js, "file:///foo#2"); KJ_ASSERT(val1.isObject()); KJ_ASSERT(val2.isObject()); KJ_ASSERT(val3.isObject()); KJ_ASSERT(val4.isObject()); KJ_ASSERT(!val1.strictEquals(val2)); KJ_ASSERT(!val2.strictEquals(val3)); KJ_ASSERT(!val3.strictEquals(val4)); KJ_ASSERT(!val4.strictEquals(val1)); auto obj = KJ_ASSERT_NONNULL(val1.tryCast()); auto url = obj.get(js, "url"); KJ_ASSERT(url.isString()); // The import.meta.url should include the query param and hash fragment KJ_ASSERT(url.toString(js) == "file:///foo?1"_kj); }, [&](Value exception) { js.throwException(kj::mv(exception)); }); }); } // ====================================================================================== KJ_TEST("Previously resolved modules not found with incompatible resolve context") { // If we have a built-in only module that is resolved with a built-in context, that // should not be found when later resolving with a bundle context. PREAMBLE([&](Lock& js) { ResolveObserverImpl observer; CompilationObserver compilationObserver; ModuleBundle::BuiltinBuilder builtinBuilder(ModuleBundle::BuiltinBuilder::Type::BUILTIN_ONLY); const auto foo = "foo:bar"_url; auto source = "export default 123;"_kjc; builtinBuilder.addEsm(foo, source.first(source.size()).attach(kj::mv(source))); auto barData = kj::heapArray({1, 2, 3}); ModuleBundle::BundleBuilder bundleBuilder(BASE); bundleBuilder.addSyntheticModule("bar", Module::newDataModuleHandler(barData)); auto registry = ModuleRegistry::Builder(observer, BASE) .add(builtinBuilder.finish()) .add(bundleBuilder.finish()) .finish(); auto attached = registry->attachToIsolate(js, compilationObserver); js.tryCatch([&] { // The built-in only foo:bar module should be found when using a built-in context auto value1 = ModuleRegistry::resolve(js, "foo:bar", "default"_kjc, ResolveContext::Type::BUILTIN); KJ_ASSERT(value1.isNumber()); // But since the module is an built-in only. it should not be found when // resolving with a bundle context. ModuleRegistry::resolve(js, "foo:bar", "default"_kjc, ResolveContext::Type::BUNDLE); JSG_FAIL_REQUIRE(Error, "Should have thrown"); }, [&](Value exception) { auto str = kj::str(exception.getHandle(js)); KJ_ASSERT(str == "Error: Module not found: foo:bar"); }); // Likewise, the bar module should be found when using a bundle context js.tryCatch([&] { auto value2 = ModuleRegistry::resolve(js, "file:///bar", "default"_kjc, ResolveContext::Type::BUNDLE); KJ_ASSERT(value2.isArrayBuffer()); // But should not be found from a built-in context ModuleRegistry::resolve(js, "file:///bar", "default"_kjc, ResolveContext::Type::BUILTIN); JSG_FAIL_REQUIRE(Error, "Should have thrown"); }, [&](Value exception) { auto str = kj::str(exception.getHandle(js)); KJ_ASSERT(str == "Error: Module not found: file:///bar"); }); }); } // ====================================================================================== KJ_TEST("Awaiting top-level dynamic import in synchronous require works as expected") { PREAMBLE([&](Lock& js) { ResolveObserverImpl observer; CompilationObserver compilationObserver; ModuleBundle::BundleBuilder bundleBuilder(BASE); auto foo = kj::str("export default (await import('bar')).default;"); bundleBuilder.addEsmModule("foo", foo); auto bar = kj::str("export default 123;"); bundleBuilder.addEsmModule("bar", bar); auto registry = ModuleRegistry::Builder(observer, BASE).add(bundleBuilder.finish()).finish(); auto attached = registry->attachToIsolate(js, compilationObserver); ModuleRegistry::resolve(js, "file:///foo", "default"_kjc); }); } // ====================================================================================== KJ_TEST("Awaiting a never resolved promise in synchronous require fails as expected") { PREAMBLE([&](Lock& js) { ResolveObserverImpl observer; CompilationObserver compilationObserver; ModuleBundle::BundleBuilder bundleBuilder(BASE); auto foo = kj::str("const p = new Promise(() => {}); await p;"); bundleBuilder.addEsmModule("foo", foo); auto registry = ModuleRegistry::Builder(observer, BASE).add(bundleBuilder.finish()).finish(); auto attached = registry->attachToIsolate(js, compilationObserver); js.tryCatch([&] { ModuleRegistry::resolve(js, "file:///foo", "default"_kjc); KJ_FAIL_ASSERT("Should have failed"); }, [&](Value exception) { auto str = kj::str(exception.getHandle(js)); KJ_ASSERT(str == "Error: Use of top-level await in a synchronously " "required module is restricted to promises that are resolved " "synchronously. This includes any top-level awaits in the " "entrypoint module for a worker. Specifier: \"file:///foo\"."); }); }); } // ====================================================================================== KJ_TEST("Throwing an exception inside a ESM module works as expected") { PREAMBLE([&](Lock& js) { ResolveObserverImpl observer; CompilationObserver compilationObserver; ModuleBundle::BundleBuilder bundleBuilder(BASE); auto foo = kj::str("throw new Error('foo');"); bundleBuilder.addEsmModule("foo", foo); auto registry = ModuleRegistry::Builder(observer, BASE).add(bundleBuilder.finish()).finish(); auto attached = registry->attachToIsolate(js, compilationObserver); js.tryCatch([&] { ModuleRegistry::resolve(js, "file:///foo", "default"_kjc); JSG_FAIL_REQUIRE(Error, "Should have thrown"); }, [&](Value exception) { auto str = kj::str(exception.getHandle(js)); KJ_ASSERT(str == "Error: foo"); }); }); } // ====================================================================================== KJ_TEST("Syntax error in ESM module is properly reported") { PREAMBLE([&](Lock& js) { ResolveObserverImpl observer; CompilationObserver compilationObserver; ModuleBundle::BundleBuilder bundleBuilder(BASE); auto foo = kj::str("export default 123; syntax error"); bundleBuilder.addEsmModule("foo", foo); auto registry = ModuleRegistry::Builder(observer, BASE).add(bundleBuilder.finish()).finish(); auto attached = registry->attachToIsolate(js, compilationObserver); js.tryCatch( [&] { ModuleRegistry::resolve(js, "file:///foo", "default"_kjc); }, [&](Value exception) { auto str = kj::str(exception.getHandle(js)); KJ_ASSERT(str == "SyntaxError: Unexpected identifier 'error'"); }); }); } // ====================================================================================== KJ_TEST("Throwing an exception inside a CJS-style eval module works as expected") { PREAMBLE([&](Lock& js) { ResolveObserverImpl observer; CompilationObserver compilationObserver; ModuleBundle::BundleBuilder bundleBuilder(BASE); kj::String source = kj::str("exports.foo = 123; throw new Error('bar');"); bundleBuilder.addSyntheticModule("foo", Module::newCjsStyleModuleHandler(source, "foo"_kj)); auto registry = ModuleRegistry::Builder(observer, BASE).add(bundleBuilder.finish()).finish(); auto attached = registry->attachToIsolate(js, compilationObserver); js.tryCatch([&] { ModuleRegistry::resolve(js, "file:///foo", "foo"_kjc); JSG_FAIL_REQUIRE(Error, "Should have thrown"); }, [&](Value exception) { auto str = kj::str(exception.getHandle(js)); KJ_ASSERT(str == "Error: bar"); }); }); } // ====================================================================================== KJ_TEST("Invalid JSON syntax module throws exception as expected") { PREAMBLE([&](Lock& js) { ResolveObserverImpl observer; CompilationObserver compilationObserver; ModuleBundle::BundleBuilder bundleBuilder(BASE); auto json = kj::str("not valid json"); bundleBuilder.addSyntheticModule("foo", Module::newJsonModuleHandler(json.first(json.size()))); auto esm = kj::str("import foo from 'foo'"); bundleBuilder.addEsmModule("bar", esm, Module::Flags::MAIN); auto registry = ModuleRegistry::Builder(observer, BASE).add(bundleBuilder.finish()).finish(); auto attached = registry->attachToIsolate(js, compilationObserver); js.tryCatch([&] { ModuleRegistry::resolve(js, "file:///foo", "default"_kjc); JSG_FAIL_REQUIRE(Error, "Should have thrown"); }, [&](Value exception) { auto str = kj::str(exception.getHandle(js)); KJ_ASSERT(str == "SyntaxError: Unexpected token 'o', \"not valid json\" is not valid JSON"); }); // We can try multiple times and it doesn't matter. js.tryCatch([&] { ModuleRegistry::resolve(js, "file:///foo", "default"_kjc); JSG_FAIL_REQUIRE(Error, "Should have thrown"); }, [&](Value exception) { auto str = kj::str(exception.getHandle(js)); KJ_ASSERT(str == "SyntaxError: Unexpected token 'o', \"not valid json\" is not valid JSON"); }); // We get the same error even if statically imported after the previous imports js.tryCatch([&] { ModuleRegistry::resolve(js, "file:///bar", "default"_kjc); JSG_FAIL_REQUIRE(Error, "Should have thrown"); }, [&](Value exception) { auto str = kj::str(exception.getHandle(js)); KJ_ASSERT(str == "SyntaxError: Unexpected token 'o', \"not valid json\" is not valid JSON"); }); }); } // ====================================================================================== KJ_TEST("Recursive import works or fails as expected") { PREAMBLE([&](Lock& js) { ResolveObserverImpl observer; CompilationObserver compilationObserver; ModuleBundle::BundleBuilder bundleBuilder(BASE); // A recursive import with an ESM works just fine... auto foo = kj::str("import foo from 'foo'; export default 123;"); bundleBuilder.addEsmModule("foo", foo); auto source = kj::str("require('bar')"); // A CommonJS-style module, however, does not allow recursive evaluation. bundleBuilder.addSyntheticModule("bar", Module::newCjsStyleModuleHandler(source, "bar"_kj)); auto registry = ModuleRegistry::Builder(observer, BASE).add(bundleBuilder.finish()).finish(); auto attached = registry->attachToIsolate(js, compilationObserver); auto val1 = ModuleRegistry::resolve(js, "file:///foo", "default"_kjc); KJ_ASSERT(val1.isNumber()); js.tryCatch( [&] { ModuleRegistry::resolve(js, "file:///bar", "default"_kjc); }, [&](Value exception) { auto str = kj::str(exception.getHandle(js)); KJ_ASSERT(str == "Error: Module cannot be recursively evaluated: file:///bar"); }); }); } // ====================================================================================== KJ_TEST("Recursively require ESM from CJS required from ESM fails as expected (dynamic import)") { PREAMBLE([&](Lock& js) { ResolveObserverImpl observer; CompilationObserver compilationObserver; ModuleBundle::BundleBuilder bundleBuilder(BASE); // In this test, we have an ESM module (bar) that imports a CJS style // module (foo) that synchronously tries to require the ESM module (bar). // The circular dependency between foo and baz here, as CJS style modules, // should be ok in that it should not throw an error. However, the circular // dependency between foo and bar is more problematic since it is forbidden // to depend on an ESM that has not yet been fully resolved. auto source1 = kj::str("exports = require('foo');"); auto source2 = kj::str("require('baz'); exports = require('bar');"); bundleBuilder.addSyntheticModule("baz", Module::newCjsStyleModuleHandler(source1, "baz"_kj)); bundleBuilder.addSyntheticModule("foo", Module::newCjsStyleModuleHandler(source2, "foo"_kj)); auto bar = kj::str("export default {}; await import('foo');"); bundleBuilder.addEsmModule("bar", bar); auto registry = ModuleRegistry::Builder(observer, BASE).add(bundleBuilder.finish()).finish(); auto attached = registry->attachToIsolate(js, compilationObserver); js.tryCatch([&] { ModuleRegistry::resolve(js, "file:///bar", "default"_kjc); JSG_FAIL_REQUIRE(Error, "should have failed"); }, [&](Value exception) { auto str = kj::str(exception.getHandle(js)); KJ_ASSERT(str == "Error: Circular dependency when resolving module: file:///bar"); }); }); } // ====================================================================================== KJ_TEST("Recursively require ESM from CJS required from ESM fails as expected (static import)") { PREAMBLE([&](Lock& js) { ResolveObserverImpl observer; CompilationObserver compilationObserver; ModuleBundle::BundleBuilder bundleBuilder(BASE); // In this test, we have an ESM module (bar) that imports a CJS style // module (foo) that synchronously tries to require the ESM module (bar). // The circular dependency between foo and baz here, as CJS style modules, // should be ok in that it should not throw an error. However, the circular // dependency between foo and bar is more problematic since it is forbidden // to depend on an ESM that has not yet been fully resolved. auto source1 = kj::str("exports = require('foo');"); auto source2 = kj::str("require('baz'); exports = require('bar');"); bundleBuilder.addSyntheticModule("baz", Module::newCjsStyleModuleHandler(source1, "baz"_kj)); bundleBuilder.addSyntheticModule("foo", Module::newCjsStyleModuleHandler(source2, "foo"_kj)); auto bar = kj::str("export default {}; import bar from 'foo';"); bundleBuilder.addEsmModule("bar", bar); auto registry = ModuleRegistry::Builder(observer, BASE).add(bundleBuilder.finish()).finish(); auto attached = registry->attachToIsolate(js, compilationObserver); js.tryCatch([&] { ModuleRegistry::resolve(js, "file:///bar", "default"_kjc); JSG_FAIL_REQUIRE(Error, "should have failed"); }, [&](Value exception) { auto str = kj::str(exception.getHandle(js)); KJ_ASSERT(str == "Error: Circular dependency when resolving module: file:///bar"); }); }); } // ====================================================================================== KJ_TEST("ESM -> CJS -> require(ESM) -> static import CJS circular dependency fails gracefully") { // This tests a specific crash scenario: when a CJS module (b) is mid-evaluation // (kEvaluating), and it require()s an ESM (c) that statically imports the same // CJS module (b), V8's InnerModuleEvaluation would call Module::Evaluate on // the kEvaluating synthetic module, hitting a CHECK crash. The resolveModuleCallback // kEvaluating guard prevents this by rejecting the circular dependency at // instantiation time. PREAMBLE([&](Lock& js) { ResolveObserverImpl observer; CompilationObserver compilationObserver; ModuleBundle::BundleBuilder bundleBuilder(BASE); // a.js (ESM) -> imports b (CJS) auto a = kj::str("import b from 'b'; export default b;"); bundleBuilder.addEsmModule("a", a, Module::Flags::MAIN); // b (CJS) -> require('c') which is an ESM that imports b back auto bSource = kj::str("exports = require('c');"); bundleBuilder.addSyntheticModule( "b", Module::newCjsStyleModuleHandler(bSource, "b"_kj)); // c.js (ESM) -> imports b (CJS) — creates the circular dependency auto c = kj::str("import b from 'b'; export default b;"); bundleBuilder.addEsmModule("c", c); auto registry = ModuleRegistry::Builder(observer, BASE).add(bundleBuilder.finish()).finish(); auto attached = registry->attachToIsolate(js, compilationObserver); js.tryCatch([&] { ModuleRegistry::resolve(js, "file:///a", "default"_kjc); JSG_FAIL_REQUIRE(Error, "Should have thrown"); }, [&](Value exception) { auto str = kj::str(exception.getHandle(js)); KJ_ASSERT(str == "TypeError: Circular dependency when resolving module: b"); }); }); } // ====================================================================================== KJ_TEST("UNWRAP_DEFAULT returns namespace for bundle ESM, default for others") { PREAMBLE([&](Lock& js) { ResolveObserverImpl observer; CompilationObserver compilationObserver; ModuleBundle::BundleBuilder bundleBuilder(BASE); // Bundle ESM with named exports (no __cjsUnwrapDefault) auto esm = kj::str("export default 42; export const name = 'esm';"); bundleBuilder.addEsmModule("esm-mod", esm, Module::Flags::MAIN); // Bundle ESM with __cjsUnwrapDefault convention auto esmCjs = kj::str("export default 'unwrapped'; export const __cjsUnwrapDefault = true;"); bundleBuilder.addEsmModule("esm-cjs", esmCjs); // JSON synthetic module auto json = kj::str("{\"key\": \"value\"}"); bundleBuilder.addSyntheticModule( "data.json", Module::newJsonModuleHandler(json.first(json.size()))); // Text synthetic module auto text = kj::str("hello world"); bundleBuilder.addSyntheticModule( "data.txt", Module::newTextModuleHandler(text.first(text.size()))); auto registry = ModuleRegistry::Builder(observer, BASE).add(bundleBuilder.finish()).finish(); auto attached = registry->attachToIsolate(js, compilationObserver); // Bundle ESM without __cjsUnwrapDefault: tryResolveModuleNamespace with UnwrapDefault::YES // returns the full namespace (has "default", "name" properties). js.tryCatch([&] { auto val = KJ_ASSERT_NONNULL(ModuleRegistry::tryResolveModuleNamespace(js, "file:///esm-mod", ResolveContext::Type::BUNDLE, ResolveContext::Source::REQUIRE, kj::none, modules::UnwrapDefault::YES)); auto ns = KJ_ASSERT_NONNULL(val.tryCast()); // The namespace should have both "default" and "name" properties. auto nameVal = ns.get(js, "name"); KJ_ASSERT(!nameVal.isUndefined()); KJ_ASSERT(kj::str(nameVal) == "esm"); auto defaultVal = ns.get(js, "default"); KJ_ASSERT(!defaultVal.isUndefined()); }, [&](Value exception) { js.throwException(kj::mv(exception)); }); // Bundle ESM with __cjsUnwrapDefault: returns the default export. js.tryCatch([&] { auto result = KJ_ASSERT_NONNULL(ModuleRegistry::tryResolveModuleNamespace(js, "file:///esm-cjs", ResolveContext::Type::BUNDLE, ResolveContext::Source::REQUIRE, kj::none, modules::UnwrapDefault::YES)); KJ_ASSERT(kj::str(result) == "unwrapped"); }, [&](Value exception) { js.throwException(kj::mv(exception)); }); // JSON synthetic module: returns the parsed value (the default export). js.tryCatch([&] { auto result = KJ_ASSERT_NONNULL(ModuleRegistry::tryResolveModuleNamespace(js, "file:///data.json", ResolveContext::Type::BUNDLE, ResolveContext::Source::REQUIRE, kj::none, modules::UnwrapDefault::YES)); // Should be the parsed JSON object, not the namespace. auto obj = KJ_ASSERT_NONNULL(result.tryCast()); KJ_ASSERT(kj::str(obj.get(js, "key")) == "value"); }, [&](Value exception) { js.throwException(kj::mv(exception)); }); // Text synthetic module: returns the string value (the default export). js.tryCatch([&] { auto result = KJ_ASSERT_NONNULL(ModuleRegistry::tryResolveModuleNamespace(js, "file:///data.txt", ResolveContext::Type::BUNDLE, ResolveContext::Source::REQUIRE, kj::none, modules::UnwrapDefault::YES)); // The default is a string — JsValue directly. KJ_ASSERT(kj::str(result) == "hello world"); }, [&](Value exception) { js.throwException(kj::mv(exception)); }); // Without UnwrapDefault, all modules return the namespace. js.tryCatch([&] { auto val = KJ_ASSERT_NONNULL(ModuleRegistry::tryResolveModuleNamespace( js, "file:///data.json", ResolveContext::Type::BUNDLE, ResolveContext::Source::REQUIRE)); auto ns = KJ_ASSERT_NONNULL(val.tryCast()); // Should have a "default" property (it's the namespace). KJ_ASSERT(!ns.get(js, "default").isUndefined()); }, [&](Value exception) { js.throwException(kj::mv(exception)); }); }); } // ====================================================================================== KJ_TEST("Resolution occurs relative to the referrer") { ResolveObserver observer; CompilationObserver compilationObserver; ModuleRegistry::Builder registryBuilder(observer, BASE); ModuleBundle::BundleBuilder builder(BASE); builder.addSyntheticModule("foo/bar", Module::newDataModuleHandler(nullptr)); builder.addSyntheticModule("bar", Module::newDataModuleHandler(nullptr)); // The base URL of the referrer is file:///foo/ ... so in each of the // following cases, the specifier should be resolved relative to that. // For instance, 'bar' should resolve as file:///foo/bar, while '../bar' // should resolve as file:///bar auto bar = kj::str("export * as abc from 'bar';" // file:///foo/bar "export * as def from './bar';" // file:///foo/bar "export * as ghi from '../bar';" // file:///bar "export * as jkl from '/bar';" // file:///bar "export * as lmn from '../foo/bar';"); // file:///foo/bar builder.addEsmModule("foo/", bar); auto registry = registryBuilder.add(builder.finish()).finish(); PREAMBLE([&](Lock& js) { auto attached = registry->attachToIsolate(js, compilationObserver); js.tryCatch([&] { auto abc = ModuleRegistry::resolve(js, "file:///foo/", "abc"_kjc); auto def = ModuleRegistry::resolve(js, "file:///foo/", "def"_kjc); auto ghi = ModuleRegistry::resolve(js, "file:///foo/", "ghi"_kjc); auto jkl = ModuleRegistry::resolve(js, "file:///foo/", "jkl"_kjc); auto lmn = ModuleRegistry::resolve(js, "file:///foo/", "lmn"_kjc); KJ_ASSERT(abc.strictEquals(def)); KJ_ASSERT(abc.strictEquals(lmn)); KJ_ASSERT(!abc.strictEquals(ghi)); KJ_ASSERT(ghi.strictEquals(jkl)); }, [&](Value exception) { js.throwException(kj::mv(exception)); }); }); } // ====================================================================================== KJ_TEST("Building a bundle from a capnp description works") { capnp::MallocMessageBuilder builder; auto bundle = builder.initRoot(); auto modules = bundle.initModules(3); auto str = kj::str("export default 1+1;"); auto wasm = kj::heapArray({ 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x07, 0x01, 0x60, 0x02, 0x7f, 0x7f, 0x01, 0x7f, 0x03, 0x02, 0x01, 0x00, 0x07, 0x07, 0x01, 0x03, 0x61, 0x64, 0x64, 0x00, 0x00, 0x0a, 0x09, 0x01, 0x07, 0x00, 0x20, 0x00, 0x20, 0x01, 0x6a, 0x0b, }); auto data = kj::heapArray({1, 2, 3}); modules[0].setName("foo:bar"); modules[0].setSrc(str.asBytes()); modules[0].setType(workerd::jsg::ModuleType::BUILTIN); modules[1].setName("foo:baz"); modules[1].setWasm(wasm); modules[1].setType(workerd::jsg::ModuleType::BUILTIN); modules[2].setName("foo:qux"); modules[2].setSrc(data.asBytes()); modules[2].setType(workerd::jsg::ModuleType::BUILTIN); ModuleBundle::BuiltinBuilder bundleBuilder; ModuleBundle::getBuiltInBundleFromCapnp(bundleBuilder, bundle.asReader()); auto moduleBundle = bundleBuilder.finish(); { const auto foo = "foo:bar"_url; ResolveContext context{ .type = ResolveContext::Type::BUILTIN, .source = ResolveContext::Source::INTERNAL, .normalizedSpecifier = foo, .referrerNormalizedSpecifier = BASE, }; auto resolved = KJ_ASSERT_NONNULL(moduleBundle->lookup(context)); auto& module = KJ_ASSERT_NONNULL(resolved.module); KJ_ASSERT(module.id() == foo); } { const auto bar = "foo:baz"_url; ResolveContext context{ .type = ResolveContext::Type::BUILTIN, .source = ResolveContext::Source::INTERNAL, .normalizedSpecifier = bar, .referrerNormalizedSpecifier = BASE, }; auto resolved = KJ_ASSERT_NONNULL(moduleBundle->lookup(context)); auto& module = KJ_ASSERT_NONNULL(resolved.module); KJ_ASSERT(module.id() == bar); } { const auto qux = "foo:qux"_url; ResolveContext context{ .type = ResolveContext::Type::BUILTIN, .source = ResolveContext::Source::INTERNAL, .normalizedSpecifier = qux, .referrerNormalizedSpecifier = BASE, }; auto resolved = KJ_ASSERT_NONNULL(moduleBundle->lookup(context)); auto& module = KJ_ASSERT_NONNULL(resolved.module); KJ_ASSERT(module.id() == qux); } PREAMBLE([&](Lock& js) { ResolveObserver resolveObserver; CompilationObserver compilationObserver; auto registry = ModuleRegistry::Builder(resolveObserver, BASE).add(kj::mv(moduleBundle)).finish(); auto attached = registry->attachToIsolate(js, compilationObserver); // The foo:bar module is interpreted as an ESM js.tryCatch([&] { auto val = ModuleRegistry::resolve(js, "foo:bar"); KJ_ASSERT(val.isNumber()); }, [&](Value exception) { js.throwException(kj::mv(exception)); }); }); } // ====================================================================================== KJ_TEST("Using a registry from multiple threads works") { kj::AsyncIoContext io = kj::setupAsyncIo(); ModuleBundle::BundleBuilder bundleBuilder(BASE); static const auto foo = "export default 123; for (let n = 0; n < 100000; n++) {}"_kjc; bundleBuilder.addEsmModule("foo", foo); ResolveObserver resolveObserver; auto registry = ModuleRegistry::Builder(resolveObserver, BASE).add(bundleBuilder.finish()).finish(); struct NonOpErrorHandler final: public kj::TaskSet::ErrorHandler { void taskFailed(kj::Exception&& exception) override {} }; NonOpErrorHandler errorHandler; kj::TaskSet tasks(errorHandler); static const auto makeRunnable = [](kj::Arc registry, kj::Own> fulfiller) { return [registry = kj::mv(registry), fulfiller = kj::mv(fulfiller)]() mutable { PREAMBLE([&](Lock& js) { CompilationObserver compilationObserver; auto attached = registry->attachToIsolate(js, compilationObserver); js.tryCatch([&] { auto val = ModuleRegistry::resolve(js, "file:///foo"); KJ_ASSERT(val.isNumber()); }, [&](Value exception) { js.throwException(kj::mv(exception)); }); }); fulfiller->fulfill(); }; }; struct RunnableAndPromise { kj::Promise promise; kj::Function runnable; }; static const auto makeRunnableAndPromise = [](kj::Arc registry) -> RunnableAndPromise { auto paf = kj::newPromiseAndCrossThreadFulfiller(); return {kj::mv(paf.promise), makeRunnable(kj::mv(registry), kj::mv(paf.fulfiller))}; }; auto [paf1, task1] = makeRunnableAndPromise(registry.addRef()); kj::Thread(kj::mv(task1)).detach(); auto [paf2, task2] = makeRunnableAndPromise(registry.addRef()); kj::Thread(kj::mv(task2)).detach(); auto [paf3, task3] = makeRunnableAndPromise(registry.addRef()); kj::Thread(kj::mv(task3)).detach(); auto [paf4, task4] = makeRunnableAndPromise(registry.addRef()); kj::Thread(kj::mv(task4)).detach(); auto [paf5, task5] = makeRunnableAndPromise(registry.addRef()); kj::Thread(kj::mv(task5)).detach(); tasks.add(kj::mv(paf1)); tasks.add(kj::mv(paf2)); tasks.add(kj::mv(paf3)); tasks.add(kj::mv(paf4)); tasks.add(kj::mv(paf5)); tasks.onEmpty().wait(io.waitScope); } // ====================================================================================== KJ_TEST("Fallback service can see original raw specifier if provided") { ResolveObserver resolveObserver; CompilationObserver compilationObserver; ModuleRegistry::Builder builder( resolveObserver, BASE, ModuleRegistry::Builder::Options::ALLOW_FALLBACK); auto rawSpecifier = "nothing"_kjc; const auto id = "file:///nothing"_url; bool called = false; builder.add(ModuleBundle::newFallbackBundle([&](const ResolveContext& context) { KJ_ASSERT(context.rawSpecifier == rawSpecifier); KJ_ASSERT(context.normalizedSpecifier == id); KJ_ASSERT(context.referrerNormalizedSpecifier == BASE); called = true; return kj::none; })); auto registry = builder.finish(); ResolveContext context = { .type = ResolveContext::Type::BUNDLE, .source = ResolveContext::Source::INTERNAL, .normalizedSpecifier = id, .referrerNormalizedSpecifier = BASE, .rawSpecifier = rawSpecifier, }; KJ_ASSERT(registry->lookup(context) == kj::none); KJ_ASSERT(called); } // ====================================================================================== KJ_TEST("Fallback service can return a module with a different specifier") { ResolveObserver resolveObserver; CompilationObserver compilationObserver; ModuleRegistry::Builder builder( resolveObserver, BASE, ModuleRegistry::Builder::Options::ALLOW_FALLBACK); auto rawSpecifier = "nothing"_kjc; const auto id = "file:///nothing"_url; const auto url = "file:///different"_url; int called = 0; builder.add(ModuleBundle::newFallbackBundle([&](const ResolveContext& context) { called++; kj::Own mod = Module::newSynthetic( url.clone(), Module::Type::FALLBACK, Module::newDataModuleHandler(nullptr)); return kj::Maybe>>(kj::mv(mod)); })); auto registry = builder.finish(); ResolveContext context = { .type = ResolveContext::Type::BUNDLE, .source = ResolveContext::Source::INTERNAL, .normalizedSpecifier = id, .referrerNormalizedSpecifier = BASE, .rawSpecifier = rawSpecifier, }; auto& module1 = KJ_ASSERT_NONNULL(registry->lookup(context)); ResolveContext context2 = { .type = ResolveContext::Type::BUNDLE, .source = ResolveContext::Source::INTERNAL, .normalizedSpecifier = url, .referrerNormalizedSpecifier = BASE, .rawSpecifier = rawSpecifier, }; auto& module2 = KJ_ASSERT_NONNULL(registry->lookup(context2)); auto& module3 = KJ_ASSERT_NONNULL(registry->lookup(context)); // Both specifiers should resolve to the same module so the called count should be 1. KJ_ASSERT(called == 1); KJ_ASSERT(module1.id() == url); KJ_ASSERT(&module1 == &module2); KJ_ASSERT(&module2 == &module3); } // ====================================================================================== KJ_TEST("Percent-encoding in specifiers is normalized properly") { ResolveObserver resolveObserver; CompilationObserver compilationObserver; ModuleBundle::BundleBuilder builder(BASE); // A specifier might have percent-encoded characters. We want those to be normalized // so that they are matched correctly. For instance, %66oo%2fbar should be normalized // to foo%2Fbar, and %66oo/bar should be normalized to foo/bar. Specifically, characters // that generally do not need to be percent-encoded should be normalized to their // unencoded form, while characters that need percent encoded should be normalized // to their capitalized percent-encoded form (e.g. %2f becomes %2F). This ensures that // when these different forms are used to import they will resolve to the expected // module. builder.addSyntheticModule("foo%2fbar", Module::newDataModuleHandler(nullptr)); builder.addSyntheticModule("foo/bar", Module::newDataModuleHandler(nullptr)); auto foo = kj::str("export { default as abc } from 'foo%2fbar';" "export { default as def } from 'foo/bar';" "export { default as ghi } from '%66oo/bar';" "export { default as jkl } from '%66oo%2fbar';"); builder.addEsmModule("foo", foo); auto registry = ModuleRegistry::Builder(resolveObserver, BASE).add(builder.finish()).finish(); PREAMBLE([&](Lock& js) { auto attached = registry->attachToIsolate(js, compilationObserver); js.tryCatch([&] { auto abc = ModuleRegistry::resolve(js, "foo", "abc"_kjc); auto def = ModuleRegistry::resolve(js, "foo", "def"_kjc); auto ghi = ModuleRegistry::resolve(js, "foo", "ghi"_kjc); auto jkl = ModuleRegistry::resolve(js, "foo", "jkl"_kjc); KJ_ASSERT(abc.strictEquals(jkl)); KJ_ASSERT(def.strictEquals(ghi)); KJ_ASSERT(!abc.strictEquals(def)); }, [&](Value exception) { js.throwException(kj::mv(exception)); }); }); } // ====================================================================================== KJ_TEST("Aliased modules (import maps) work") { ResolveObserver resolveObserver; CompilationObserver compilationObserver; ModuleBundle::BundleBuilder builder(BASE); builder.addSyntheticModule("http://example/foo", Module::newDataModuleHandler(nullptr)); builder.alias("bar", "http://example/foo"); try { builder.alias("bar", "baz"); KJ_FAIL_ASSERT("should have thrown"); } catch (kj::Exception& ex) { KJ_ASSERT(ex.getDescription() == "Module \"file:///bar\" already added to bundle"); } try { builder.alias("http://example/%66oo", "baz"); KJ_FAIL_ASSERT("should have thrown"); } catch (kj::Exception& ex) { KJ_ASSERT(ex.getDescription() == "Module \"http://example/foo\" already added to bundle"); } auto src = kj::str("export { default as abc } from 'bar';" "export { default as def } from 'http://example/%66oo';"); builder.addEsmModule("qux", src); auto registry = ModuleRegistry::Builder(resolveObserver, BASE).add(builder.finish()).finish(); ResolveContext contextBar{ .type = ResolveContext::Type::BUNDLE, .source = ResolveContext::Source::INTERNAL, .normalizedSpecifier = "file:///bar"_url, .referrerNormalizedSpecifier = BASE, }; ResolveContext contextFoo{ .type = ResolveContext::Type::BUNDLE, .source = ResolveContext::Source::INTERNAL, .normalizedSpecifier = "http://example/foo"_url, .referrerNormalizedSpecifier = BASE, }; auto& bar = KJ_ASSERT_NONNULL(registry->lookup(contextBar)); auto& foo = KJ_ASSERT_NONNULL(registry->lookup(contextFoo)); // The aliases resolve to the same underlying module... KJ_ASSERT(&bar == &foo); PREAMBLE([&](Lock& js) { auto attached = registry->attachToIsolate(js, compilationObserver); js.tryCatch([&] { // While the aliased identifiers resolve to the same underlying module, the // evaluate into two separate module instances. This is similar in behavior // to how query string and fragments work. The fact that they use the same // underlying definition is not really that important. auto abc = ModuleRegistry::resolve(js, "qux", "abc"_kjc); auto def = ModuleRegistry::resolve(js, "qux", "def"_kjc); KJ_ASSERT(abc.isArrayBuffer()); KJ_ASSERT(def.isArrayBuffer()); KJ_ASSERT(!abc.strictEquals(def)); }, [&](Value exception) { js.throwException(kj::mv(exception)); }); }); } // ====================================================================================== KJ_TEST("Import attribute type:json succeeds for JSON modules") { ResolveObserver resolveObserver; CompilationObserver compilationObserver; ModuleBundle::BundleBuilder builder(BASE); // An ESM that imports a JSON module with the type attribute auto entry = kj::str("import data from 'data.json' with { type: 'json' }; export default data;"); builder.addEsmModule("entry", entry, Module::Flags::MAIN); auto json = kj::str("{\"key\": \"value\"}"); builder.addSyntheticModule("data.json", Module::newJsonModuleHandler(json.first(json.size())), nullptr, Module::ContentType::JSON); auto registry = ModuleRegistry::Builder(resolveObserver, BASE).add(builder.finish()).finish(); PREAMBLE([&](Lock& js) { auto attached = registry->attachToIsolate(js, compilationObserver); js.tryCatch([&] { auto val = ModuleRegistry::resolve(js, "file:///entry", "default"_kjc); // The JSON module should have been resolved and its value should be the parsed object. KJ_ASSERT(!val.isUndefined()); }, [&](Value exception) { js.throwException(kj::mv(exception)); }); }); } // ====================================================================================== KJ_TEST("Import attribute type:json fails for non-JSON modules") { ResolveObserver resolveObserver; CompilationObserver compilationObserver; ModuleBundle::BundleBuilder builder(BASE); // An ESM that imports another ESM with type:json (should fail - ESM is not JSON) auto entry = kj::str("import foo from 'other' with { type: 'json' }; export default foo;"); builder.addEsmModule("entry", entry, Module::Flags::MAIN); auto other = kj::str("export default 42;"); builder.addEsmModule("other", other); auto registry = ModuleRegistry::Builder(resolveObserver, BASE).add(builder.finish()).finish(); PREAMBLE([&](Lock& js) { auto attached = registry->attachToIsolate(js, compilationObserver); js.tryCatch([&] { ModuleRegistry::resolve(js, "file:///entry", "default"_kjc); JSG_FAIL_REQUIRE(Error, "Should have thrown"); }, [&](Value exception) { auto str = kj::str(exception.getHandle(js)); KJ_ASSERT(str == "TypeError: Module \"other\" is not of type \"json\""); }); }); } // ====================================================================================== KJ_TEST("Unsupported import attributes are rejected") { ResolveObserver resolveObserver; CompilationObserver compilationObserver; ModuleBundle::BundleBuilder builder(BASE); auto foo = kj::str("import abc from 'foo' with { unsupported: 'value' };"); builder.addEsmModule("foo", foo); auto registry = ModuleRegistry::Builder(resolveObserver, BASE).add(builder.finish()).finish(); PREAMBLE([&](Lock& js) { auto attached = registry->attachToIsolate(js, compilationObserver); js.tryCatch([&] { ModuleRegistry::resolve(js, "foo", "default"_kjc); JSG_FAIL_REQUIRE(Error, "Should have thrown"); }, [&](Value exception) { auto str = kj::str(exception.getHandle(js)); KJ_ASSERT(str == "TypeError: Unsupported import attribute: \"unsupported\""); }); }); } // ====================================================================================== KJ_TEST("Using a deferred eval callback works") { ResolveObserver resolveObserver; CompilationObserver compilationObserver; ModuleBundle::BundleBuilder builder(BASE); auto foo = kj::str("export default 1;"); builder.addEsmModule("foo", foo); bool called = false; auto registry = ModuleRegistry::Builder(resolveObserver, BASE) .add(builder.finish()) .setEvalCallback([&called](Lock& js, const Module& module, auto v8Module, const auto& observer) { called = true; return js.resolvedJsPromise(js.num(123)); }).finish(); PREAMBLE([&](Lock& js) { auto attached = registry->attachToIsolate(js, compilationObserver); js.tryCatch([&] { ModuleRegistry::resolve(js, "foo", "default"_kjc); KJ_ASSERT(false); }, [&](auto exception) {}); // We don't care about the specific exception above. We only want to know that // the eval callback was invoked. KJ_ASSERT(called); }); } // ====================================================================================== KJ_TEST("Fallback receives rawSpecifier and source through V8 static import resolution") { // This test verifies that when a module is resolved through V8's static import // pipeline (which goes through IsolateModuleRegistry::resolveWithCaching), the // fallback callback receives the correct rawSpecifier and source fields. // Regression test for https://github.com/cloudflare/workerd/issues/6474 and // https://github.com/cloudflare/workerd/issues/6475. ResolveObserverImpl resolveObserver; CompilationObserver compilationObserver; // The entry module statically imports "./missing", which is not in the bundle. // This forces fallback resolution through resolveWithCaching. ModuleBundle::BundleBuilder bundleBuilder(BASE); auto main = kj::str("import './missing';"); bundleBuilder.addEsmModule("main", main, Module::Flags::MAIN); bool fallbackCalled = false; auto fallback = ModuleBundle::newFallbackBundle( [&](const ResolveContext& context) -> kj::Maybe>> { fallbackCalled = true; // The rawSpecifier should be the original "./missing" as written in the import. KJ_ASSERT(context.rawSpecifier == "./missing"_kjc); // A static import should have source == STATIC_IMPORT. KJ_ASSERT(context.source == ResolveContext::Source::STATIC_IMPORT); // Return a synthetic module to satisfy the import. return kj::Maybe>>( Module::newSynthetic(context.normalizedSpecifier.clone(), Module::Type::FALLBACK, Module::newDataModuleHandler(nullptr))); }); auto registry = ModuleRegistry::Builder( resolveObserver, BASE, ModuleRegistry::Builder::Options::ALLOW_FALLBACK) .add(bundleBuilder.finish()) .add(kj::mv(fallback)) .finish(); PREAMBLE([&](Lock& js) { auto attached = registry->attachToIsolate(js, compilationObserver); js.tryCatch([&] { ModuleRegistry::resolve(js, "file:///main"); }, [&](Value exception) { js.throwException(kj::mv(exception)); }); }); KJ_ASSERT(fallbackCalled); } // ====================================================================================== KJ_TEST("Fallback receives REQUIRE source through require() resolution") { // This test verifies that when a module is resolved through require() (which also // goes through IsolateModuleRegistry::resolveWithCaching), the fallback callback // receives source == REQUIRE. // Regression test for https://github.com/cloudflare/workerd/issues/6475. ResolveObserverImpl resolveObserver; CompilationObserver compilationObserver; ModuleBundle::BundleBuilder bundleBuilder(BASE); bool fallbackCalled = false; auto fallback = ModuleBundle::newFallbackBundle( [&](const ResolveContext& context) -> kj::Maybe>> { fallbackCalled = true; // A require() call should have source == REQUIRE. KJ_ASSERT(context.source == ResolveContext::Source::REQUIRE); KJ_ASSERT(context.rawSpecifier == "missing"_kjc); return kj::Maybe>>( Module::newSynthetic(context.normalizedSpecifier.clone(), Module::Type::FALLBACK, Module::newDataModuleHandler(nullptr))); }); auto registry = ModuleRegistry::Builder( resolveObserver, BASE, ModuleRegistry::Builder::Options::ALLOW_FALLBACK) .add(bundleBuilder.finish()) .add(kj::mv(fallback)) .finish(); PREAMBLE([&](Lock& js) { auto attached = registry->attachToIsolate(js, compilationObserver); js.tryCatch([&] { // tryResolveModuleNamespace with Source::REQUIRE exercises the require() path // through IsolateModuleRegistry::require() -> resolveWithCaching(). ModuleRegistry::tryResolveModuleNamespace( js, "missing", ResolveContext::Type::BUNDLE, ResolveContext::Source::REQUIRE); }, [&](Value exception) { js.throwException(kj::mv(exception)); }); }); KJ_ASSERT(fallbackCalled); } } // namespace } // namespace workerd::jsg::test