Skip to content
File

Blob: src/workerd/jsg/modules.h

cpp782 lines
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#pragma once
6 
7#include <workerd/jsg/function.h>
8#include <workerd/jsg/modules.capnp.h>
9#include <workerd/jsg/observer.h>
10#include <workerd/util/sentry.h>
11#include <workerd/util/thread-scopes.h>
12 
13#include <v8-json.h>
14 
15#include <kj/filesystem.h>
16#include <kj/map.h>
17 
18namespace workerd::jsg {
19 
20template <typename T>
21class Promise;
22 
23enum class InstantiateModuleOptions {
24 // Allows pending top-level await in the module when evaluated. Will cause
25 // the microtask queue to be drained once in an attempt to resolve those.
26 DEFAULT,
27 // Throws if the module evaluation results in a pending promise.
28 NO_TOP_LEVEL_AWAIT,
29};
30 
31void instantiateModule(jsg::Lock& js,
32 v8::Local<v8::Module>& module,
33 InstantiateModuleOptions options = InstantiateModuleOptions::DEFAULT);
34 
35enum class ModuleInfoCompileOption {
36 // The BUNDLE options tells the compile operation to treat the content as coming
37 // from a worker bundle.
38 BUNDLE,
39 
40 // The BUILTIN option tells the compile operation to treat the content as a builtin
41 // module. This implies certain changes in behavior, such as treating the content
42 // as an immutable, process-lifetime buffer that will never be destroyed, and caching
43 // the compilation data.
44 BUILTIN,
45};
46 
47v8::Local<v8::WasmModuleObject> compileWasmModule(
48 jsg::Lock& js, kj::ArrayPtr<const uint8_t> code, const CompilationObserver& observer);
49 
50// The ModuleRegistry maintains the collection of modules known to a script that can be
51// required or imported.
52class ModuleRegistry {
53 public:
54 KJ_DISALLOW_COPY_AND_MOVE(ModuleRegistry);
55 
56 ModuleRegistry() {}
57 
58 using Type = ModuleType;
59 
60 JSG_MEMORY_INFO(ModuleRegistry) {
61 // TODO(soon): Implement memory tracking for ModuleRegistry
62 }
63 
64 enum class ResolveOption {
65 // Default resolution. Check the worker bundle first, then builtins.
66 DEFAULT,
67 // Built-in resolution. Check only non-internal builtins.
68 BUILTIN_ONLY,
69 // Internal resolution. Check only internal builtins.
70 INTERNAL_ONLY,
71 };
72 
73 static inline ModuleRegistry* from(jsg::Lock& js) {
74 return &KJ_ASSERT_NONNULL(jsg::getAlignedPointerFromEmbedderData<ModuleRegistry>(
75 js.v8Context(), jsg::ContextPointerSlot::MODULE_REGISTRY));
76 }
77 
78 struct CapnpModuleInfo {
79 Value fileScope; // default import
80 kj::HashMap<kj::StringPtr, jsg::Value> topLevelDecls; // named imports
81 
82 CapnpModuleInfo(Value fileScope, kj::HashMap<kj::StringPtr, jsg::Value> topLevelDecls);
83 CapnpModuleInfo(CapnpModuleInfo&&) = default;
84 CapnpModuleInfo& operator=(CapnpModuleInfo&&) = default;
85 };
86 
87 struct CommonJsModuleInfo {
88 struct CommonJsModuleProvider {
89 virtual JsObject getContext(Lock& js) = 0;
90 virtual JsValue getExports(Lock& js) = 0;
91 virtual ~CommonJsModuleProvider() noexcept(false) = default;
92 };
93 
94 kj::Own<CommonJsModuleProvider> provider;
95 jsg::Function<void()> evalFunc;
96 
97 CommonJsModuleInfo(auto& lock,
98 kj::StringPtr name,
99 kj::StringPtr content,
100 kj::Own<CommonJsModuleProvider> provider)
101 : provider(kj::mv(provider)),
102 evalFunc(initEvalFunc(lock, *this->provider, name, content)) {}
103 
104 CommonJsModuleInfo(CommonJsModuleInfo&&) = default;
105 CommonJsModuleInfo& operator=(CommonJsModuleInfo&&) = default;
106 
107 jsg::JsValue getExports(jsg::Lock& js);
108 
109 static jsg::Function<void()> initEvalFunc(
110 auto& lock, CommonJsModuleProvider& provider, kj::StringPtr name, kj::StringPtr content) {
111 v8::ScriptOrigin origin(v8StrIntern(lock.v8Isolate, name));
112 v8::ScriptCompiler::Source source(v8Str(lock.v8Isolate, content), origin);
113 auto context = lock.v8Context();
114 v8::Local<v8::Object> handle = provider.getContext(lock);
115 auto fn =
116 jsg::check(v8::ScriptCompiler::CompileFunction(context, &source, 0, nullptr, 1, &handle));
117 return lock.template unwrap<jsg::Function<void()>>(context, fn);
118 }
119 };
120 
121 template <typename T>
122 struct ValueModuleInfo {
123 jsg::V8Ref<T> value;
124 
125 ValueModuleInfo(jsg::Lock& js, v8::Local<T> value): value(js.v8Isolate, value) {}
126 
127 ValueModuleInfo(ValueModuleInfo&&) = default;
128 ValueModuleInfo& operator=(ValueModuleInfo&&) = default;
129 };
130 
131 using DataModuleInfo = ValueModuleInfo<v8::ArrayBuffer>;
132 using TextModuleInfo = ValueModuleInfo<v8::String>;
133 using WasmModuleInfo = ValueModuleInfo<v8::WasmModuleObject>;
134 using JsonModuleInfo = ValueModuleInfo<v8::Value>;
135 using ObjectModuleInfo = ValueModuleInfo<v8::Object>;
136 
137 struct ModuleInfo {
138 HashableV8Ref<v8::Module> module;
139 
140 using SyntheticModuleInfo = kj::OneOf<CapnpModuleInfo,
141 CommonJsModuleInfo,
142 DataModuleInfo,
143 TextModuleInfo,
144 WasmModuleInfo,
145 JsonModuleInfo,
146 ObjectModuleInfo>;
147 kj::Maybe<SyntheticModuleInfo> maybeSynthetic;
148 kj::Maybe<kj::Array<kj::String>> maybeNamedExports;
149 
150 // For source phase imports - stores the module source object (e.g., WebAssembly.Module)
151 kj::Maybe<V8Ref<v8::Object>> maybeModuleSourceObject;
152 
153 // Cache for mutable module exports wrapper when require_returns_default_export flag is enabled.
154 // Used to ensure require() returns the same mutable object for the same module.
155 // This enables frameworks like Next.js to patch built-in module exports.
156 // See: https://github.com/cloudflare/workerd/issues/5844
157 mutable kj::Maybe<V8Ref<v8::Object>> maybeMutableExports;
158 
159 ModuleInfo(jsg::Lock& js,
160 v8::Local<v8::Module> module,
161 kj::Maybe<SyntheticModuleInfo> maybeSynthetic = kj::none);
162 
163 ModuleInfo(jsg::Lock& js,
164 kj::StringPtr name,
165 kj::ArrayPtr<const char> content,
166 kj::ArrayPtr<const kj::byte> compileCache,
167 ModuleInfoCompileOption flags,
168 const CompilationObserver& observer);
169 
170 ModuleInfo(jsg::Lock& js,
171 kj::StringPtr name,
172 kj::Maybe<kj::ArrayPtr<const kj::StringPtr>> maybeExports,
173 SyntheticModuleInfo synthetic);
174 
175 ModuleInfo(ModuleInfo&&) = default;
176 ModuleInfo& operator=(ModuleInfo&&) = default;
177 
178 uint hashCode() const {
179 return module.hashCode();
180 }
181 
182 // Set the module source object for source phase imports
183 void setModuleSourceObject(jsg::Lock& js, v8::Local<v8::Object> sourceObject) {
184 maybeModuleSourceObject = V8Ref<v8::Object>(js.v8Isolate, sourceObject);
185 }
186 
187 // Get the module source object for source phase imports
188 kj::Maybe<v8::Local<v8::Object>> getModuleSourceObject(jsg::Lock& js) const {
189 KJ_IF_SOME(sourceObject, maybeModuleSourceObject) {
190 return sourceObject.getHandle(js);
191 }
192 return kj::none;
193 }
194 };
195 
196 struct ModuleRef {
197 const kj::Path& specifier;
198 Type type;
199 ModuleInfo& module;
200 };
201 
202 enum class ResolveMethod {
203 // Resolving using the standard static or dynamic import.
204 IMPORT,
205 // Resolving using the commonjs require method.
206 REQUIRE,
207 };
208 
209 using ModuleCallback =
210 kj::Function<kj::Maybe<ModuleInfo>(Lock&, ResolveMethod, kj::Maybe<const kj::Path&>&)>;
211 
212 virtual kj::Maybe<ModuleInfo&> resolve(jsg::Lock& js,
213 const kj::Path& specifier,
214 kj::Maybe<const kj::Path&> referrer = kj::none,
215 ResolveOption option = ResolveOption::DEFAULT,
216 ResolveMethod method = ResolveMethod::IMPORT,
217 kj::Maybe<kj::StringPtr> rawSpecifier = kj::none) = 0;
218 
219 virtual kj::Maybe<ModuleRef> resolve(jsg::Lock& js, v8::Local<v8::Module> module) = 0;
220 
221 virtual Promise<Value> resolveDynamicImport(jsg::Lock& js,
222 const kj::Path& specifier,
223 const kj::Path& referrer,
224 kj::StringPtr rawSpecifier) = 0;
225 
226 virtual Value resolveInternalImport(jsg::Lock& js, kj::StringPtr specifier) = 0;
227 
228 // The dynamic import callback is provided by the embedder to set up any context necessary
229 // for instantiating the module during a dynamic import. The handler function passed into
230 // the callback is called to actually perform the instantiation of the module.
231 using DynamicImportCallback = Promise<Value>(jsg::Lock& js, kj::Function<Value()> handler);
232 
233 virtual void setDynamicImportCallback(kj::Function<DynamicImportCallback> func) = 0;
234 
235 enum class RequireImplOptions {
236 // Require returns the module namespace.
237 DEFAULT,
238 // Require returns the default export.
239 EXPORT_DEFAULT,
240 };
241 
242 static JsValue requireImpl(
243 Lock& js, ModuleInfo& info, RequireImplOptions options = RequireImplOptions::DEFAULT);
244};
245 
246template <typename TypeWrapper>
247v8::MaybeLocal<v8::Promise> dynamicImportCallback(v8::Local<v8::Context> context,
248 v8::Local<v8::Data> host_defined_options,
249 v8::Local<v8::Value> resource_name,
250 v8::Local<v8::String> specifier,
251 v8::Local<v8::FixedArray> import_attributes);
252 
253kj::Maybe<kj::OneOf<kj::String, ModuleRegistry::ModuleInfo>> tryResolveFromFallbackService(Lock& js,
254 const kj::Path& specifier,
255 kj::Maybe<const kj::Path&>& referrer,
256 CompilationObserver& observer,
257 ModuleRegistry::ResolveMethod method,
258 kj::Maybe<kj::StringPtr> rawSpecifier);
259 
260template <typename TypeWrapper>
261class ModuleRegistryImpl final: public ModuleRegistry {
262 public:
263 KJ_DISALLOW_COPY_AND_MOVE(ModuleRegistryImpl);
264 
265 ModuleRegistryImpl(CompilationObserver& observer): observer(observer) {}
266 
267 static kj::Own<ModuleRegistryImpl<TypeWrapper>> install(
268 v8::Isolate* isolate, v8::Local<v8::Context> context, CompilationObserver& observer) {
269 auto registry = kj::heap<ModuleRegistryImpl<TypeWrapper>>(observer);
270 jsg::setAlignedPointerInEmbedderData(
271 context, jsg::ContextPointerSlot::MODULE_REGISTRY, registry.get());
272 isolate->SetHostImportModuleDynamicallyCallback(dynamicImportCallback<TypeWrapper>);
273 return kj::mv(registry);
274 }
275 
276 static inline ModuleRegistryImpl* from(jsg::Lock& js) {
277 return &KJ_ASSERT_NONNULL(jsg::getAlignedPointerFromEmbedderData<ModuleRegistryImpl>(
278 js.v8Context(), jsg::ContextPointerSlot::MODULE_REGISTRY));
279 }
280 
281 void setDynamicImportCallback(kj::Function<DynamicImportCallback> func) override {
282 dynamicImportHandler = kj::mv(func);
283 }
284 
285 void add(kj::Path& specifier, ModuleInfo&& info) {
286 entries.insert(kj::heap<Entry>(specifier, Type::BUNDLE, kj::fwd<ModuleInfo>(info)));
287 }
288 
289 void addBuiltinModule(Module::Reader module) {
290 if (module.which() != Module::SRC) {
291 auto specifier = module.getName();
292 auto path = kj::Path::parse(specifier);
293 switch (module.which()) {
294 case Module::WASM:
295 // The body of this callback is copied from `compileWasmGlobal` in
296 // src/workerd/server/workerd-api.c++.
297 addBuiltinModule(specifier,
298 [specifier, module, this](Lock& lock, ResolveMethod, kj::Maybe<const kj::Path&>&) {
299 lock.setAllowEval(true);
300 KJ_DEFER(lock.setAllowEval(false));
301 
302 // Allow Wasm compilation to spawn a background thread for tier-up, i.e.
303 // recompiling Wasm with optimizations in the background. Otherwise Wasm startup
304 // is way too slow. Until tier-up finishes, requests will be handled using
305 // Liftoff-generated code, which compiles fast but runs slower.
306 AllowV8BackgroundThreadsScope scope;
307 auto wasmModule =
308 jsg::compileWasmModule(lock, module.getWasm().asBytes(), this->observer);
309 auto moduleInfo = jsg::ModuleRegistry::ModuleInfo(
310 lock, specifier, kj::none, jsg::ModuleRegistry::WasmModuleInfo(lock, wasmModule));
311 // Uncomment iff we want to permit source phase imports for builtin Wasm modules
312 // moduleInfo.setModuleSourceObject(lock, wasmModule.template As<v8::Object>());
313 return moduleInfo;
314 }, module.getType());
315 return;
316 case Module::DATA:
317 addBuiltinModule(specifier,
318 [specifier, module](Lock& lock, ResolveMethod, kj::Maybe<const kj::Path&>&) {
319 v8::Local<v8::ArrayBuffer> data =
320 lock.wrapBytes(kj::heapArray(module.getData().asBytes()));
321 return jsg::ModuleRegistry::ModuleInfo(
322 lock, specifier, kj::none, jsg::ModuleRegistry::DataModuleInfo(lock, data));
323 }, module.getType());
324 return;
325 case Module::JSON:
326 addBuiltinModule(specifier,
327 [specifier, module](Lock& lock, ResolveMethod, kj::Maybe<const kj::Path&>&) {
328 auto data =
329 jsg::check(v8::JSON::Parse(lock.v8Context(), lock.wrapString(module.getJson())));
330 return jsg::ModuleRegistry::ModuleInfo(
331 lock, specifier, kj::none, jsg::ModuleRegistry::JsonModuleInfo(lock, data));
332 }, module.getType());
333 return;
334 case Module::SRC:
335 KJ_UNREACHABLE
336 }
337 }
338 // TODO: asChars() might be wrong for wide characters
339 addBuiltinModule(module.getName(), module.getSrc().asChars(), module.getType(),
340 module.getCompileCache().asBytes());
341 }
342 
343 void addBuiltinBundle(Bundle::Reader bundle, kj::Maybe<Type> maybeFilter = kj::none) {
344 for (auto module: bundle.getModules()) {
345 if (module.getType() == maybeFilter.orDefault(module.getType())) addBuiltinModule(module);
346 }
347 }
348 
349 template <typename Func>
350 void addBuiltinBundleFiltered(Bundle::Reader bundle, Func filter) {
351 for (auto module: bundle.getModules()) {
352 if (filter(module)) {
353 addBuiltinModule(module);
354 }
355 }
356 }
357 
358 // Register new module accessible by a given importPath. The module is instantiated
359 // after first resolve attempt within application has failed, i.e. it is possible for
360 // application to override the module.
361 // sourceCode has to exist while this ModuleRegistry exists.
362 // The expectation is for this method to be called during the assembly of worker global context
363 // after registering all user modules.
364 void addBuiltinModule(kj::StringPtr specifier,
365 kj::ArrayPtr<const char> sourceCode,
366 Type type = Type::BUILTIN,
367 kj::ArrayPtr<const kj::byte> compileCache = {}) {
368 KJ_ASSERT(type != Type::BUNDLE);
369 auto path = kj::Path::parse(specifier);
370 entries.insert(kj::heap<Entry>(path, type, sourceCode, compileCache));
371 }
372 
373 void addBuiltinModule(
374 kj::StringPtr specifier, ModuleCallback factory, Type type = Type::BUILTIN) {
375 KJ_ASSERT(type != Type::BUNDLE);
376 auto path = kj::Path::parse(specifier);
377 entries.insert(kj::heap<Entry>(path, type, kj::mv(factory)));
378 }
379 
380 template <typename T>
381 void addBuiltinModule(kj::StringPtr specifier, Type type = Type::BUILTIN) {
382 addBuiltinModule(specifier, alloc<T>(), type);
383 }
384 
385 template <typename T>
386 void addBuiltinModule(kj::StringPtr specifier, Ref<T> object, Type type = Type::BUILTIN) {
387 addBuiltinModule(specifier,
388 [specifier = kj::str(specifier), object = kj::mv(object)](
389 Lock& js, ResolveMethod, kj::Maybe<const kj::Path&>&) mutable -> kj::Maybe<ModuleInfo> {
390 auto& wrapper = TypeWrapper::from(js.v8Isolate);
391 auto wrap = wrapper.wrap(js, js.v8Context(), kj::none, kj::mv(object));
392 return kj::Maybe(ModuleInfo(js, specifier, kj::none, ObjectModuleInfo(js, wrap)));
393 },
394 type);
395 }
396 
397 kj::Maybe<ModuleInfo&> resolve(jsg::Lock& js,
398 const kj::Path& specifier,
399 kj::Maybe<const kj::Path&> referrer = kj::none,
400 ResolveOption option = ResolveOption::DEFAULT,
401 ResolveMethod method = ResolveMethod::IMPORT,
402 kj::Maybe<kj::StringPtr> rawSpecifier = kj::none) override {
403 using Key = Entry::Key;
404 if (option == ResolveOption::INTERNAL_ONLY) {
405 KJ_IF_SOME(entry, entries.find(Key(specifier, Type::INTERNAL))) {
406 return entry->module(js, observer, referrer, method);
407 }
408 return kj::none;
409 } else if (option == ResolveOption::BUILTIN_ONLY) {
410 KJ_IF_SOME(entry, entries.find(Key(specifier, Type::BUILTIN))) {
411 return entry->module(js, observer, referrer, method);
412 }
413 } else {
414 if (option == ResolveOption::DEFAULT) {
415 // First, we try to resolve a worker bundle version of the module.
416 KJ_IF_SOME(entry, entries.find(Key(specifier, Type::BUNDLE))) {
417 return entry->module(js, observer, referrer, method);
418 }
419 }
420 // Then we look for a built-in version of the module.
421 KJ_IF_SOME(entry, entries.find(Key(specifier, Type::BUILTIN))) {
422 return entry->module(js, observer, referrer, method);
423 }
424 }
425 
426 // An internal only resolution should never go to the fallback service
427 KJ_DASSERT(option != ResolveOption::INTERNAL_ONLY);
428 
429 // If the module is not found and we have a module fallback service configured,
430 // let's try that as a means of looking it up.
431 auto str = specifier.toString(true);
432 KJ_IF_SOME(found, fallbackServiceRedirects.find(str)) {
433 // The fallback service has already given us a redirect response for this specifier.
434 // let's use it to try to resolve. Make sure we're using DEFAULT resolution so BUNDLE-typed
435 // modules from the fallback service can be used.
436 option = ResolveOption::DEFAULT;
437 return resolve(js, specifier.parent().eval(found), referrer, option, method, rawSpecifier);
438 }
439 KJ_IF_SOME(info,
440 tryResolveFromFallbackService(js, specifier, referrer, observer, method, rawSpecifier)) {
441 // If we resolved a module from the fallback service, we have to be sure
442 // to add it to the registry...
443 KJ_SWITCH_ONEOF(info) {
444 KJ_CASE_ONEOF(i, ModuleInfo) {
445 auto type = Type::BUNDLE;
446 if (option == ResolveOption::BUILTIN_ONLY) {
447 if (str.startsWith("/node:") || str.startsWith("/cloudflare:") ||
448 str.startsWith("/workerd:")) {
449 type = Type::BUILTIN;
450 }
451 }
452 
453 entries.insert(kj::heap<Entry>(specifier, type, kj::mv(i)));
454 auto& entry = KJ_ASSERT_NONNULL(entries.find(Key(specifier, type)));
455 return entry->module(js, observer, referrer, method);
456 }
457 KJ_CASE_ONEOF(s, kj::String) {
458 // If a kj::String is returned, it means the fallback service is redirecting
459 // us to another module that should already be in the registry... or could
460 // itself end up calling back to the fallback service.
461 fallbackServiceRedirects.upsert(kj::mv(str), kj::str(s));
462 // Make sure we're using DEFAULT resolution so BUNDLE-typed modules from the fallback
463 // service can be used.
464 option = ResolveOption::DEFAULT;
465 return resolve(js, specifier.parent().eval(s), referrer, option, method, rawSpecifier);
466 }
467 }
468 }
469 
470 return kj::none;
471 }
472 
473 kj::Maybe<ModuleRef> resolve(jsg::Lock& js, v8::Local<v8::Module> module) override {
474 for (const kj::Own<Entry>& entry: entries) {
475 // Unfortunately we cannot use entries.find(...) in here because the module info can
476 // be initialized lazily at any point after the entry is indexed, making the lookup
477 // by module a bit problematic. Iterating through the entries is slower but it works.
478 KJ_IF_SOME(info, entry->info.template tryGet<ModuleInfo>()) {
479 if (info.module == module) {
480 return ModuleRef{
481 .specifier = entry->specifier,
482 .type = entry->type,
483 .module = const_cast<ModuleInfo&>(info),
484 };
485 }
486 }
487 }
488 return kj::none;
489 }
490 
491 size_t size() const {
492 return entries.size();
493 }
494 
495 Promise<Value> resolveDynamicImport(jsg::Lock& js,
496 const kj::Path& specifier,
497 const kj::Path& referrer,
498 kj::StringPtr rawSpecifier) override {
499 // Here, we first need to determine if the referrer is a built-in module
500 // or not. If it is a built-in, then we are only permitted to resolve
501 // internal modules. If the worker bundle provided an override for the
502 // built-in module, then the built-in was never registered and won't
503 // be found.
504 using Key = Entry::Key;
505 auto resolveOption = ModuleRegistry::ResolveOption::DEFAULT;
506 if (entries.find(Key(referrer, Type::BUNDLE)) != kj::none) {
507 // The referrer is found in the module bundle, so we use the default.
508 } else if (entries.find(Key(referrer, Type::BUILTIN)) != kj::none) {
509 resolveOption = ModuleRegistry::ResolveOption::INTERNAL_ONLY;
510 }
511 
512 KJ_IF_SOME(info,
513 resolve(js, specifier, referrer, resolveOption, ResolveMethod::IMPORT, rawSpecifier)) {
514 KJ_IF_SOME(func, dynamicImportHandler) {
515 auto handler = [&info, isolate = js.v8Isolate]() -> Value {
516 auto& js = Lock::from(isolate);
517 auto module = info.module.getHandle(js);
518 instantiateModule(js, module);
519 return js.v8Ref(module->GetModuleNamespace());
520 };
521 return func(js, kj::mv(handler));
522 }
523 
524 // If there is no dynamicImportHandler set, then we are going to handle that as if
525 // the module does not exist and fall through to the rejected promise below.
526 }
527 
528 return js.rejectedPromise<Value>(
529 js.v8Error(kj::str("No such module \"", specifier.toString(), "\".")));
530 }
531 
532 Value resolveInternalImport(jsg::Lock& js, const kj::StringPtr specifier) override {
533 auto specifierPath = kj::Path(specifier);
534 auto resolveOption = jsg::ModuleRegistry::ResolveOption::INTERNAL_ONLY;
535 auto maybeModuleInfo =
536 resolve(js, specifierPath, kj::none, resolveOption, ResolveMethod::IMPORT, specifier);
537 auto moduleInfo = &KJ_REQUIRE_NONNULL(maybeModuleInfo, "No such module \"", specifier, "\".");
538 auto handle = moduleInfo->module.getHandle(js);
539 jsg::instantiateModule(js, handle);
540 return js.v8Ref(handle->GetModuleNamespace());
541 }
542 
543 CompilationObserver& getObserver() {
544 return observer;
545 }
546 
547 private:
548 CompilationObserver& observer;
549 kj::Maybe<kj::Function<DynamicImportCallback>> dynamicImportHandler;
550 
551 // When we build a bundle containing modules, we must build a table of modules to resolve imports.
552 //
553 // Because of the design of V8's resolver callback, we end up needing a table with two indexes:
554 // we need to be able to search it by path (filename) as well as search for a specific module
555 // object by identity. We use a kj::Table!
556 struct Entry {
557 using Info = kj::OneOf<ModuleInfo, kj::ArrayPtr<const char>, ModuleCallback>;
558 
559 struct Key {
560 const kj::Path& specifier;
561 const Type type = Type::BUNDLE;
562 uint hash;
563 
564 Key(const kj::Path& specifier, Type type)
565 : specifier(specifier),
566 type(type),
567 hash(kj::hashCode(specifier, type)) {}
568 
569 uint hashCode() const {
570 return hash;
571 }
572 };
573 
574 kj::Path specifier;
575 Type type;
576 
577 // Either instantiated module or module source code.
578 Info info;
579 
580 // Optional compileCache.
581 kj::ArrayPtr<const kj::byte> compileCache;
582 
583 Entry(const kj::Path& specifier, Type type, ModuleInfo info)
584 : specifier(specifier.clone()),
585 type(type),
586 info(kj::mv(info)) {}
587 
588 Entry(const kj::Path& specifier,
589 Type type,
590 kj::ArrayPtr<const char> src,
591 kj::ArrayPtr<const kj::byte> compileCache)
592 : specifier(specifier.clone()),
593 type(type),
594 info(src),
595 compileCache(compileCache) {}
596 
597 Entry(const kj::Path& specifier, Type type, ModuleCallback factory)
598 : specifier(specifier.clone()),
599 type(type),
600 info(kj::mv(factory)) {}
601 
602 Entry(Entry&&) = default;
603 Entry& operator=(Entry&&) = default;
604 
605 // Lazily instantiate module from source code if needed
606 kj::Maybe<ModuleInfo&> module(jsg::Lock& js,
607 CompilationObserver& observer,
608 kj::Maybe<const kj::Path&> referrer,
609 ModuleRegistry::ResolveMethod method = ModuleRegistry::ResolveMethod::IMPORT) {
610 KJ_SWITCH_ONEOF(info) {
611 KJ_CASE_ONEOF(moduleInfo, ModuleInfo) {
612 return kj::Maybe<ModuleInfo&>(moduleInfo);
613 }
614 KJ_CASE_ONEOF(src, kj::ArrayPtr<const char>) {
615 info = ModuleInfo(js, specifier.toString(), src, compileCache,
616 ModuleInfoCompileOption::BUILTIN, observer);
617 return info.tryGet<ModuleInfo>();
618 }
619 KJ_CASE_ONEOF(src, ModuleCallback) {
620 KJ_IF_SOME(result, src(js, method, referrer)) {
621 info = kj::mv(result);
622 }
623 return info.tryGet<ModuleInfo>();
624 }
625 }
626 KJ_UNREACHABLE;
627 }
628 };
629 
630 struct SpecifierHashCallbacks {
631 using Key = Entry::Key;
632 
633 const Key keyForRow(const kj::Own<Entry>& row) const {
634 return Key(row->specifier, row->type);
635 }
636 
637 bool matches(const kj::Own<Entry>& row, Key key) const {
638 return row->specifier == key.specifier && row->type == key.type;
639 }
640 
641 uint hashCode(Key key) const {
642 return key.hashCode();
643 }
644 };
645 
646 kj::Table<kj::Own<Entry>, kj::HashIndex<SpecifierHashCallbacks>> entries;
647 kj::HashMap<kj::String, kj::String> fallbackServiceRedirects;
648};
649 
650template <typename TypeWrapper>
651v8::MaybeLocal<v8::Promise> dynamicImportCallback(v8::Local<v8::Context> context,
652 v8::Local<v8::Data> host_defined_options,
653 v8::Local<v8::Value> resource_name,
654 v8::Local<v8::String> specifier,
655 v8::Local<v8::FixedArray> import_attributes) {
656 auto& js = Lock::current();
657 auto registry = ModuleRegistry::from(js);
658 auto& wrapper = TypeWrapper::from(js.v8Isolate);
659 
660 // TODO(cleanup): This could probably be simplified using jsg::Promise
661 const auto makeRejected = [&](auto reason) {
662 v8::Local<v8::Promise::Resolver> resolver;
663 if (v8::Promise::Resolver::New(context).ToLocal(&resolver) &&
664 resolver->Reject(context, reason).IsJust()) {
665 return resolver->GetPromise();
666 }
667 return v8::Local<v8::Promise>();
668 };
669 
670 // The specification for import attributes strongly recommends that embedders
671 // reject import attributes and types they do not understand/implement. This
672 // is because import attributes can alter the interpretation of a module and
673 // are considered to be part of the unique key for caching a module.
674 // Throwing an error for things we do not understand is the safest thing to do.
675 // However, historically we have not followed this guideline in the spec
676 // and unfortunately there are applications deployed that will break if we
677 // started enforcing that guideline without a compat flag.
678 if (!import_attributes.IsEmpty() && import_attributes->Length() > 0 &&
679 js.getThrowOnUnrecognizedImportAssertion()) {
680 return makeRejected(js.v8Error("Unrecognized import attributes specified"));
681 }
682 
683 // The dynamic import might be resolved synchronously or asynchronously.
684 // Accordingly, resolveDynamicImport will return a jsg::Promise<jsg::Value>
685 // that will resolve to the module's namespace object or will reject if there
686 // was any error.
687 //
688 // Importantly, we defensively catch any synchronous errors here and handle them
689 // explicitly as rejected Promises.
690 v8::TryCatch tryCatch(js.v8Isolate);
691 
692 // TODO(cleanup): If kj::Path::parse or kj::Path::eval fail it is most likely the application's
693 // fault. We'll return a "No such module" error. We could handle this more gracefully
694 // if kj::Path had tryParse()/tryEval() variants.
695 
696 auto maybeReferrerPath = ([&]() -> kj::Maybe<kj::Path> {
697 try {
698 return kj::Path::parse(kj::str(resource_name));
699 } catch (kj::Exception& ex) {
700 return kj::none;
701 }
702 })();
703 
704 auto spec = kj::str(specifier);
705 if (isNodeJsCompatEnabled(js)) {
706 KJ_IF_SOME(nodeSpec, checkNodeSpecifier(spec)) {
707 spec = kj::mv(nodeSpec);
708 }
709 }
710 
711 // Handle process module redirection based on enable_nodejs_process_v2 flag
712 if (spec == "node:process") {
713 auto processSpec = isNodeJsProcessV2Enabled(js) ? "node-internal:public_process"_kj
714 : "node-internal:legacy_process"_kj;
715 try {
716 // Use resolveInternalImport for internal modules
717 auto moduleNamespace = registry->resolveInternalImport(js, processSpec);
718 v8::Local<v8::Promise::Resolver> resolver;
719 if (v8::Promise::Resolver::New(context).ToLocal(&resolver) &&
720 resolver->Resolve(context, moduleNamespace.getHandle(js)).IsJust()) {
721 return resolver->GetPromise();
722 }
723 return v8::Local<v8::Promise>();
724 } catch (JsExceptionThrown&) {
725 if (!tryCatch.CanContinue() || tryCatch.Exception().IsEmpty()) {
726 return v8::MaybeLocal<v8::Promise>();
727 }
728 return makeRejected(tryCatch.Exception());
729 } catch (kj::Exception& ex) {
730 return makeRejected(js.exceptionToJs(kj::mv(ex)).getHandle(js));
731 }
732 }
733 
734 auto maybeSpecifierPath = ([&]() -> kj::Maybe<kj::Path> {
735 // If the specifier begins with one of our known prefixes, let's not resolve
736 // it against the referrer.
737 if (spec.startsWith("node:") || spec.startsWith("cloudflare:") || spec.startsWith("workerd:")) {
738 return kj::Path::parse(spec);
739 }
740 KJ_IF_SOME(referrerPath, maybeReferrerPath) {
741 try {
742 return referrerPath.parent().eval(spec);
743 } catch (kj::Exception& ex) {
744 return kj::none;
745 }
746 }
747 return kj::none;
748 })();
749 
750 if (maybeReferrerPath == kj::none || maybeSpecifierPath == kj::none) {
751 // If either of these are nullptr it means the kj::Path::parse or
752 // kj::Path::eval failed. We want to handle these as No such module
753 // errors.
754 return makeRejected(js.v8Error(kj::str("No such module \"", specifier, "\"")));
755 }
756 
757 auto& referrerPath = KJ_ASSERT_NONNULL(maybeReferrerPath);
758 auto& specifierPath = KJ_ASSERT_NONNULL(maybeSpecifierPath);
759 
760 try {
761 return wrapper.wrap(js, context, kj::none,
762 registry->resolveDynamicImport(js, specifierPath, referrerPath, spec));
763 } catch (JsExceptionThrown&) {
764 // If the tryCatch.Exception().IsEmpty() here is true, no JavaScript error
765 // was scheduled which can happen in a few edge cases. Treat it as if
766 // CanContinue() is false.
767 if (!tryCatch.CanContinue() || tryCatch.Exception().IsEmpty()) {
768 // There's nothing else we can reasonably do.
769 return v8::MaybeLocal<v8::Promise>();
770 }
771 
772 return makeRejected(tryCatch.Exception());
773 } catch (kj::Exception& ex) {
774 return makeRejected(exceptionToJs(js.v8Isolate, kj::mv(ex)));
775 }
776 KJ_UNREACHABLE;
777}
778 
779ModuleRegistry* getModulesForResolveCallback(v8::Isolate* isolate);
780 
781} // namespace workerd::jsg