Skip to content
File

Blob: src/workerd/jsg/modules-new-test.c++

77.5 KB
1// Copyright (c) 2017-2022 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 "observer.h"
6#include "type-wrapper.h"
7#include "url.h"
8 
9#include <workerd/jsg/modules-new.h>
10#include <workerd/jsg/modules.capnp.h>
11#include <workerd/jsg/setup.h>
12 
13#include <capnp/message.h>
14#include <kj/async-io.h>
15#include <kj/test.h>
16#include <kj/thread.h>
17#include <kj/vector.h>
18 
19namespace workerd::jsg::test {
20namespace {
21using workerd::jsg::modules::Module;
22using workerd::jsg::modules::ModuleBundle;
23using workerd::jsg::modules::ModuleRegistry;
24using workerd::jsg::modules::ResolveContext;
25 
26V8System v8System;
27const jsg::Url BASE = "file:///"_url;
28 
29struct ResolveObserverImpl: public ResolveObserver {
30 struct Request {
31 Url id;
32 ResolveObserver::Context context;
33 ResolveObserver::Source source;
34 bool found = false;
35 };
36 mutable kj::Vector<Request> modules;
37 
38 struct MyResolveStatus: public ResolveObserver::ResolveStatus {
39 Request& request;
40 MyResolveStatus(Request& request): request(request) {}
41 void found() override {
42 request.found = true;
43 }
44 void notFound() override {
45 request.found = false;
46 }
47 };
48 
49 kj::Own<ResolveObserver::ResolveStatus> onResolveModule(
50 const Url& id, Context context, Source source) const override {
51 modules.add(Request{
52 .id = id.clone(),
53 .context = context,
54 .source = source,
55 });
56 return kj::heap<MyResolveStatus>(modules.back());
57 }
58};
59 
60struct TestType: public jsg::Object {
61 bool barCalled = false;
62 kj::Maybe<JsRef<JsObject>> exports;
63 
64 TestType(Lock&, const jsg::Url&) {}
65 
66 void bar() {
67 barCalled = true;
68 }
69 
70 JsObject getExports(Lock& js) {
71 KJ_IF_SOME(exp, exports) {
72 return exp.getHandle(js);
73 }
74 return exports.emplace(JsRef<JsObject>(js, js.obj())).getHandle(js);
75 }
76 
77 void setExports(Lock& js, JsObject obj) {
78 exports = JsRef(js, obj);
79 }
80 
81 JsValue require(Lock& js, kj::String specifier) {
82 return js.tryCatch([&] { return ModuleRegistry::resolve(js, specifier); },
83 [&](Value exception) -> JsValue { js.throwException(kj::mv(exception)); });
84 }
85 
86 jsg::JsValue getModuleExports(jsg::Lock& js) {
87 return getExports(js);
88 }
89 
90 JSG_RESOURCE_TYPE(TestType) {
91 JSG_METHOD(bar);
92 JSG_METHOD(require);
93 JSG_PROTOTYPE_PROPERTY(exports, getExports, setExports);
94 }
95};
96 
97struct TestTypeWrapper {
98 static TestTypeWrapper& from(v8::Isolate*) {
99 KJ_UNIMPLEMENTED("not implemented");
100 }
101 v8::Local<v8::Value> wrap(jsg::Lock& lock,
102 v8::Local<v8::Context>,
103 kj::Maybe<v8::Local<v8::Object>>,
104 jsg::Ref<TestType>) {
105 KJ_UNIMPLEMENTED("not implemented");
106 }
107};
108 
109struct TestContext: public Object, public ContextGlobal {
110 JSG_RESOURCE_TYPE(TestContext) {}
111};
112JSG_DECLARE_ISOLATE_TYPE(TestIsolate, TestContext, TestType);
113 
114#define PREAMBLE(fn) \
115 TestIsolate isolate(v8System, v8::IsolateGroup::GetDefault(), 123, kj::heap<IsolateObserver>()); \
116 isolate.runInLockScope([&](auto& lock) { \
117 IsolateBase::from(lock.v8Isolate).setUsingNewModuleRegistry(); \
118 JSG_WITHIN_CONTEXT_SCOPE(lock, lock.template newContext<TestContext>().getHandle(lock), \
119 [&](jsg::Lock& js) { fn(lock); }); \
120 });
121 
122// ======================================================================================
123 
124KJ_TEST("An empty registry") {
125 // We should be able to create an empty registry that returns nothing.
126 // Basic resolution of this kind does not require an isolate lock.
127 
128 ResolveObserverImpl observer;
129 ModuleRegistry::Builder registryBuilder(observer, BASE);
130 auto registry = registryBuilder.finish();
131 KJ_ASSERT(registry.get() != nullptr);
132 
133 ResolveContext context = {
134 .type = ResolveContext::Type::BUNDLE,
135 .source = ResolveContext::Source::INTERNAL,
136 .normalizedSpecifier = "file:///foo"_url,
137 .referrerNormalizedSpecifier = BASE,
138 };
139 
140 KJ_ASSERT(registry->lookup(context) == kj::none);
141 
142 KJ_ASSERT(observer.modules.size() == 1);
143 KJ_ASSERT(observer.modules[0].found == false);
144}
145 
146// ======================================================================================
147 
148KJ_TEST("A empty fallback bundle") {
149 // We should be able to create an empty fallback bundle that returns nothing.
150 // Basic resolution of this kind does not require an isolate lock.
151 
152 bool called = false;
153 auto fallback = ModuleBundle::newFallbackBundle([&called](const ResolveContext& context) {
154 called = true;
155 return kj::none;
156 });
157 
158 ResolveContext context = {
159 .type = ResolveContext::Type::BUNDLE,
160 .source = ResolveContext::Source::INTERNAL,
161 .normalizedSpecifier = "file:///foo"_url,
162 .referrerNormalizedSpecifier = BASE,
163 };
164 
165 KJ_ASSERT(fallback->lookup(context) == kj::none);
166 KJ_ASSERT(called);
167}
168 
169// ======================================================================================
170 
171KJ_TEST("An empty user bundle") {
172 // We should be able to create an empty user bundle that returns nothing.
173 // Basic resolution of this kind does not require an isolate lock.
174 
175 ModuleBundle::BundleBuilder builder(BASE);
176 auto bundle = builder.finish();
177 
178 ResolveContext context = {
179 .type = ResolveContext::Type::BUNDLE,
180 .source = ResolveContext::Source::INTERNAL,
181 .normalizedSpecifier = "file:///foo"_url,
182 .referrerNormalizedSpecifier = BASE,
183 };
184 
185 KJ_ASSERT(bundle->lookup(context) == kj::none);
186}
187 
188// ======================================================================================
189 
190KJ_TEST("An empty built-in bundle") {
191 // We should be able to create an empty built-in bundle that returns nothing.
192 // Basic resolution of this kind does not require an isolate lock.
193 
194 ModuleBundle::BuiltinBuilder builder;
195 auto bundle = builder.finish();
196 
197 ResolveContext context = {
198 .type = ResolveContext::Type::BUNDLE,
199 .source = ResolveContext::Source::INTERNAL,
200 .normalizedSpecifier = "file:///foo"_url,
201 .referrerNormalizedSpecifier = BASE,
202 };
203 
204 KJ_ASSERT(bundle->lookup(context) == kj::none);
205}
206 
207// ======================================================================================
208 
209KJ_TEST("A registry with empty bundles") {
210 // We should be able to create a registry with empty bundles that return nothing.
211 // Basic resolution of this kind does not require an isolate lock.
212 
213 ResolveObserverImpl observer;
214 ModuleRegistry::Builder registryBuilder(
215 observer, BASE, ModuleRegistry::Builder::Options::ALLOW_FALLBACK);
216 
217 registryBuilder.add(
218 ModuleBundle::newFallbackBundle([](const ResolveContext& context) { return kj::none; }));
219 
220 ModuleBundle::BundleBuilder bundleBuilder(BASE);
221 registryBuilder.add(bundleBuilder.finish());
222 
223 ModuleBundle::BuiltinBuilder builtinBuilder;
224 registryBuilder.add(builtinBuilder.finish());
225 
226 auto registry = registryBuilder.finish();
227 
228 ResolveContext context = {
229 .type = ResolveContext::Type::BUNDLE,
230 .source = ResolveContext::Source::INTERNAL,
231 .normalizedSpecifier = "file:///foo"_url,
232 .referrerNormalizedSpecifier = BASE,
233 };
234 
235 KJ_ASSERT(registry->lookup(context) == kj::none);
236 KJ_ASSERT(observer.modules.size() == 1);
237 KJ_ASSERT(observer.modules[0].found == false);
238}
239 
240// ======================================================================================
241 
242KJ_TEST("A user bundle with a single ESM module") {
243 ModuleBundle::BundleBuilder builder(BASE);
244 
245 auto source = kj::str("export const foo = 123;");
246 builder.addEsmModule("foo", source, Module::Flags::MAIN);
247 
248 auto bundle = builder.finish();
249 
250 const auto id = "file:///foo"_url;
251 
252 ResolveContext context = {
253 .type = ResolveContext::Type::BUNDLE,
254 .source = ResolveContext::Source::INTERNAL,
255 .normalizedSpecifier = id,
256 .referrerNormalizedSpecifier = BASE,
257 };
258 
259 auto resolved = KJ_ASSERT_NONNULL(bundle->lookup(context));
260 auto& module = KJ_ASSERT_NONNULL(resolved.module);
261 
262 KJ_ASSERT(module.id() == id);
263 KJ_ASSERT(module.isEsm());
264 KJ_ASSERT(module.isMain());
265 KJ_ASSERT(module.type() == Module::Type::BUNDLE);
266}
267 
268// ======================================================================================
269 
270KJ_TEST("A user bundle with an ESM module and a Synthetic module") {
271 ModuleBundle::BundleBuilder builder(BASE);
272 
273 auto source = kj::str("export const foo = 123;");
274 builder.addEsmModule("foo", source, Module::Flags::MAIN);
275 builder.addSyntheticModule(
276 "foo/bar", [](Lock&, const Url&, const Module::ModuleNamespace&, const CompilationObserver&) {
277 return true;
278 });
279 
280 const auto foo = "file:///foo"_url;
281 const auto bar = "file:///foo/bar"_url;
282 
283 auto bundle = builder.finish();
284 
285 {
286 ResolveContext context = {
287 .type = ResolveContext::Type::BUNDLE,
288 .source = ResolveContext::Source::INTERNAL,
289 .normalizedSpecifier = foo,
290 .referrerNormalizedSpecifier = BASE,
291 };
292 
293 auto resolved = KJ_ASSERT_NONNULL(bundle->lookup(context));
294 auto& module = KJ_ASSERT_NONNULL(resolved.module);
295 
296 KJ_ASSERT(module.id() == foo);
297 KJ_ASSERT(module.isEsm());
298 KJ_ASSERT(module.isMain());
299 KJ_ASSERT(module.type() == Module::Type::BUNDLE);
300 }
301 
302 {
303 ResolveContext context = {
304 .type = ResolveContext::Type::BUNDLE,
305 .source = ResolveContext::Source::INTERNAL,
306 .normalizedSpecifier = bar,
307 .referrerNormalizedSpecifier = BASE,
308 };
309 
310 auto resolved = KJ_ASSERT_NONNULL(bundle->lookup(context));
311 auto& module = KJ_ASSERT_NONNULL(resolved.module);
312 
313 KJ_ASSERT(module.id() == bar);
314 KJ_ASSERT(!module.isEsm());
315 KJ_ASSERT(!module.isMain());
316 KJ_ASSERT(module.type() == Module::Type::BUNDLE);
317 }
318}
319 
320// ======================================================================================
321 
322KJ_TEST("A built-in bundle with two modules") {
323 ResolveObserverImpl observer;
324 ModuleRegistry::Builder registryBuilder(observer, BASE);
325 
326 ModuleBundle::BuiltinBuilder builder;
327 
328 const auto foo = "foo:bar"_url;
329 const auto bar = "bar:baz"_url;
330 auto source = "export const foo = 123;"_kjc;
331 builder.addEsm(foo, source.asArray());
332 
333 struct W {
334 static W& from(v8::Isolate*) {
335 static W w;
336 return w;
337 }
338 v8::Local<v8::Value> wrap(jsg::Lock& lock,
339 v8::Local<v8::Context>,
340 kj::Maybe<v8::Local<v8::Object>>,
341 jsg::Ref<TestType>) {
342 return v8::Local<v8::Value>();
343 }
344 };
345 builder.addObject<TestType, W>(bar);
346 
347 auto registry = registryBuilder.add(builder.finish()).finish();
348 
349 {
350 ResolveContext context = {
351 .type = ResolveContext::Type::BUNDLE,
352 .source = ResolveContext::Source::INTERNAL,
353 .normalizedSpecifier = foo,
354 .referrerNormalizedSpecifier = foo,
355 };
356 
357 auto& module = KJ_ASSERT_NONNULL(registry->lookup(context));
358 
359 KJ_ASSERT(module.id() == foo);
360 KJ_ASSERT(module.isEsm());
361 KJ_ASSERT(!module.isMain());
362 KJ_ASSERT(module.type() == Module::Type::BUILTIN);
363 }
364 
365 {
366 ResolveContext context = {
367 .type = ResolveContext::Type::BUNDLE,
368 .source = ResolveContext::Source::INTERNAL,
369 .normalizedSpecifier = bar,
370 .referrerNormalizedSpecifier = bar,
371 };
372 
373 auto& module = KJ_ASSERT_NONNULL(registry->lookup(context));
374 
375 KJ_ASSERT(module.id() == bar);
376 KJ_ASSERT(!module.isEsm());
377 KJ_ASSERT(!module.isMain());
378 KJ_ASSERT(module.type() == Module::Type::BUILTIN);
379 }
380 
381 KJ_ASSERT(observer.modules.size() == 2);
382 KJ_ASSERT(observer.modules[0].id == foo);
383 KJ_ASSERT(observer.modules[1].id == bar);
384}
385 
386// ======================================================================================
387 
388KJ_TEST("Built-in and Built-in only bundles") {
389 ResolveObserverImpl observer;
390 ModuleRegistry::Builder registryBuilder(observer, BASE);
391 
392 ModuleBundle::BuiltinBuilder builtinBuilder;
393 ModuleBundle::BuiltinBuilder builtinOnlyBuilder(ModuleBundle::BuiltinBuilder::Type::BUILTIN_ONLY);
394 
395 const auto foo = "foo:bar"_url;
396 const auto bar = "bar:baz"_url;
397 auto source = "export const foo = 123;"_kjc;
398 builtinBuilder.addEsm(foo, source.asArray());
399 
400 builtinOnlyBuilder.addObject<TestType, TestTypeWrapper>(bar);
401 
402 auto registry =
403 registryBuilder.add(builtinBuilder.finish()).add(builtinOnlyBuilder.finish()).finish();
404 
405 {
406 ResolveContext context = {
407 .type = ResolveContext::Type::BUNDLE,
408 .source = ResolveContext::Source::INTERNAL,
409 .normalizedSpecifier = foo,
410 .referrerNormalizedSpecifier = foo,
411 };
412 
413 auto& module = KJ_ASSERT_NONNULL(registry->lookup(context));
414 
415 KJ_ASSERT(module.id() == foo);
416 KJ_ASSERT(module.isEsm());
417 KJ_ASSERT(!module.isMain());
418 KJ_ASSERT(module.type() == Module::Type::BUILTIN);
419 }
420 
421 {
422 ResolveContext context = {
423 .type = ResolveContext::Type::BUNDLE,
424 .source = ResolveContext::Source::INTERNAL,
425 .normalizedSpecifier = bar,
426 .referrerNormalizedSpecifier = bar,
427 };
428 
429 // Built-in only modules cannot be resolved from a bundle context.
430 KJ_ASSERT(registry->lookup(context) == kj::none);
431 }
432 
433 {
434 ResolveContext context = {
435 .type = ResolveContext::Type::BUILTIN,
436 .source = ResolveContext::Source::INTERNAL,
437 .normalizedSpecifier = bar,
438 .referrerNormalizedSpecifier = bar,
439 };
440 
441 auto& module = KJ_ASSERT_NONNULL(registry->lookup(context));
442 
443 KJ_ASSERT(module.id() == bar);
444 KJ_ASSERT(!module.isEsm());
445 KJ_ASSERT(!module.isMain());
446 KJ_ASSERT(module.type() == Module::Type::BUILTIN_ONLY);
447 }
448 
449 {
450 ResolveContext context = {
451 .type = ResolveContext::Type::BUILTIN_ONLY,
452 .source = ResolveContext::Source::INTERNAL,
453 .normalizedSpecifier = bar,
454 .referrerNormalizedSpecifier = bar,
455 };
456 
457 auto& module = KJ_ASSERT_NONNULL(registry->lookup(context));
458 
459 KJ_ASSERT(module.id() == bar);
460 KJ_ASSERT(!module.isEsm());
461 KJ_ASSERT(!module.isMain());
462 KJ_ASSERT(module.type() == Module::Type::BUILTIN_ONLY);
463 }
464}
465 
466// ======================================================================================
467 
468KJ_TEST("Built-in modules cannot use file:") {
469 ModuleBundle::BuiltinBuilder builder;
470 const auto foo = "file:///foo"_url;
471 auto source = "export const foo = 123;"_kjc;
472 
473 try {
474 builder.addEsm(foo, source.asArray());
475 KJ_FAIL_ASSERT("Expected an exception");
476 } catch (kj::Exception& exception) {
477 KJ_ASSERT(exception.getDescription().endsWith(
478 "The file: protocol is reserved for bundle type modules"_kjc));
479 }
480}
481 
482// ======================================================================================
483 
484KJ_TEST("Fallback bundle that returns something") {
485 auto fallback = ModuleBundle::newFallbackBundle([](const ResolveContext& context) {
486 kj::Own<Module> mod = Module::newSynthetic("file:///foo"_url, Module::Type::FALLBACK,
487 [](Lock&, const Url&, const Module::ModuleNamespace&, const CompilationObserver&) -> bool {
488 KJ_FAIL_ASSERT("Should not be called");
489 });
490 return kj::Maybe<kj::OneOf<kj::String, kj::Own<Module>>>(kj::mv(mod));
491 });
492 
493 ResolveObserverImpl observer;
494 ModuleRegistry::Builder registryBuilder(
495 observer, BASE, ModuleRegistry::Builder::Options::ALLOW_FALLBACK);
496 auto registry = registryBuilder.add(kj::mv(fallback)).finish();
497 
498 const auto id = "file:///foo"_url;
499 
500 {
501 ResolveContext context{
502 .type = ResolveContext::Type::BUNDLE,
503 .source = ResolveContext::Source::INTERNAL,
504 .normalizedSpecifier = id,
505 .referrerNormalizedSpecifier = BASE,
506 };
507 
508 auto& module = KJ_ASSERT_NONNULL(registry->lookup(context));
509 KJ_ASSERT(module.id() == id);
510 KJ_ASSERT(module.type() == Module::Type::FALLBACK);
511 KJ_ASSERT(!module.isEsm());
512 }
513 
514 // Built-in and built-in only contexts do not use the fallback
515 {
516 ResolveContext context{
517 .type = ResolveContext::Type::BUILTIN,
518 .source = ResolveContext::Source::INTERNAL,
519 .normalizedSpecifier = id,
520 .referrerNormalizedSpecifier = BASE,
521 };
522 
523 KJ_ASSERT(registry->lookup(context) == kj::none);
524 }
525 
526 {
527 ResolveContext context{
528 .type = ResolveContext::Type::BUILTIN_ONLY,
529 .source = ResolveContext::Source::INTERNAL,
530 .normalizedSpecifier = id,
531 .referrerNormalizedSpecifier = BASE,
532 };
533 
534 KJ_ASSERT(registry->lookup(context) == kj::none);
535 }
536}
537 
538// ======================================================================================
539 
540KJ_TEST("Duplicate module names in a single are caught and throw properly") {
541 ModuleBundle::BundleBuilder builder(BASE);
542 builder.addSyntheticModule(
543 "foo", [](Lock&, const Url&, const Module::ModuleNamespace&, const CompilationObserver&) {
544 return true;
545 });
546 try {
547 builder.addSyntheticModule(
548 "foo", [](Lock&, const Url&, const Module::ModuleNamespace&, const CompilationObserver&) {
549 return true;
550 });
551 KJ_FAIL_ASSERT("Expected an exception");
552 } catch (kj::Exception& exception) {
553 KJ_ASSERT(exception.getDescription() == "Module \"file:///foo\" already added to bundle"_kjc);
554 }
555}
556 
557// ======================================================================================
558 
559KJ_TEST("Fallback bundles are not permitted in production") {
560 ResolveObserverImpl observer;
561 ModuleRegistry::Builder registryBuilder(observer, BASE);
562 try {
563 registryBuilder.add(ModuleBundle::newFallbackBundle([](const ResolveContext& context) {
564 kj::Own<Module> mod =
565 Module::newSynthetic(context.normalizedSpecifier.clone(), Module::Type::FALLBACK,
566 [](Lock&, const Url&, const Module::ModuleNamespace&,
567 const CompilationObserver&) -> bool { KJ_FAIL_ASSERT("Should not be called"); });
568 return kj::Maybe<kj::OneOf<kj::String, kj::Own<Module>>>(kj::mv(mod));
569 }));
570 KJ_FAIL_ASSERT("Expected an exception");
571 } catch (kj::Exception& exception) {
572 KJ_ASSERT(exception.getDescription().endsWith(
573 "Fallback bundle types are not allowed for this registry"_kjc));
574 }
575}
576 
577// ======================================================================================
578 
579KJ_TEST("Compound Registry") {
580 ResolveObserverImpl observer;
581 ModuleRegistry::Builder registryBuilder(
582 observer, BASE, ModuleRegistry::Builder::Options::ALLOW_FALLBACK);
583 
584 const auto foo = "foo:bar"_url; // Fallback
585 const auto bar = "bar:baz"_url; // Built-in
586 const auto baz = "abc:xyz"_url; // Built-in only
587 const auto qux = "file:///qux"_url; // Bundle
588 
589 registryBuilder.add(ModuleBundle::newFallbackBundle(
590 [&](const ResolveContext& context) -> kj::Maybe<kj::OneOf<kj::String, kj::Own<Module>>> {
591 if (context.normalizedSpecifier != foo) return kj::none;
592 kj::Own<Module> mod = Module::newSynthetic(foo.clone(), Module::Type::FALLBACK,
593 [](Lock&, const Url&, const Module::ModuleNamespace&, const CompilationObserver&) -> bool {
594 KJ_FAIL_ASSERT("should not have been called");
595 });
596 return kj::Maybe<kj::OneOf<kj::String, kj::Own<Module>>>(kj::mv(mod));
597 }));
598 
599 ModuleBundle::BuiltinBuilder builtinBuilder;
600 auto barSource = "export const foo = 123;"_kjc;
601 builtinBuilder.addEsm(bar, barSource.asArray());
602 registryBuilder.add(builtinBuilder.finish());
603 
604 ModuleBundle::BuiltinBuilder builtinOnlyBuilder(ModuleBundle::BuiltinBuilder::Type::BUILTIN_ONLY);
605 builtinOnlyBuilder.addObject<TestType, TestTypeWrapper>(baz);
606 registryBuilder.add(builtinOnlyBuilder.finish());
607 
608 ModuleBundle::BundleBuilder bundleBuilder(BASE);
609 auto quxSource = kj::str("export const foo = 123;");
610 bundleBuilder.addEsmModule("qux", quxSource, Module::Flags::MAIN);
611 registryBuilder.add(bundleBuilder.finish());
612 
613 auto registry = registryBuilder.finish();
614 
615 constexpr auto resolve = [](const auto& registry, ResolveContext::Type type, const Url& id) {
616 ResolveContext context{
617 .type = type,
618 .source = ResolveContext::Source::INTERNAL,
619 .normalizedSpecifier = id,
620 .referrerNormalizedSpecifier = BASE,
621 };
622 return registry->lookup(context);
623 };
624 
625 {
626 // The fallback module is resolved when using a bundle context
627 auto& module = KJ_ASSERT_NONNULL(resolve(registry, ResolveContext::Type::BUNDLE, foo));
628 KJ_ASSERT(module.id() == foo);
629 KJ_ASSERT(module.type() == Module::Type::FALLBACK);
630 KJ_ASSERT(!module.isEsm());
631 KJ_ASSERT(!module.isMain());
632 }
633 
634 {
635 // A built-in module is resolved when using a bundle context
636 auto& module = KJ_ASSERT_NONNULL(resolve(registry, ResolveContext::Type::BUNDLE, bar));
637 KJ_ASSERT(module.id() == bar);
638 KJ_ASSERT(module.type() == Module::Type::BUILTIN);
639 KJ_ASSERT(module.isEsm());
640 KJ_ASSERT(!module.isMain());
641 }
642 
643 {
644 // A bundle module is resolved when using a bundle context
645 auto& module = KJ_ASSERT_NONNULL(resolve(registry, ResolveContext::Type::BUNDLE, qux));
646 KJ_ASSERT(module.id() == qux);
647 KJ_ASSERT(module.type() == Module::Type::BUNDLE);
648 KJ_ASSERT(module.isEsm());
649 KJ_ASSERT(module.isMain());
650 }
651 
652 {
653 // A built-in module is resolved when using a builtin context
654 auto& module = KJ_ASSERT_NONNULL(resolve(registry, ResolveContext::Type::BUILTIN, bar));
655 KJ_ASSERT(module.id() == bar);
656 KJ_ASSERT(module.type() == Module::Type::BUILTIN);
657 KJ_ASSERT(module.isEsm());
658 KJ_ASSERT(!module.isMain());
659 }
660 
661 {
662 // A built-in only module is resolved when using a built-in context
663 auto& module = KJ_ASSERT_NONNULL(resolve(registry, ResolveContext::Type::BUILTIN, baz));
664 KJ_ASSERT(module.id() == baz);
665 KJ_ASSERT(module.type() == Module::Type::BUILTIN_ONLY);
666 KJ_ASSERT(!module.isEsm());
667 KJ_ASSERT(!module.isMain());
668 }
669 
670 {
671 // A built-in only module is resolved when using a built-in only context
672 auto& module = KJ_ASSERT_NONNULL(resolve(registry, ResolveContext::Type::BUILTIN_ONLY, baz));
673 KJ_ASSERT(module.id() == baz);
674 KJ_ASSERT(module.type() == Module::Type::BUILTIN_ONLY);
675 KJ_ASSERT(!module.isEsm());
676 KJ_ASSERT(!module.isMain());
677 }
678 
679 // A built-in only module cannot be resolved from a bundle context
680 KJ_ASSERT(resolve(registry, ResolveContext::Type::BUNDLE, baz) == kj::none);
681 
682 // Fallback modules cannot be resolved from a built-in context
683 KJ_ASSERT(resolve(registry, ResolveContext::Type::BUILTIN, foo) == kj::none);
684 KJ_ASSERT(resolve(registry, ResolveContext::Type::BUILTIN_ONLY, foo) == kj::none);
685 
686 // Bundle modules cannot be resolved from a built-in or built-in only context
687 KJ_ASSERT(resolve(registry, ResolveContext::Type::BUILTIN, qux) == kj::none);
688 KJ_ASSERT(resolve(registry, ResolveContext::Type::BUILTIN_ONLY, qux) == kj::none);
689 
690 // We should have seen eleven distinct resolution events.
691 KJ_ASSERT(observer.modules.size() == 11);
692}
693 
694// ======================================================================================
695 
696KJ_TEST("Bundle shadows built-in") {
697 // A bundle module can shadow a built-in
698 ResolveObserverImpl observer;
699 ModuleRegistry::Builder registryBuilder(observer, BASE);
700 
701 const auto foo = "foo:bar"_url;
702 
703 ModuleBundle::BuiltinBuilder builtinBuilder;
704 auto source = "export const foo = 123;"_kjc;
705 builtinBuilder.addEsm(foo, source.asArray());
706 registryBuilder.add(builtinBuilder.finish());
707 
708 ModuleBundle::BundleBuilder bundleBuilder(BASE);
709 auto bundleSource = kj::str("export const foo = 456;");
710 bundleBuilder.addEsmModule("foo:bar", bundleSource, Module::Flags::MAIN);
711 registryBuilder.add(bundleBuilder.finish());
712 
713 auto registry = registryBuilder.finish();
714 
715 ResolveContext context{
716 .type = ResolveContext::Type::BUNDLE,
717 .source = ResolveContext::Source::INTERNAL,
718 .normalizedSpecifier = foo,
719 .referrerNormalizedSpecifier = BASE,
720 };
721 
722 auto& module = KJ_ASSERT_NONNULL(registry->lookup(context));
723 KJ_ASSERT(module.id() == foo);
724 KJ_ASSERT(module.type() == Module::Type::BUNDLE);
725 KJ_ASSERT(module.isEsm());
726 KJ_ASSERT(module.isMain());
727}
728 
729// ======================================================================================
730 
731KJ_TEST("Attaching a module registry works") {
732 PREAMBLE(([&](Lock& js) {
733 ResolveObserver resolveObserver;
734 CompilationObserver compilationObserver;
735 ModuleRegistry::Builder registryBuilder(resolveObserver, BASE);
736 
737 ModuleBundle::BundleBuilder bundleBuilder(BASE);
738 auto source = kj::str("export default 123; export const m = 'abc';");
739 // Done this way to avoid including the nullptr at the end...
740 bundleBuilder.addEsmModule("main", source);
741 
742 auto mainSource = kj::str("import foo from 'main'; export default foo;");
743 bundleBuilder.addEsmModule("worker1", mainSource.first(mainSource.size()), Module::Flags::MAIN);
744 
745 registryBuilder.add(bundleBuilder.finish());
746 
747 auto registry = registryBuilder.finish();
748 
749 const auto id = "file:///main"_url;
750 
751 ResolveContext resolveContext{
752 .type = ResolveContext::Type::BUNDLE,
753 .source = ResolveContext::Source::INTERNAL,
754 .normalizedSpecifier = id,
755 .referrerNormalizedSpecifier = BASE,
756 };
757 KJ_ASSERT(registry->lookup(resolveContext) != kj::none);
758 
759 auto attached = registry->attachToIsolate(js, compilationObserver);
760 
761 js.tryCatch([&] {
762 auto val = ModuleRegistry::resolve(js, "file:///worker1");
763 KJ_ASSERT(val.isNumber());
764 }, [&](Value exception) { js.throwException(kj::mv(exception)); });
765 
766 js.tryCatch([&] {
767 auto val = ModuleRegistry::resolve(js, "worker1");
768 KJ_ASSERT(val.isNumber());
769 }, [&](Value exception) { js.throwException(kj::mv(exception)); });
770 
771 js.tryCatch([&] {
772 auto val = ModuleRegistry::resolve(js, "./.././../worker1");
773 KJ_ASSERT(val.isNumber());
774 }, [&](Value exception) { js.throwException(kj::mv(exception)); });
775 
776 js.tryCatch([&] {
777 auto val = ModuleRegistry::resolve(js, "file:///main", "m"_kjc);
778 KJ_ASSERT(val.isString());
779 }, [&](Value exception) { js.throwException(kj::mv(exception)); });
780 }));
781}
782 
783// ======================================================================================
784 
785KJ_TEST("Basic types of modules work (text, data, json, wasm)") {
786 PREAMBLE(([&](Lock& js) {
787 ResolveObserver resolveObserver;
788 CompilationObserver compilationObserver;
789 ModuleRegistry::Builder registryBuilder(resolveObserver, BASE);
790 
791 ModuleBundle::BundleBuilder bundleBuilder(BASE);
792 auto abcSource = kj::str("hello");
793 auto xyzData = kj::heapArray<kj::byte>({1, 2, 3});
794 bundleBuilder.addSyntheticModule("abc", Module::newTextModuleHandler(abcSource));
795 bundleBuilder.addSyntheticModule("xyz", Module::newDataModuleHandler(xyzData));
796 
797 auto json = kj::str("{\"foo\":123}");
798 bundleBuilder.addSyntheticModule("json", Module::newJsonModuleHandler(json.first(json.size())));
799 
800 auto wasm = kj::heapArray<kj::byte>({
801 0x00,
802 0x61,
803 0x73,
804 0x6d,
805 0x01,
806 0x00,
807 0x00,
808 0x00,
809 0x01,
810 0x07,
811 0x01,
812 0x60,
813 0x02,
814 0x7f,
815 0x7f,
816 0x01,
817 0x7f,
818 0x03,
819 0x02,
820 0x01,
821 0x00,
822 0x07,
823 0x07,
824 0x01,
825 0x03,
826 0x61,
827 0x64,
828 0x64,
829 0x00,
830 0x00,
831 0x0a,
832 0x09,
833 0x01,
834 0x07,
835 0x00,
836 0x20,
837 0x00,
838 0x20,
839 0x01,
840 0x6a,
841 0x0b,
842 });
843 bundleBuilder.addSyntheticModule("wasm", Module::newWasmModuleHandler(wasm));
844 
845 auto mainSource2 = kj::str("export { default as abc } from 'abc';"
846 "export { default as xyz } from 'xyz';"
847 "export { default as json } from 'json';"
848 "export { default as wasm } from 'wasm';"
849 "export { default as wasm2 } from 'wasm?a';");
850 
851 bundleBuilder.addEsmModule("worker", mainSource2, Module::Flags::MAIN);
852 
853 registryBuilder.add(bundleBuilder.finish());
854 
855 auto registry = registryBuilder.finish();
856 
857 const auto id = "file:///worker"_url;
858 
859 ResolveContext resolveContext{
860 .type = ResolveContext::Type::BUNDLE,
861 .source = ResolveContext::Source::INTERNAL,
862 .normalizedSpecifier = id,
863 .referrerNormalizedSpecifier = BASE,
864 };
865 auto& resolved KJ_UNUSED = KJ_ASSERT_NONNULL(registry->lookup(resolveContext));
866 
867 auto attached = registry->attachToIsolate(js, compilationObserver);
868 
869 js.tryCatch([&] {
870 auto val = ModuleRegistry::resolve(js, "file:///worker", "abc"_kjc);
871 KJ_ASSERT(val.isString());
872 KJ_ASSERT(kj::str(val) == "hello"_kjc);
873 }, [&](Value exception) { js.throwException(kj::mv(exception)); });
874 
875 js.tryCatch([&] {
876 auto val = ModuleRegistry::resolve(js, "file:///worker", "xyz"_kjc);
877 KJ_ASSERT(val.isArrayBuffer());
878 }, [&](Value exception) { js.throwException(kj::mv(exception)); });
879 
880 js.tryCatch([&] {
881 auto val1 = ModuleRegistry::resolve(js, "file:///worker", "json"_kjc);
882 auto val2 = ModuleRegistry::resolve(js, "file:///json", "default"_kjc);
883 KJ_ASSERT(val1.isObject());
884 KJ_ASSERT(val2.isObject());
885 KJ_ASSERT(val1.strictEquals(val2));
886 auto obj = KJ_ASSERT_NONNULL(val1.tryCast<JsObject>());
887 KJ_ASSERT(obj.get(js, "foo").isNumber());
888 }, [&](Value exception) { js.throwException(kj::mv(exception)); });
889 
890 js.tryCatch([&] {
891 auto wasm1 = ModuleRegistry::resolve(js, "file:///worker", "wasm"_kjc);
892 auto wasm2 = ModuleRegistry::resolve(js, "file:///wasm", "default"_kjc);
893 auto wasm3 = ModuleRegistry::resolve(js, "file:///worker", "wasm2"_kjc);
894 KJ_ASSERT(wasm1.isWasmModuleObject());
895 KJ_ASSERT(wasm2.isWasmModuleObject());
896 KJ_ASSERT(wasm3.isWasmModuleObject());
897 KJ_ASSERT(wasm1.strictEquals(wasm2));
898 KJ_ASSERT(!wasm1.strictEquals(wasm3));
899 }, [&](Value exception) { js.throwException(kj::mv(exception)); });
900 }));
901}
902 
903// ======================================================================================
904 
905KJ_TEST("compileEvalFunction in synthetic module works") {
906 PREAMBLE([&](Lock& js) {
907 CompilationObserver compilationObserver;
908 ResolveObserver resolveObserver;
909 ModuleBundle::BundleBuilder bundleBuilder(BASE);
910 bundleBuilder.addSyntheticModule("abc",
911 [](Lock& js, const Url& id, const Module::ModuleNamespace& ns,
912 const CompilationObserver& observer) mutable -> bool {
913 // The compileEvalFunction is used in CommonJs/Node.js compat modules to
914 // evaluate the module as a function rather than as an ESM. This test just
915 // verifies that compileEvalFunction works as expected.
916 auto ext = js.alloc<TestType>(js, id);
917 auto& wrapper = TestIsolate_TypeWrapper::from(js.v8Isolate);
918 auto fn = Module::compileEvalFunction(js, "bar(123);"_kj, "foo"_kj,
919 JsObject(wrapper.wrap(js, js.v8Context(), kj::none, ext.addRef())), observer);
920 return js.tryCatch([&] {
921 fn(js);
922 KJ_ASSERT(ext->barCalled);
923 return ns.setDefault(js, js.num(123));
924 }, [&](Value exception) {
925 js.v8Isolate->ThrowException(exception.getHandle(js));
926 return false;
927 });
928 });
929 
930 auto source = kj::str("import 'abc'");
931 bundleBuilder.addEsmModule("main", source, Module::Flags::MAIN);
932 
933 auto registry =
934 ModuleRegistry::Builder(resolveObserver, BASE).add(bundleBuilder.finish()).finish();
935 
936 auto attached = registry->attachToIsolate(js, compilationObserver);
937 
938 js.tryCatch([&] {
939 auto val = ModuleRegistry::resolve(js, "file:///main");
940 KJ_ASSERT(val.isUndefined());
941 }, [&](Value exception) { js.throwException(kj::mv(exception)); });
942 });
943}
944 
945// ======================================================================================
946 
947KJ_TEST("import.meta works as expected") {
948 PREAMBLE([&](Lock& js) {
949 ResolveObserver ResolveObserver;
950 CompilationObserver compilationObserver;
951 
952 ModuleBundle::BundleBuilder bundleBuilder(BASE);
953 auto foo = kj::str("export default import.meta");
954 bundleBuilder.addEsmModule("foo", foo);
955 auto bar = kj::str("export default import.meta");
956 bundleBuilder.addEsmModule("foo/././././bar", bar, Module::Flags::MAIN);
957 auto registry =
958 ModuleRegistry::Builder(ResolveObserver, BASE).add(bundleBuilder.finish()).finish();
959 
960 auto attached = registry->attachToIsolate(js, compilationObserver);
961 
962 js.tryCatch([&] {
963 auto val = ModuleRegistry::resolve(js, "file:///foo");
964 KJ_ASSERT(val.isObject());
965 auto obj = KJ_ASSERT_NONNULL(val.tryCast<JsObject>());
966 JsValue url = obj.get(js, "url");
967 JsValue main = obj.get(js, "main");
968 JsValue res = obj.get(js, "resolve");
969 
970 KJ_ASSERT(url.isString());
971 KJ_ASSERT(main.isBoolean());
972 KJ_ASSERT(res.isFunction());
973 
974 KJ_ASSERT(url.toString(js) == "file:///foo"_kj);
975 
976 auto mainVal = KJ_ASSERT_NONNULL(main.tryCast<JsBoolean>());
977 KJ_ASSERT(!mainVal.value(js));
978 
979 auto& wrapper = TestIsolate_TypeWrapper::from(js.v8Isolate);
980 KJ_IF_SOME(fn,
981 wrapper.tryUnwrap(
982 js, js.v8Context(), res, (Function<kj::String(kj::String)>*)nullptr, kj::none)) {
983 KJ_ASSERT(fn(js, kj::str("foo/bar")) == "file:///foo/bar"_kj);
984 } else {
985 }
986 }, [&](Value exception) { js.throwException(kj::mv(exception)); });
987 
988 js.tryCatch([&] {
989 auto val = ModuleRegistry::resolve(js, "file:///foo/bar");
990 KJ_ASSERT(val.isObject());
991 auto obj = KJ_ASSERT_NONNULL(val.tryCast<JsObject>());
992 JsValue url = obj.get(js, "url");
993 JsValue main = obj.get(js, "main");
994 JsValue res = obj.get(js, "resolve");
995 
996 KJ_ASSERT(url.isString());
997 KJ_ASSERT(main.isBoolean());
998 KJ_ASSERT(res.isFunction());
999 
1000 KJ_ASSERT(url.toString(js) == "file:///foo/bar"_kj);
1001 
1002 auto mainVal = KJ_ASSERT_NONNULL(main.tryCast<JsBoolean>());
1003 KJ_ASSERT(mainVal.value(js));
1004 }, [&](Value exception) { js.throwException(kj::mv(exception)); });
1005 });
1006}
1007 
1008// ======================================================================================
1009 
1010KJ_TEST("import specifiers with query params and hash fragments work") {
1011 // If we have two imports with the same base specifier URL
1012 // but different query params or hash fragments, they should
1013 // resolve to the same underlying Module but get evaluated
1014 // separately. This means the EvaluationCallback can be called
1015 // multiple times.
1016 
1017 PREAMBLE([&](jsg::Lock& js) {
1018 ResolveObserver ResolveObserver;
1019 CompilationObserver compilationObserver;
1020 
1021 ModuleBundle::BundleBuilder bundleBuilder(BASE);
1022 auto foo = kj::str("export default import.meta");
1023 bundleBuilder.addEsmModule("foo", foo);
1024 
1025 auto registry =
1026 ModuleRegistry::Builder(ResolveObserver, BASE).add(bundleBuilder.finish()).finish();
1027 
1028 auto attached = registry->attachToIsolate(js, compilationObserver);
1029 
1030 js.tryCatch([&] {
1031 auto val1 = ModuleRegistry::resolve(js, "file:///foo?1");
1032 auto val2 = ModuleRegistry::resolve(js, "file:///foo?2");
1033 auto val3 = ModuleRegistry::resolve(js, "file:///foo#1");
1034 auto val4 = ModuleRegistry::resolve(js, "file:///foo#2");
1035 
1036 KJ_ASSERT(val1.isObject());
1037 KJ_ASSERT(val2.isObject());
1038 KJ_ASSERT(val3.isObject());
1039 KJ_ASSERT(val4.isObject());
1040 KJ_ASSERT(!val1.strictEquals(val2));
1041 KJ_ASSERT(!val2.strictEquals(val3));
1042 KJ_ASSERT(!val3.strictEquals(val4));
1043 KJ_ASSERT(!val4.strictEquals(val1));
1044 
1045 auto obj = KJ_ASSERT_NONNULL(val1.tryCast<JsObject>());
1046 auto url = obj.get(js, "url");
1047 KJ_ASSERT(url.isString());
1048 // The import.meta.url should include the query param and hash fragment
1049 KJ_ASSERT(url.toString(js) == "file:///foo?1"_kj);
1050 }, [&](Value exception) { js.throwException(kj::mv(exception)); });
1051 });
1052}
1053 
1054// ======================================================================================
1055 
1056KJ_TEST("Previously resolved modules not found with incompatible resolve context") {
1057 // If we have a built-in only module that is resolved with a built-in context, that
1058 // should not be found when later resolving with a bundle context.
1059 
1060 PREAMBLE([&](Lock& js) {
1061 ResolveObserverImpl observer;
1062 CompilationObserver compilationObserver;
1063 
1064 ModuleBundle::BuiltinBuilder builtinBuilder(ModuleBundle::BuiltinBuilder::Type::BUILTIN_ONLY);
1065 const auto foo = "foo:bar"_url;
1066 
1067 auto source = "export default 123;"_kjc;
1068 builtinBuilder.addEsm(foo, source.first(source.size()).attach(kj::mv(source)));
1069 
1070 auto barData = kj::heapArray<kj::byte>({1, 2, 3});
1071 
1072 ModuleBundle::BundleBuilder bundleBuilder(BASE);
1073 bundleBuilder.addSyntheticModule("bar", Module::newDataModuleHandler(barData));
1074 
1075 auto registry = ModuleRegistry::Builder(observer, BASE)
1076 .add(builtinBuilder.finish())
1077 .add(bundleBuilder.finish())
1078 .finish();
1079 
1080 auto attached = registry->attachToIsolate(js, compilationObserver);
1081 
1082 js.tryCatch([&] {
1083 // The built-in only foo:bar module should be found when using a built-in context
1084 auto value1 =
1085 ModuleRegistry::resolve(js, "foo:bar", "default"_kjc, ResolveContext::Type::BUILTIN);
1086 
1087 KJ_ASSERT(value1.isNumber());
1088 
1089 // But since the module is an built-in only. it should not be found when
1090 // resolving with a bundle context.
1091 ModuleRegistry::resolve(js, "foo:bar", "default"_kjc, ResolveContext::Type::BUNDLE);
1092 JSG_FAIL_REQUIRE(Error, "Should have thrown");
1093 }, [&](Value exception) {
1094 auto str = kj::str(exception.getHandle(js));
1095 KJ_ASSERT(str == "Error: Module not found: foo:bar");
1096 });
1097 
1098 // Likewise, the bar module should be found when using a bundle context
1099 js.tryCatch([&] {
1100 auto value2 =
1101 ModuleRegistry::resolve(js, "file:///bar", "default"_kjc, ResolveContext::Type::BUNDLE);
1102 KJ_ASSERT(value2.isArrayBuffer());
1103 
1104 // But should not be found from a built-in context
1105 ModuleRegistry::resolve(js, "file:///bar", "default"_kjc, ResolveContext::Type::BUILTIN);
1106 JSG_FAIL_REQUIRE(Error, "Should have thrown");
1107 }, [&](Value exception) {
1108 auto str = kj::str(exception.getHandle(js));
1109 KJ_ASSERT(str == "Error: Module not found: file:///bar");
1110 });
1111 });
1112}
1113 
1114// ======================================================================================
1115 
1116KJ_TEST("Awaiting top-level dynamic import in synchronous require works as expected") {
1117 PREAMBLE([&](Lock& js) {
1118 ResolveObserverImpl observer;
1119 CompilationObserver compilationObserver;
1120 
1121 ModuleBundle::BundleBuilder bundleBuilder(BASE);
1122 auto foo = kj::str("export default (await import('bar')).default;");
1123 bundleBuilder.addEsmModule("foo", foo);
1124 
1125 auto bar = kj::str("export default 123;");
1126 bundleBuilder.addEsmModule("bar", bar);
1127 
1128 auto registry = ModuleRegistry::Builder(observer, BASE).add(bundleBuilder.finish()).finish();
1129 
1130 auto attached = registry->attachToIsolate(js, compilationObserver);
1131 
1132 ModuleRegistry::resolve(js, "file:///foo", "default"_kjc);
1133 });
1134}
1135 
1136// ======================================================================================
1137 
1138KJ_TEST("Awaiting a never resolved promise in synchronous require fails as expected") {
1139 PREAMBLE([&](Lock& js) {
1140 ResolveObserverImpl observer;
1141 CompilationObserver compilationObserver;
1142 
1143 ModuleBundle::BundleBuilder bundleBuilder(BASE);
1144 auto foo = kj::str("const p = new Promise(() => {}); await p;");
1145 bundleBuilder.addEsmModule("foo", foo);
1146 
1147 auto registry = ModuleRegistry::Builder(observer, BASE).add(bundleBuilder.finish()).finish();
1148 
1149 auto attached = registry->attachToIsolate(js, compilationObserver);
1150 
1151 js.tryCatch([&] {
1152 ModuleRegistry::resolve(js, "file:///foo", "default"_kjc);
1153 KJ_FAIL_ASSERT("Should have failed");
1154 }, [&](Value exception) {
1155 auto str = kj::str(exception.getHandle(js));
1156 KJ_ASSERT(str ==
1157 "Error: Use of top-level await in a synchronously "
1158 "required module is restricted to promises that are resolved "
1159 "synchronously. This includes any top-level awaits in the "
1160 "entrypoint module for a worker. Specifier: \"file:///foo\".");
1161 });
1162 });
1163}
1164 
1165// ======================================================================================
1166 
1167KJ_TEST("Throwing an exception inside a ESM module works as expected") {
1168 PREAMBLE([&](Lock& js) {
1169 ResolveObserverImpl observer;
1170 CompilationObserver compilationObserver;
1171 
1172 ModuleBundle::BundleBuilder bundleBuilder(BASE);
1173 auto foo = kj::str("throw new Error('foo');");
1174 bundleBuilder.addEsmModule("foo", foo);
1175 
1176 auto registry = ModuleRegistry::Builder(observer, BASE).add(bundleBuilder.finish()).finish();
1177 
1178 auto attached = registry->attachToIsolate(js, compilationObserver);
1179 
1180 js.tryCatch([&] {
1181 ModuleRegistry::resolve(js, "file:///foo", "default"_kjc);
1182 JSG_FAIL_REQUIRE(Error, "Should have thrown");
1183 }, [&](Value exception) {
1184 auto str = kj::str(exception.getHandle(js));
1185 KJ_ASSERT(str == "Error: foo");
1186 });
1187 });
1188}
1189 
1190// ======================================================================================
1191 
1192KJ_TEST("Syntax error in ESM module is properly reported") {
1193 PREAMBLE([&](Lock& js) {
1194 ResolveObserverImpl observer;
1195 CompilationObserver compilationObserver;
1196 
1197 ModuleBundle::BundleBuilder bundleBuilder(BASE);
1198 
1199 auto foo = kj::str("export default 123; syntax error");
1200 bundleBuilder.addEsmModule("foo", foo);
1201 
1202 auto registry = ModuleRegistry::Builder(observer, BASE).add(bundleBuilder.finish()).finish();
1203 
1204 auto attached = registry->attachToIsolate(js, compilationObserver);
1205 
1206 js.tryCatch(
1207 [&] { ModuleRegistry::resolve(js, "file:///foo", "default"_kjc); }, [&](Value exception) {
1208 auto str = kj::str(exception.getHandle(js));
1209 KJ_ASSERT(str == "SyntaxError: Unexpected identifier 'error'");
1210 });
1211 });
1212}
1213 
1214// ======================================================================================
1215 
1216KJ_TEST("Throwing an exception inside a CJS-style eval module works as expected") {
1217 PREAMBLE([&](Lock& js) {
1218 ResolveObserverImpl observer;
1219 CompilationObserver compilationObserver;
1220 
1221 ModuleBundle::BundleBuilder bundleBuilder(BASE);
1222 
1223 kj::String source = kj::str("exports.foo = 123; throw new Error('bar');");
1224 
1225 bundleBuilder.addSyntheticModule("foo",
1226 Module::newCjsStyleModuleHandler<TestType, TestIsolate_TypeWrapper>(source, "foo"_kj));
1227 
1228 auto registry = ModuleRegistry::Builder(observer, BASE).add(bundleBuilder.finish()).finish();
1229 
1230 auto attached = registry->attachToIsolate(js, compilationObserver);
1231 
1232 js.tryCatch([&] {
1233 ModuleRegistry::resolve(js, "file:///foo", "foo"_kjc);
1234 JSG_FAIL_REQUIRE(Error, "Should have thrown");
1235 }, [&](Value exception) {
1236 auto str = kj::str(exception.getHandle(js));
1237 KJ_ASSERT(str == "Error: bar");
1238 });
1239 });
1240}
1241 
1242// ======================================================================================
1243 
1244KJ_TEST("Invalid JSON syntax module throws exception as expected") {
1245 PREAMBLE([&](Lock& js) {
1246 ResolveObserverImpl observer;
1247 CompilationObserver compilationObserver;
1248 
1249 ModuleBundle::BundleBuilder bundleBuilder(BASE);
1250 auto json = kj::str("not valid json");
1251 bundleBuilder.addSyntheticModule("foo", Module::newJsonModuleHandler(json.first(json.size())));
1252 
1253 auto esm = kj::str("import foo from 'foo'");
1254 bundleBuilder.addEsmModule("bar", esm, Module::Flags::MAIN);
1255 
1256 auto registry = ModuleRegistry::Builder(observer, BASE).add(bundleBuilder.finish()).finish();
1257 
1258 auto attached = registry->attachToIsolate(js, compilationObserver);
1259 
1260 js.tryCatch([&] {
1261 ModuleRegistry::resolve(js, "file:///foo", "default"_kjc);
1262 JSG_FAIL_REQUIRE(Error, "Should have thrown");
1263 }, [&](Value exception) {
1264 auto str = kj::str(exception.getHandle(js));
1265 KJ_ASSERT(str == "SyntaxError: Unexpected token 'o', \"not valid json\" is not valid JSON");
1266 });
1267 
1268 // We can try multiple times and it doesn't matter.
1269 js.tryCatch([&] {
1270 ModuleRegistry::resolve(js, "file:///foo", "default"_kjc);
1271 JSG_FAIL_REQUIRE(Error, "Should have thrown");
1272 }, [&](Value exception) {
1273 auto str = kj::str(exception.getHandle(js));
1274 KJ_ASSERT(str == "SyntaxError: Unexpected token 'o', \"not valid json\" is not valid JSON");
1275 });
1276 
1277 // We get the same error even if statically imported after the previous imports
1278 js.tryCatch([&] {
1279 ModuleRegistry::resolve(js, "file:///bar", "default"_kjc);
1280 JSG_FAIL_REQUIRE(Error, "Should have thrown");
1281 }, [&](Value exception) {
1282 auto str = kj::str(exception.getHandle(js));
1283 KJ_ASSERT(str == "SyntaxError: Unexpected token 'o', \"not valid json\" is not valid JSON");
1284 });
1285 });
1286}
1287 
1288// ======================================================================================
1289 
1290KJ_TEST("Recursive import works or fails as expected") {
1291 PREAMBLE([&](Lock& js) {
1292 ResolveObserverImpl observer;
1293 CompilationObserver compilationObserver;
1294 
1295 ModuleBundle::BundleBuilder bundleBuilder(BASE);
1296 
1297 // A recursive import with an ESM works just fine...
1298 auto foo = kj::str("import foo from 'foo'; export default 123;");
1299 bundleBuilder.addEsmModule("foo", foo);
1300 
1301 auto source = kj::str("require('bar')");
1302 
1303 // A CommonJS-style module, however, does not allow recursive evaluation.
1304 bundleBuilder.addSyntheticModule("bar",
1305 Module::newCjsStyleModuleHandler<TestType, TestIsolate_TypeWrapper>(source, "bar"_kj));
1306 
1307 auto registry = ModuleRegistry::Builder(observer, BASE).add(bundleBuilder.finish()).finish();
1308 
1309 auto attached = registry->attachToIsolate(js, compilationObserver);
1310 
1311 auto val1 = ModuleRegistry::resolve(js, "file:///foo", "default"_kjc);
1312 KJ_ASSERT(val1.isNumber());
1313 
1314 js.tryCatch(
1315 [&] { ModuleRegistry::resolve(js, "file:///bar", "default"_kjc); }, [&](Value exception) {
1316 auto str = kj::str(exception.getHandle(js));
1317 KJ_ASSERT(str == "Error: Module cannot be recursively evaluated: file:///bar");
1318 });
1319 });
1320}
1321 
1322// ======================================================================================
1323 
1324KJ_TEST("Recursively require ESM from CJS required from ESM fails as expected (dynamic import)") {
1325 PREAMBLE([&](Lock& js) {
1326 ResolveObserverImpl observer;
1327 CompilationObserver compilationObserver;
1328 
1329 ModuleBundle::BundleBuilder bundleBuilder(BASE);
1330 
1331 // In this test, we have an ESM module (bar) that imports a CJS style
1332 // module (foo) that synchronously tries to require the ESM module (bar).
1333 
1334 // The circular dependency between foo and baz here, as CJS style modules,
1335 // should be ok in that it should not throw an error. However, the circular
1336 // dependency between foo and bar is more problematic since it is forbidden
1337 // to depend on an ESM that has not yet been fully resolved.
1338 
1339 auto source1 = kj::str("exports = require('foo');");
1340 auto source2 = kj::str("require('baz'); exports = require('bar');");
1341 
1342 bundleBuilder.addSyntheticModule("baz",
1343 Module::newCjsStyleModuleHandler<TestType, TestIsolate_TypeWrapper>(source1, "baz"_kj));
1344 
1345 bundleBuilder.addSyntheticModule("foo",
1346 Module::newCjsStyleModuleHandler<TestType, TestIsolate_TypeWrapper>(source2, "foo"_kj));
1347 
1348 auto bar = kj::str("export default {}; await import('foo');");
1349 bundleBuilder.addEsmModule("bar", bar);
1350 
1351 auto registry = ModuleRegistry::Builder(observer, BASE).add(bundleBuilder.finish()).finish();
1352 
1353 auto attached = registry->attachToIsolate(js, compilationObserver);
1354 
1355 js.tryCatch([&] {
1356 ModuleRegistry::resolve(js, "file:///bar", "default"_kjc);
1357 JSG_FAIL_REQUIRE(Error, "should have failed");
1358 }, [&](Value exception) {
1359 auto str = kj::str(exception.getHandle(js));
1360 KJ_ASSERT(str == "Error: Circular dependency when resolving module: file:///bar");
1361 });
1362 });
1363}
1364 
1365// ======================================================================================
1366 
1367KJ_TEST("Recursively require ESM from CJS required from ESM fails as expected (static import)") {
1368 PREAMBLE([&](Lock& js) {
1369 ResolveObserverImpl observer;
1370 CompilationObserver compilationObserver;
1371 
1372 ModuleBundle::BundleBuilder bundleBuilder(BASE);
1373 
1374 // In this test, we have an ESM module (bar) that imports a CJS style
1375 // module (foo) that synchronously tries to require the ESM module (bar).
1376 
1377 // The circular dependency between foo and baz here, as CJS style modules,
1378 // should be ok in that it should not throw an error. However, the circular
1379 // dependency between foo and bar is more problematic since it is forbidden
1380 // to depend on an ESM that has not yet been fully resolved.
1381 
1382 auto source1 = kj::str("exports = require('foo');");
1383 auto source2 = kj::str("require('baz'); exports = require('bar');");
1384 
1385 bundleBuilder.addSyntheticModule("baz",
1386 Module::newCjsStyleModuleHandler<TestType, TestIsolate_TypeWrapper>(source1, "baz"_kj));
1387 
1388 bundleBuilder.addSyntheticModule("foo",
1389 Module::newCjsStyleModuleHandler<TestType, TestIsolate_TypeWrapper>(source2, "foo"_kj));
1390 
1391 auto bar = kj::str("export default {}; import bar from 'foo';");
1392 bundleBuilder.addEsmModule("bar", bar);
1393 
1394 auto registry = ModuleRegistry::Builder(observer, BASE).add(bundleBuilder.finish()).finish();
1395 
1396 auto attached = registry->attachToIsolate(js, compilationObserver);
1397 
1398 js.tryCatch([&] {
1399 ModuleRegistry::resolve(js, "file:///bar", "default"_kjc);
1400 JSG_FAIL_REQUIRE(Error, "should have failed");
1401 }, [&](Value exception) {
1402 auto str = kj::str(exception.getHandle(js));
1403 KJ_ASSERT(str == "Error: Circular dependency when resolving module: file:///bar");
1404 });
1405 });
1406}
1407 
1408// ======================================================================================
1409 
1410KJ_TEST("ESM -> CJS -> require(ESM) -> static import CJS circular dependency fails gracefully") {
1411 // This tests a specific crash scenario: when a CJS module (b) is mid-evaluation
1412 // (kEvaluating), and it require()s an ESM (c) that statically imports the same
1413 // CJS module (b), V8's InnerModuleEvaluation would call Module::Evaluate on
1414 // the kEvaluating synthetic module, hitting a CHECK crash. The resolveModuleCallback
1415 // kEvaluating guard prevents this by rejecting the circular dependency at
1416 // instantiation time.
1417 PREAMBLE([&](Lock& js) {
1418 ResolveObserverImpl observer;
1419 CompilationObserver compilationObserver;
1420 
1421 ModuleBundle::BundleBuilder bundleBuilder(BASE);
1422 
1423 // a.js (ESM) -> imports b (CJS)
1424 auto a = kj::str("import b from 'b'; export default b;");
1425 bundleBuilder.addEsmModule("a", a, Module::Flags::MAIN);
1426 
1427 // b (CJS) -> require('c') which is an ESM that imports b back
1428 auto bSource = kj::str("exports = require('c');");
1429 bundleBuilder.addSyntheticModule(
1430 "b", Module::newCjsStyleModuleHandler<TestType, TestIsolate_TypeWrapper>(bSource, "b"_kj));
1431 
1432 // c.js (ESM) -> imports b (CJS) โ€” creates the circular dependency
1433 auto c = kj::str("import b from 'b'; export default b;");
1434 bundleBuilder.addEsmModule("c", c);
1435 
1436 auto registry = ModuleRegistry::Builder(observer, BASE).add(bundleBuilder.finish()).finish();
1437 
1438 auto attached = registry->attachToIsolate(js, compilationObserver);
1439 
1440 js.tryCatch([&] {
1441 ModuleRegistry::resolve(js, "file:///a", "default"_kjc);
1442 JSG_FAIL_REQUIRE(Error, "Should have thrown");
1443 }, [&](Value exception) {
1444 auto str = kj::str(exception.getHandle(js));
1445 KJ_ASSERT(str == "TypeError: Circular dependency when resolving module: b");
1446 });
1447 });
1448}
1449 
1450// ======================================================================================
1451 
1452KJ_TEST("UNWRAP_DEFAULT returns namespace for bundle ESM, default for others") {
1453 PREAMBLE([&](Lock& js) {
1454 ResolveObserverImpl observer;
1455 CompilationObserver compilationObserver;
1456 
1457 ModuleBundle::BundleBuilder bundleBuilder(BASE);
1458 
1459 // Bundle ESM with named exports (no __cjsUnwrapDefault)
1460 auto esm = kj::str("export default 42; export const name = 'esm';");
1461 bundleBuilder.addEsmModule("esm-mod", esm, Module::Flags::MAIN);
1462 
1463 // Bundle ESM with __cjsUnwrapDefault convention
1464 auto esmCjs = kj::str("export default 'unwrapped'; export const __cjsUnwrapDefault = true;");
1465 bundleBuilder.addEsmModule("esm-cjs", esmCjs);
1466 
1467 // JSON synthetic module
1468 auto json = kj::str("{\"key\": \"value\"}");
1469 bundleBuilder.addSyntheticModule(
1470 "data.json", Module::newJsonModuleHandler(json.first(json.size())));
1471 
1472 // Text synthetic module
1473 auto text = kj::str("hello world");
1474 bundleBuilder.addSyntheticModule(
1475 "data.txt", Module::newTextModuleHandler(text.first(text.size())));
1476 
1477 auto registry = ModuleRegistry::Builder(observer, BASE).add(bundleBuilder.finish()).finish();
1478 auto attached = registry->attachToIsolate(js, compilationObserver);
1479 
1480 // Bundle ESM without __cjsUnwrapDefault: tryResolveModuleNamespace with UnwrapDefault::YES
1481 // returns the full namespace (has "default", "name" properties).
1482 js.tryCatch([&] {
1483 auto val = KJ_ASSERT_NONNULL(ModuleRegistry::tryResolveModuleNamespace(js, "file:///esm-mod",
1484 ResolveContext::Type::BUNDLE, ResolveContext::Source::REQUIRE, kj::none,
1485 modules::UnwrapDefault::YES));
1486 auto ns = KJ_ASSERT_NONNULL(val.tryCast<JsObject>());
1487 // The namespace should have both "default" and "name" properties.
1488 auto nameVal = ns.get(js, "name");
1489 KJ_ASSERT(!nameVal.isUndefined());
1490 KJ_ASSERT(kj::str(nameVal) == "esm");
1491 auto defaultVal = ns.get(js, "default");
1492 KJ_ASSERT(!defaultVal.isUndefined());
1493 }, [&](Value exception) { js.throwException(kj::mv(exception)); });
1494 
1495 // Bundle ESM with __cjsUnwrapDefault: returns the default export.
1496 js.tryCatch([&] {
1497 auto result = KJ_ASSERT_NONNULL(ModuleRegistry::tryResolveModuleNamespace(js,
1498 "file:///esm-cjs", ResolveContext::Type::BUNDLE, ResolveContext::Source::REQUIRE,
1499 kj::none, modules::UnwrapDefault::YES));
1500 KJ_ASSERT(kj::str(result) == "unwrapped");
1501 }, [&](Value exception) { js.throwException(kj::mv(exception)); });
1502 
1503 // JSON synthetic module: returns the parsed value (the default export).
1504 js.tryCatch([&] {
1505 auto result = KJ_ASSERT_NONNULL(ModuleRegistry::tryResolveModuleNamespace(js,
1506 "file:///data.json", ResolveContext::Type::BUNDLE, ResolveContext::Source::REQUIRE,
1507 kj::none, modules::UnwrapDefault::YES));
1508 // Should be the parsed JSON object, not the namespace.
1509 auto obj = KJ_ASSERT_NONNULL(result.tryCast<JsObject>());
1510 KJ_ASSERT(kj::str(obj.get(js, "key")) == "value");
1511 }, [&](Value exception) { js.throwException(kj::mv(exception)); });
1512 
1513 // Text synthetic module: returns the string value (the default export).
1514 js.tryCatch([&] {
1515 auto result = KJ_ASSERT_NONNULL(ModuleRegistry::tryResolveModuleNamespace(js,
1516 "file:///data.txt", ResolveContext::Type::BUNDLE, ResolveContext::Source::REQUIRE,
1517 kj::none, modules::UnwrapDefault::YES));
1518 // The default is a string โ€” JsValue directly.
1519 KJ_ASSERT(kj::str(result) == "hello world");
1520 }, [&](Value exception) { js.throwException(kj::mv(exception)); });
1521 
1522 // Without UnwrapDefault, all modules return the namespace.
1523 js.tryCatch([&] {
1524 auto val = KJ_ASSERT_NONNULL(ModuleRegistry::tryResolveModuleNamespace(
1525 js, "file:///data.json", ResolveContext::Type::BUNDLE, ResolveContext::Source::REQUIRE));
1526 auto ns = KJ_ASSERT_NONNULL(val.tryCast<JsObject>());
1527 // Should have a "default" property (it's the namespace).
1528 KJ_ASSERT(!ns.get(js, "default").isUndefined());
1529 }, [&](Value exception) { js.throwException(kj::mv(exception)); });
1530 });
1531}
1532 
1533// ======================================================================================
1534KJ_TEST("Resolution occurs relative to the referrer") {
1535 ResolveObserver observer;
1536 CompilationObserver compilationObserver;
1537 ModuleRegistry::Builder registryBuilder(observer, BASE);
1538 
1539 ModuleBundle::BundleBuilder builder(BASE);
1540 builder.addSyntheticModule("foo/bar", Module::newDataModuleHandler(nullptr));
1541 builder.addSyntheticModule("bar", Module::newDataModuleHandler(nullptr));
1542 
1543 // The base URL of the referrer is file:///foo/ ... so in each of the
1544 // following cases, the specifier should be resolved relative to that.
1545 // For instance, 'bar' should resolve as file:///foo/bar, while '../bar'
1546 // should resolve as file:///bar
1547 
1548 auto bar = kj::str("export * as abc from 'bar';" // file:///foo/bar
1549 "export * as def from './bar';" // file:///foo/bar
1550 "export * as ghi from '../bar';" // file:///bar
1551 "export * as jkl from '/bar';" // file:///bar
1552 "export * as lmn from '../foo/bar';"); // file:///foo/bar
1553 builder.addEsmModule("foo/", bar);
1554 
1555 auto registry = registryBuilder.add(builder.finish()).finish();
1556 
1557 PREAMBLE([&](Lock& js) {
1558 auto attached = registry->attachToIsolate(js, compilationObserver);
1559 
1560 js.tryCatch([&] {
1561 auto abc = ModuleRegistry::resolve(js, "file:///foo/", "abc"_kjc);
1562 auto def = ModuleRegistry::resolve(js, "file:///foo/", "def"_kjc);
1563 auto ghi = ModuleRegistry::resolve(js, "file:///foo/", "ghi"_kjc);
1564 auto jkl = ModuleRegistry::resolve(js, "file:///foo/", "jkl"_kjc);
1565 auto lmn = ModuleRegistry::resolve(js, "file:///foo/", "lmn"_kjc);
1566 
1567 KJ_ASSERT(abc.strictEquals(def));
1568 KJ_ASSERT(abc.strictEquals(lmn));
1569 KJ_ASSERT(!abc.strictEquals(ghi));
1570 KJ_ASSERT(ghi.strictEquals(jkl));
1571 }, [&](Value exception) { js.throwException(kj::mv(exception)); });
1572 });
1573}
1574 
1575// ======================================================================================
1576 
1577KJ_TEST("Building a bundle from a capnp description works") {
1578 
1579 capnp::MallocMessageBuilder builder;
1580 auto bundle = builder.initRoot<workerd::jsg::Bundle>();
1581 
1582 auto modules = bundle.initModules(3);
1583 
1584 auto str = kj::str("export default 1+1;");
1585 auto wasm = kj::heapArray<kj::byte>({
1586 0x00,
1587 0x61,
1588 0x73,
1589 0x6d,
1590 0x01,
1591 0x00,
1592 0x00,
1593 0x00,
1594 0x01,
1595 0x07,
1596 0x01,
1597 0x60,
1598 0x02,
1599 0x7f,
1600 0x7f,
1601 0x01,
1602 0x7f,
1603 0x03,
1604 0x02,
1605 0x01,
1606 0x00,
1607 0x07,
1608 0x07,
1609 0x01,
1610 0x03,
1611 0x61,
1612 0x64,
1613 0x64,
1614 0x00,
1615 0x00,
1616 0x0a,
1617 0x09,
1618 0x01,
1619 0x07,
1620 0x00,
1621 0x20,
1622 0x00,
1623 0x20,
1624 0x01,
1625 0x6a,
1626 0x0b,
1627 });
1628 auto data = kj::heapArray<kj::byte>({1, 2, 3});
1629 
1630 modules[0].setName("foo:bar");
1631 modules[0].setSrc(str.asBytes());
1632 modules[0].setType(workerd::jsg::ModuleType::BUILTIN);
1633 
1634 modules[1].setName("foo:baz");
1635 modules[1].setWasm(wasm);
1636 modules[1].setType(workerd::jsg::ModuleType::BUILTIN);
1637 
1638 modules[2].setName("foo:qux");
1639 modules[2].setSrc(data.asBytes());
1640 modules[2].setType(workerd::jsg::ModuleType::BUILTIN);
1641 
1642 ModuleBundle::BuiltinBuilder bundleBuilder;
1643 ModuleBundle::getBuiltInBundleFromCapnp(bundleBuilder, bundle.asReader());
1644 auto moduleBundle = bundleBuilder.finish();
1645 
1646 {
1647 const auto foo = "foo:bar"_url;
1648 ResolveContext context{
1649 .type = ResolveContext::Type::BUILTIN,
1650 .source = ResolveContext::Source::INTERNAL,
1651 .normalizedSpecifier = foo,
1652 .referrerNormalizedSpecifier = BASE,
1653 };
1654 auto resolved = KJ_ASSERT_NONNULL(moduleBundle->lookup(context));
1655 auto& module = KJ_ASSERT_NONNULL(resolved.module);
1656 
1657 KJ_ASSERT(module.id() == foo);
1658 }
1659 
1660 {
1661 const auto bar = "foo:baz"_url;
1662 ResolveContext context{
1663 .type = ResolveContext::Type::BUILTIN,
1664 .source = ResolveContext::Source::INTERNAL,
1665 .normalizedSpecifier = bar,
1666 .referrerNormalizedSpecifier = BASE,
1667 };
1668 auto resolved = KJ_ASSERT_NONNULL(moduleBundle->lookup(context));
1669 auto& module = KJ_ASSERT_NONNULL(resolved.module);
1670 KJ_ASSERT(module.id() == bar);
1671 }
1672 
1673 {
1674 const auto qux = "foo:qux"_url;
1675 ResolveContext context{
1676 .type = ResolveContext::Type::BUILTIN,
1677 .source = ResolveContext::Source::INTERNAL,
1678 .normalizedSpecifier = qux,
1679 .referrerNormalizedSpecifier = BASE,
1680 };
1681 auto resolved = KJ_ASSERT_NONNULL(moduleBundle->lookup(context));
1682 auto& module = KJ_ASSERT_NONNULL(resolved.module);
1683 KJ_ASSERT(module.id() == qux);
1684 }
1685 
1686 PREAMBLE([&](Lock& js) {
1687 ResolveObserver resolveObserver;
1688 CompilationObserver compilationObserver;
1689 auto registry =
1690 ModuleRegistry::Builder(resolveObserver, BASE).add(kj::mv(moduleBundle)).finish();
1691 
1692 auto attached = registry->attachToIsolate(js, compilationObserver);
1693 
1694 // The foo:bar module is interpreted as an ESM
1695 js.tryCatch([&] {
1696 auto val = ModuleRegistry::resolve(js, "foo:bar");
1697 KJ_ASSERT(val.isNumber());
1698 }, [&](Value exception) { js.throwException(kj::mv(exception)); });
1699 });
1700}
1701 
1702// ======================================================================================
1703 
1704KJ_TEST("Using a registry from multiple threads works") {
1705 
1706 kj::AsyncIoContext io = kj::setupAsyncIo();
1707 
1708 ModuleBundle::BundleBuilder bundleBuilder(BASE);
1709 static const auto foo = "export default 123; for (let n = 0; n < 100000; n++) {}"_kjc;
1710 bundleBuilder.addEsmModule("foo", foo);
1711 ResolveObserver resolveObserver;
1712 auto registry =
1713 ModuleRegistry::Builder(resolveObserver, BASE).add(bundleBuilder.finish()).finish();
1714 
1715 struct NonOpErrorHandler final: public kj::TaskSet::ErrorHandler {
1716 void taskFailed(kj::Exception&& exception) override {}
1717 };
1718 NonOpErrorHandler errorHandler;
1719 
1720 kj::TaskSet tasks(errorHandler);
1721 
1722 static const auto makeRunnable = [](kj::Arc<workerd::jsg::modules::ModuleRegistry> registry,
1723 kj::Own<kj::PromiseFulfiller<void>> fulfiller) {
1724 return [registry = kj::mv(registry), fulfiller = kj::mv(fulfiller)]() mutable {
1725 PREAMBLE([&](Lock& js) {
1726 CompilationObserver compilationObserver;
1727 auto attached = registry->attachToIsolate(js, compilationObserver);
1728 js.tryCatch([&] {
1729 auto val = ModuleRegistry::resolve(js, "file:///foo");
1730 KJ_ASSERT(val.isNumber());
1731 }, [&](Value exception) { js.throwException(kj::mv(exception)); });
1732 });
1733 fulfiller->fulfill();
1734 };
1735 };
1736 
1737 struct RunnableAndPromise {
1738 kj::Promise<void> promise;
1739 kj::Function<void()> runnable;
1740 };
1741 
1742 static const auto makeRunnableAndPromise =
1743 [](kj::Arc<workerd::jsg::modules::ModuleRegistry> registry) -> RunnableAndPromise {
1744 auto paf = kj::newPromiseAndCrossThreadFulfiller<void>();
1745 return {kj::mv(paf.promise), makeRunnable(kj::mv(registry), kj::mv(paf.fulfiller))};
1746 };
1747 
1748 auto [paf1, task1] = makeRunnableAndPromise(registry.addRef());
1749 kj::Thread(kj::mv(task1)).detach();
1750 auto [paf2, task2] = makeRunnableAndPromise(registry.addRef());
1751 kj::Thread(kj::mv(task2)).detach();
1752 auto [paf3, task3] = makeRunnableAndPromise(registry.addRef());
1753 kj::Thread(kj::mv(task3)).detach();
1754 auto [paf4, task4] = makeRunnableAndPromise(registry.addRef());
1755 kj::Thread(kj::mv(task4)).detach();
1756 auto [paf5, task5] = makeRunnableAndPromise(registry.addRef());
1757 kj::Thread(kj::mv(task5)).detach();
1758 
1759 tasks.add(kj::mv(paf1));
1760 tasks.add(kj::mv(paf2));
1761 tasks.add(kj::mv(paf3));
1762 tasks.add(kj::mv(paf4));
1763 tasks.add(kj::mv(paf5));
1764 tasks.onEmpty().wait(io.waitScope);
1765}
1766 
1767// ======================================================================================
1768 
1769KJ_TEST("Fallback service can see original raw specifier if provided") {
1770 
1771 ResolveObserver resolveObserver;
1772 CompilationObserver compilationObserver;
1773 ModuleRegistry::Builder builder(
1774 resolveObserver, BASE, ModuleRegistry::Builder::Options::ALLOW_FALLBACK);
1775 auto rawSpecifier = "nothing"_kjc;
1776 const auto id = "file:///nothing"_url;
1777 
1778 bool called = false;
1779 
1780 builder.add(ModuleBundle::newFallbackBundle([&](const ResolveContext& context) {
1781 KJ_ASSERT(context.rawSpecifier == rawSpecifier);
1782 KJ_ASSERT(context.normalizedSpecifier == id);
1783 KJ_ASSERT(context.referrerNormalizedSpecifier == BASE);
1784 called = true;
1785 return kj::none;
1786 }));
1787 
1788 auto registry = builder.finish();
1789 
1790 ResolveContext context = {
1791 .type = ResolveContext::Type::BUNDLE,
1792 .source = ResolveContext::Source::INTERNAL,
1793 .normalizedSpecifier = id,
1794 .referrerNormalizedSpecifier = BASE,
1795 .rawSpecifier = rawSpecifier,
1796 };
1797 
1798 KJ_ASSERT(registry->lookup(context) == kj::none);
1799 KJ_ASSERT(called);
1800}
1801 
1802// ======================================================================================
1803 
1804KJ_TEST("Fallback service can return a module with a different specifier") {
1805 
1806 ResolveObserver resolveObserver;
1807 CompilationObserver compilationObserver;
1808 ModuleRegistry::Builder builder(
1809 resolveObserver, BASE, ModuleRegistry::Builder::Options::ALLOW_FALLBACK);
1810 auto rawSpecifier = "nothing"_kjc;
1811 const auto id = "file:///nothing"_url;
1812 const auto url = "file:///different"_url;
1813 
1814 int called = 0;
1815 
1816 builder.add(ModuleBundle::newFallbackBundle([&](const ResolveContext& context) {
1817 called++;
1818 kj::Own<Module> mod = Module::newSynthetic(
1819 url.clone(), Module::Type::FALLBACK, Module::newDataModuleHandler(nullptr));
1820 return kj::Maybe<kj::OneOf<kj::String, kj::Own<Module>>>(kj::mv(mod));
1821 }));
1822 
1823 auto registry = builder.finish();
1824 
1825 ResolveContext context = {
1826 .type = ResolveContext::Type::BUNDLE,
1827 .source = ResolveContext::Source::INTERNAL,
1828 .normalizedSpecifier = id,
1829 .referrerNormalizedSpecifier = BASE,
1830 .rawSpecifier = rawSpecifier,
1831 };
1832 
1833 auto& module1 = KJ_ASSERT_NONNULL(registry->lookup(context));
1834 
1835 ResolveContext context2 = {
1836 .type = ResolveContext::Type::BUNDLE,
1837 .source = ResolveContext::Source::INTERNAL,
1838 .normalizedSpecifier = url,
1839 .referrerNormalizedSpecifier = BASE,
1840 .rawSpecifier = rawSpecifier,
1841 };
1842 
1843 auto& module2 = KJ_ASSERT_NONNULL(registry->lookup(context2));
1844 
1845 auto& module3 = KJ_ASSERT_NONNULL(registry->lookup(context));
1846 
1847 // Both specifiers should resolve to the same module so the called count should be 1.
1848 KJ_ASSERT(called == 1);
1849 KJ_ASSERT(module1.id() == url);
1850 KJ_ASSERT(&module1 == &module2);
1851 KJ_ASSERT(&module2 == &module3);
1852}
1853 
1854// ======================================================================================
1855 
1856KJ_TEST("Percent-encoding in specifiers is normalized properly") {
1857 ResolveObserver resolveObserver;
1858 CompilationObserver compilationObserver;
1859 
1860 ModuleBundle::BundleBuilder builder(BASE);
1861 
1862 // A specifier might have percent-encoded characters. We want those to be normalized
1863 // so that they are matched correctly. For instance, %66oo%2fbar should be normalized
1864 // to foo%2Fbar, and %66oo/bar should be normalized to foo/bar. Specifically, characters
1865 // that generally do not need to be percent-encoded should be normalized to their
1866 // unencoded form, while characters that need percent encoded should be normalized
1867 // to their capitalized percent-encoded form (e.g. %2f becomes %2F). This ensures that
1868 // when these different forms are used to import they will resolve to the expected
1869 // module.
1870 
1871 builder.addSyntheticModule("foo%2fbar", Module::newDataModuleHandler(nullptr));
1872 builder.addSyntheticModule("foo/bar", Module::newDataModuleHandler(nullptr));
1873 
1874 auto foo = kj::str("export { default as abc } from 'foo%2fbar';"
1875 "export { default as def } from 'foo/bar';"
1876 "export { default as ghi } from '%66oo/bar';"
1877 "export { default as jkl } from '%66oo%2fbar';");
1878 builder.addEsmModule("foo", foo);
1879 
1880 auto registry = ModuleRegistry::Builder(resolveObserver, BASE).add(builder.finish()).finish();
1881 
1882 PREAMBLE([&](Lock& js) {
1883 auto attached = registry->attachToIsolate(js, compilationObserver);
1884 
1885 js.tryCatch([&] {
1886 auto abc = ModuleRegistry::resolve(js, "foo", "abc"_kjc);
1887 auto def = ModuleRegistry::resolve(js, "foo", "def"_kjc);
1888 auto ghi = ModuleRegistry::resolve(js, "foo", "ghi"_kjc);
1889 auto jkl = ModuleRegistry::resolve(js, "foo", "jkl"_kjc);
1890 
1891 KJ_ASSERT(abc.strictEquals(jkl));
1892 KJ_ASSERT(def.strictEquals(ghi));
1893 KJ_ASSERT(!abc.strictEquals(def));
1894 }, [&](Value exception) { js.throwException(kj::mv(exception)); });
1895 });
1896}
1897 
1898// ======================================================================================
1899 
1900KJ_TEST("Aliased modules (import maps) work") {
1901 ResolveObserver resolveObserver;
1902 CompilationObserver compilationObserver;
1903 ModuleBundle::BundleBuilder builder(BASE);
1904 
1905 builder.addSyntheticModule("http://example/foo", Module::newDataModuleHandler(nullptr));
1906 builder.alias("bar", "http://example/foo");
1907 
1908 try {
1909 builder.alias("bar", "baz");
1910 KJ_FAIL_ASSERT("should have thrown");
1911 } catch (kj::Exception& ex) {
1912 KJ_ASSERT(ex.getDescription() == "Module \"file:///bar\" already added to bundle");
1913 }
1914 
1915 try {
1916 builder.alias("http://example/%66oo", "baz");
1917 KJ_FAIL_ASSERT("should have thrown");
1918 } catch (kj::Exception& ex) {
1919 KJ_ASSERT(ex.getDescription() == "Module \"http://example/foo\" already added to bundle");
1920 }
1921 
1922 auto src = kj::str("export { default as abc } from 'bar';"
1923 "export { default as def } from 'http://example/%66oo';");
1924 builder.addEsmModule("qux", src);
1925 
1926 auto registry = ModuleRegistry::Builder(resolveObserver, BASE).add(builder.finish()).finish();
1927 
1928 ResolveContext contextBar{
1929 .type = ResolveContext::Type::BUNDLE,
1930 .source = ResolveContext::Source::INTERNAL,
1931 .normalizedSpecifier = "file:///bar"_url,
1932 .referrerNormalizedSpecifier = BASE,
1933 };
1934 
1935 ResolveContext contextFoo{
1936 .type = ResolveContext::Type::BUNDLE,
1937 .source = ResolveContext::Source::INTERNAL,
1938 .normalizedSpecifier = "http://example/foo"_url,
1939 .referrerNormalizedSpecifier = BASE,
1940 };
1941 
1942 auto& bar = KJ_ASSERT_NONNULL(registry->lookup(contextBar));
1943 auto& foo = KJ_ASSERT_NONNULL(registry->lookup(contextFoo));
1944 
1945 // The aliases resolve to the same underlying module...
1946 KJ_ASSERT(&bar == &foo);
1947 
1948 PREAMBLE([&](Lock& js) {
1949 auto attached = registry->attachToIsolate(js, compilationObserver);
1950 
1951 js.tryCatch([&] {
1952 // While the aliased identifiers resolve to the same underlying module, the
1953 // evaluate into two separate module instances. This is similar in behavior
1954 // to how query string and fragments work. The fact that they use the same
1955 // underlying definition is not really that important.
1956 auto abc = ModuleRegistry::resolve(js, "qux", "abc"_kjc);
1957 auto def = ModuleRegistry::resolve(js, "qux", "def"_kjc);
1958 KJ_ASSERT(abc.isArrayBuffer());
1959 KJ_ASSERT(def.isArrayBuffer());
1960 KJ_ASSERT(!abc.strictEquals(def));
1961 }, [&](Value exception) { js.throwException(kj::mv(exception)); });
1962 });
1963}
1964 
1965// ======================================================================================
1966 
1967KJ_TEST("Import attribute type:json succeeds for JSON modules") {
1968 ResolveObserver resolveObserver;
1969 CompilationObserver compilationObserver;
1970 ModuleBundle::BundleBuilder builder(BASE);
1971 
1972 // An ESM that imports a JSON module with the type attribute
1973 auto entry = kj::str("import data from 'data.json' with { type: 'json' }; export default data;");
1974 builder.addEsmModule("entry", entry, Module::Flags::MAIN);
1975 
1976 auto json = kj::str("{\"key\": \"value\"}");
1977 builder.addSyntheticModule("data.json", Module::newJsonModuleHandler(json.first(json.size())),
1978 nullptr, Module::ContentType::JSON);
1979 
1980 auto registry = ModuleRegistry::Builder(resolveObserver, BASE).add(builder.finish()).finish();
1981 
1982 PREAMBLE([&](Lock& js) {
1983 auto attached = registry->attachToIsolate(js, compilationObserver);
1984 
1985 js.tryCatch([&] {
1986 auto val = ModuleRegistry::resolve(js, "file:///entry", "default"_kjc);
1987 // The JSON module should have been resolved and its value should be the parsed object.
1988 KJ_ASSERT(!val.isUndefined());
1989 }, [&](Value exception) { js.throwException(kj::mv(exception)); });
1990 });
1991}
1992 
1993// ======================================================================================
1994 
1995KJ_TEST("Import attribute type:json fails for non-JSON modules") {
1996 ResolveObserver resolveObserver;
1997 CompilationObserver compilationObserver;
1998 ModuleBundle::BundleBuilder builder(BASE);
1999 
2000 // An ESM that imports another ESM with type:json (should fail - ESM is not JSON)
2001 auto entry = kj::str("import foo from 'other' with { type: 'json' }; export default foo;");
2002 builder.addEsmModule("entry", entry, Module::Flags::MAIN);
2003 
2004 auto other = kj::str("export default 42;");
2005 builder.addEsmModule("other", other);
2006 
2007 auto registry = ModuleRegistry::Builder(resolveObserver, BASE).add(builder.finish()).finish();
2008 
2009 PREAMBLE([&](Lock& js) {
2010 auto attached = registry->attachToIsolate(js, compilationObserver);
2011 
2012 js.tryCatch([&] {
2013 ModuleRegistry::resolve(js, "file:///entry", "default"_kjc);
2014 JSG_FAIL_REQUIRE(Error, "Should have thrown");
2015 }, [&](Value exception) {
2016 auto str = kj::str(exception.getHandle(js));
2017 KJ_ASSERT(str == "TypeError: Module \"other\" is not of type \"json\"");
2018 });
2019 });
2020}
2021 
2022// ======================================================================================
2023 
2024KJ_TEST("Unsupported import attributes are rejected") {
2025 ResolveObserver resolveObserver;
2026 CompilationObserver compilationObserver;
2027 ModuleBundle::BundleBuilder builder(BASE);
2028 
2029 auto foo = kj::str("import abc from 'foo' with { unsupported: 'value' };");
2030 builder.addEsmModule("foo", foo);
2031 
2032 auto registry = ModuleRegistry::Builder(resolveObserver, BASE).add(builder.finish()).finish();
2033 
2034 PREAMBLE([&](Lock& js) {
2035 auto attached = registry->attachToIsolate(js, compilationObserver);
2036 
2037 js.tryCatch([&] {
2038 ModuleRegistry::resolve(js, "foo", "default"_kjc);
2039 JSG_FAIL_REQUIRE(Error, "Should have thrown");
2040 }, [&](Value exception) {
2041 auto str = kj::str(exception.getHandle(js));
2042 KJ_ASSERT(str == "TypeError: Unsupported import attribute: \"unsupported\"");
2043 });
2044 });
2045}
2046 
2047// ======================================================================================
2048KJ_TEST("Using a deferred eval callback works") {
2049 ResolveObserver resolveObserver;
2050 CompilationObserver compilationObserver;
2051 ModuleBundle::BundleBuilder builder(BASE);
2052 
2053 auto foo = kj::str("export default 1;");
2054 builder.addEsmModule("foo", foo);
2055 
2056 bool called = false;
2057 auto registry = ModuleRegistry::Builder(resolveObserver, BASE)
2058 .add(builder.finish())
2059 .setEvalCallback([&called](Lock& js, const Module& module, auto v8Module,
2060 const auto& observer) {
2061 called = true;
2062 return js.resolvedJsPromise(js.num(123));
2063 }).finish();
2064 
2065 PREAMBLE([&](Lock& js) {
2066 auto attached = registry->attachToIsolate(js, compilationObserver);
2067 
2068 js.tryCatch([&] {
2069 ModuleRegistry::resolve(js, "foo", "default"_kjc);
2070 KJ_ASSERT(false);
2071 }, [&](auto exception) {});
2072 
2073 // We don't care about the specific exception above. We only want to know that
2074 // the eval callback was invoked.
2075 KJ_ASSERT(called);
2076 });
2077}
2078 
2079// ======================================================================================
2080 
2081KJ_TEST("Fallback receives rawSpecifier and source through V8 static import resolution") {
2082 // This test verifies that when a module is resolved through V8's static import
2083 // pipeline (which goes through IsolateModuleRegistry::resolveWithCaching), the
2084 // fallback callback receives the correct rawSpecifier and source fields.
2085 // Regression test for https://github.com/cloudflare/workerd/issues/6474 and
2086 // https://github.com/cloudflare/workerd/issues/6475.
2087 
2088 ResolveObserverImpl resolveObserver;
2089 CompilationObserver compilationObserver;
2090 
2091 // The entry module statically imports "./missing", which is not in the bundle.
2092 // This forces fallback resolution through resolveWithCaching.
2093 ModuleBundle::BundleBuilder bundleBuilder(BASE);
2094 auto main = kj::str("import './missing';");
2095 bundleBuilder.addEsmModule("main", main, Module::Flags::MAIN);
2096 
2097 bool fallbackCalled = false;
2098 auto fallback = ModuleBundle::newFallbackBundle(
2099 [&](const ResolveContext& context) -> kj::Maybe<kj::OneOf<kj::String, kj::Own<Module>>> {
2100 fallbackCalled = true;
2101 // The rawSpecifier should be the original "./missing" as written in the import.
2102 KJ_ASSERT(context.rawSpecifier == "./missing"_kjc);
2103 // A static import should have source == STATIC_IMPORT.
2104 KJ_ASSERT(context.source == ResolveContext::Source::STATIC_IMPORT);
2105 // Return a synthetic module to satisfy the import.
2106 return kj::Maybe<kj::OneOf<kj::String, kj::Own<Module>>>(
2107 Module::newSynthetic(context.normalizedSpecifier.clone(), Module::Type::FALLBACK,
2108 Module::newDataModuleHandler(nullptr)));
2109 });
2110 
2111 auto registry = ModuleRegistry::Builder(
2112 resolveObserver, BASE, ModuleRegistry::Builder::Options::ALLOW_FALLBACK)
2113 .add(bundleBuilder.finish())
2114 .add(kj::mv(fallback))
2115 .finish();
2116 
2117 PREAMBLE([&](Lock& js) {
2118 auto attached = registry->attachToIsolate(js, compilationObserver);
2119 
2120 js.tryCatch([&] { ModuleRegistry::resolve(js, "file:///main"); },
2121 [&](Value exception) { js.throwException(kj::mv(exception)); });
2122 });
2123 
2124 KJ_ASSERT(fallbackCalled);
2125}
2126 
2127// ======================================================================================
2128 
2129KJ_TEST("Fallback receives REQUIRE source through require() resolution") {
2130 // This test verifies that when a module is resolved through require() (which also
2131 // goes through IsolateModuleRegistry::resolveWithCaching), the fallback callback
2132 // receives source == REQUIRE.
2133 // Regression test for https://github.com/cloudflare/workerd/issues/6475.
2134 
2135 ResolveObserverImpl resolveObserver;
2136 CompilationObserver compilationObserver;
2137 
2138 ModuleBundle::BundleBuilder bundleBuilder(BASE);
2139 
2140 bool fallbackCalled = false;
2141 auto fallback = ModuleBundle::newFallbackBundle(
2142 [&](const ResolveContext& context) -> kj::Maybe<kj::OneOf<kj::String, kj::Own<Module>>> {
2143 fallbackCalled = true;
2144 // A require() call should have source == REQUIRE.
2145 KJ_ASSERT(context.source == ResolveContext::Source::REQUIRE);
2146 KJ_ASSERT(context.rawSpecifier == "missing"_kjc);
2147 return kj::Maybe<kj::OneOf<kj::String, kj::Own<Module>>>(
2148 Module::newSynthetic(context.normalizedSpecifier.clone(), Module::Type::FALLBACK,
2149 Module::newDataModuleHandler(nullptr)));
2150 });
2151 
2152 auto registry = ModuleRegistry::Builder(
2153 resolveObserver, BASE, ModuleRegistry::Builder::Options::ALLOW_FALLBACK)
2154 .add(bundleBuilder.finish())
2155 .add(kj::mv(fallback))
2156 .finish();
2157 
2158 PREAMBLE([&](Lock& js) {
2159 auto attached = registry->attachToIsolate(js, compilationObserver);
2160 
2161 js.tryCatch([&] {
2162 // tryResolveModuleNamespace with Source::REQUIRE exercises the require() path
2163 // through IsolateModuleRegistry::require() -> resolveWithCaching().
2164 ModuleRegistry::tryResolveModuleNamespace(
2165 js, "missing", ResolveContext::Type::BUNDLE, ResolveContext::Source::REQUIRE);
2166 }, [&](Value exception) { js.throwException(kj::mv(exception)); });
2167 });
2168 
2169 KJ_ASSERT(fallbackCalled);
2170}
2171 
2172} // namespace
2173} // namespace workerd::jsg::test