char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somsnapshot: capture and restore extension layouts in one artifactd71572b

main
29.9 KiB850 linesraw
1use std::cell::{Cell, OnceCell, RefCell};
2use std::collections::HashMap;
3use std::rc::Rc;
4
5use futures::future::LocalBoxFuture;
6
7use crate::Error;
8
9/// Shared module text that retains the original String buffer.
10#[derive(Clone, Debug)]
11pub struct ModuleSource(Rc<String>);
12
13impl From<String> for ModuleSource {
14    fn from(source: String) -> Self {
15        Self(Rc::new(source))
16    }
17}
18
19impl From<&str> for ModuleSource {
20    fn from(source: &str) -> Self {
21        Self::from(source.to_owned())
22    }
23}
24
25impl std::ops::Deref for ModuleSource {
26    type Target = str;
27
28    fn deref(&self) -> &str {
29        &self.0
30    }
31}
32
33/// Execution format for a prepared module.
34#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
35pub enum ModuleFormat {
36    Esm,
37    CommonJs,
38    Json,
39}
40
41pub trait ModuleLoader {
42    fn resolve(&self, specifier: &str, referrer: &str) -> Result<String, Error>;
43    fn load(&self, name: &str) -> Result<ModuleSource, Error>;
44
45    /// Format metadata for synchronous `require` and ESM interop.
46    /// Defaults to ESM so existing loaders keep working unchanged.
47    fn module_format(&self, _name: &str) -> Result<ModuleFormat, Error> {
48        Ok(ModuleFormat::Esm)
49    }
50
51    /// Synchronous `require` resolution. Defaults to ESM `resolve`.
52    fn require_resolve(&self, specifier: &str, referrer: &str) -> Result<String, Error> {
53        self.resolve(specifier, referrer)
54    }
55
56    /// Statically discovered named exports for a CommonJS module.
57    /// Must not evaluate source; defaults to no named exports.
58    fn cjs_named_exports(&self, _name: &str) -> Result<Vec<String>, Error> {
59        Ok(Vec::new())
60    }
61
62    /// Acquire a module's static closure before V8 links it. Linking itself never awaits.
63    fn prepare(
64        &self,
65        specifier: String,
66        referrer: String,
67    ) -> LocalBoxFuture<'static, Result<String, Error>> {
68        let result = if referrer.is_empty() {
69            Ok(specifier)
70        } else {
71            self.resolve(&specifier, &referrer)
72        };
73        Box::pin(futures::future::ready(result))
74    }
75
76    /// Called before using a loader-backed module, including cached modules and queued imports.
77    fn check_access(&self, _name: &str) -> Result<(), Error> {
78        Ok(())
79    }
80
81    /// Resolve synchronously without acquiring or evaluating the target module.
82    fn resolve_import_meta(&self, specifier: &str, referrer: &str) -> Result<String, Error> {
83        let name = self.resolve(specifier, referrer)?;
84        if name.starts_with("ext:") {
85            return Err(Error::Module("Extension modules are private".into()));
86        }
87        Ok(self.module_url(&name).unwrap_or(name))
88    }
89
90    /// URL exposed as `import.meta.url`, if this loader has one.
91    fn module_url(&self, _name: &str) -> Option<String> {
92        None
93    }
94
95    /// Map a generated position to its source; lines and UTF-16 columns are one-based.
96    fn map_source_location(
97        &self,
98        _name: &str,
99        _line: u32,
100        _column: u32,
101    ) -> Option<(String, u32, u32)> {
102        None
103    }
104}
105
106pub(crate) struct ImportPreparation {
107    pub resolver: v8::Global<v8::PromiseResolver>,
108    pub result: Result<String, Error>,
109}
110
111pub(crate) fn prepare_entry(isolate: &mut v8::Isolate, name: &str) -> Result<String, Error> {
112    if name.starts_with("ext:") {
113        return Err(Error::Module("Extension modules are private".into()));
114    }
115    let map = isolate.get_slot::<Rc<ModuleMap>>().unwrap();
116    if map.is_builtin(name) {
117        return Ok(name.to_owned());
118    }
119    match &map.loader {
120        Some(loader) => futures::executor::block_on(loader.prepare(name.to_owned(), String::new())),
121        None => Err(Error::Module("No module loader configured".into())),
122    }
123}
124
125type Exports = Vec<(String, v8::Global<v8::Value>)>;
126
127mod commonjs;
128
129#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
130struct ModuleId(u32);
131
132struct Record {
133    name: Rc<str>,
134    module: v8::Global<v8::Module>,
135    builtin: Option<Exports>,
136    dependencies: Vec<ModuleId>,
137    visited: u32,
138    cjs_exports: Option<Vec<String>>,
139}
140
141struct ModuleMap {
142    loader: Option<Rc<dyn ModuleLoader>>,
143    records: RefCell<Vec<Record>>,
144    index: RefCell<HashMap<Rc<str>, ModuleId>>,
145    // Identity hashes collide, so each bucket holds candidates confirmed by Local == Global.
146    by_identity: RefCell<HashMap<i32, Vec<ModuleId>>>,
147    epoch: Cell<u32>,
148    main: OnceCell<String>,
149    repl_referrer: RefCell<Option<String>>,
150    // Per-isolate CommonJS state shared by `require` and ESM wrappers.
151    // No global registry: everything lives in the isolate slot.
152    cjs_modules: RefCell<HashMap<Rc<str>, v8::Global<v8::Object>>>,
153    cjs_deps: RefCell<HashMap<Rc<str>, Vec<String>>>,
154    json_modules: RefCell<HashMap<Rc<str>, v8::Global<v8::Value>>>,
155}
156
157impl ModuleMap {
158    fn id_of(&self, name: &str) -> Option<ModuleId> {
159        self.index.borrow().get(name).copied()
160    }
161
162    fn is_builtin(&self, name: &str) -> bool {
163        let Some(id) = self.id_of(name) else {
164            return false;
165        };
166        self.records.borrow()[id.0 as usize].builtin.is_some()
167    }
168
169    fn next_epoch(&self) -> u32 {
170        let mut epoch = self.epoch.get().wrapping_add(1);
171        if epoch == 0 {
172            for record in self.records.borrow_mut().iter_mut() {
173                record.visited = 0;
174            }
175            epoch = 1;
176        }
177        self.epoch.set(epoch);
178        epoch
179    }
180
181    fn insert(
182        &self,
183        name: Rc<str>,
184        module: v8::Global<v8::Module>,
185        builtin: Option<Exports>,
186        hash: i32,
187    ) -> ModuleId {
188        // Only called on cache miss: register() rejects duplicates and load()
189        // returns early when the name is already indexed.
190        let id = ModuleId(self.records.borrow().len() as u32);
191        self.records.borrow_mut().push(Record {
192            name: name.clone(),
193            module,
194            builtin,
195            dependencies: Vec::new(),
196            visited: 0,
197            cjs_exports: None,
198        });
199        self.index.borrow_mut().insert(name, id);
200        self.by_identity
201            .borrow_mut()
202            .entry(hash)
203            .or_default()
204            .push(id);
205        id
206    }
207}
208
209pub(crate) fn clear(isolate: &mut v8::Isolate) {
210    isolate.remove_slot::<Rc<ModuleMap>>();
211}
212
213pub(crate) fn external_references() -> Vec<v8::ExternalReference> {
214    use v8::MapFnTo;
215    vec![v8::ExternalReference {
216        function: resolve_import_meta.map_fn_to(),
217    }]
218}
219
220pub(crate) fn has_modules(isolate: &v8::Isolate) -> bool {
221    !isolate
222        .get_slot::<Rc<ModuleMap>>()
223        .unwrap()
224        .records
225        .borrow()
226        .is_empty()
227}
228
229pub(crate) fn init(isolate: &mut v8::Isolate, loader: Option<Rc<dyn ModuleLoader>>) {
230    isolate.set_slot(Rc::new(ModuleMap {
231        loader,
232        records: RefCell::new(Vec::new()),
233        index: RefCell::new(HashMap::new()),
234        by_identity: RefCell::new(HashMap::new()),
235        epoch: Cell::new(0),
236        main: OnceCell::new(),
237        repl_referrer: RefCell::new(None),
238        cjs_modules: RefCell::new(HashMap::new()),
239        cjs_deps: RefCell::new(HashMap::new()),
240        json_modules: RefCell::new(HashMap::new()),
241    }));
242    isolate.set_host_import_module_dynamically_callback(import_dynamic);
243    isolate.set_host_initialize_import_meta_object_callback(import_meta);
244}
245
246pub(crate) fn set_repl_referrer(scope: &mut v8::PinScope, referrer: Option<&str>) -> Option<()> {
247    let map = scope.get_slot::<Rc<ModuleMap>>()?;
248    *map.repl_referrer.borrow_mut() = referrer.map(str::to_owned);
249    Some(())
250}
251
252pub(crate) fn register(
253    scope: &mut v8::PinScope,
254    name: &str,
255    exports: v8::Local<v8::Object>,
256) -> Option<()> {
257    check_guest_name(scope, name)?;
258    let map = scope.get_slot::<Rc<ModuleMap>>().unwrap().clone();
259    if map.id_of(name).is_some() {
260        return fail(scope, format!("Module already registered: {name}"));
261    }
262    let keys = exports.get_own_property_names(scope, Default::default())?;
263    let mut names = Vec::new();
264    let mut values = Exports::new();
265    for index in 0..keys.length() {
266        let key = keys.get_index(scope, index)?;
267        let name = key.to_rust_string_lossy(scope);
268        let value = exports.get(scope, key)?;
269        names.push(v8::String::new(scope, &name)?);
270        values.push((name, v8::Global::new(scope, value)));
271    }
272    let module_name = v8::String::new(scope, name)?;
273    let module = v8::Module::create_synthetic_module(scope, module_name, &names, evaluate_builtin);
274    let hash = module.get_identity_hash().get();
275    map.insert(
276        name.into(),
277        v8::Global::new(scope, module),
278        Some(values),
279        hash,
280    );
281    Some(())
282}
283
284fn evaluate_builtin<'s>(
285    context: v8::Local<'s, v8::Context>,
286    module: v8::Local<'s, v8::Module>,
287) -> Option<v8::Local<'s, v8::Value>> {
288    // SAFETY: V8 invokes synthetic module evaluation with this context entered.
289    v8::callback_scope!(unsafe scope, context);
290    let id = module_id(scope, module);
291    let map = scope.get_slot::<Rc<ModuleMap>>().unwrap().clone();
292    // Only synthetic builtin modules carry this evaluation callback, so the
293    // record always has exports; panic like the old builtins-map indexing did.
294    let records = map.records.borrow();
295    let exports = records[id.0 as usize].builtin.as_ref().unwrap();
296    for (key, value) in exports {
297        let key = v8::String::new(scope, key)?;
298        let value = v8::Local::new(scope, value);
299        module.set_synthetic_module_export(scope, key, value)?;
300    }
301    let resolver = v8::PromiseResolver::new(scope)?;
302    resolver.resolve(scope, v8::undefined(scope).into())?;
303    Some(resolver.get_promise(scope).into())
304}
305
306pub(crate) fn map_source_location_op(
307    scope: &mut v8::PinScope,
308    args: v8::FunctionCallbackArguments,
309    mut result: v8::ReturnValue,
310) {
311    let name = args.get(0).to_rust_string_lossy(scope);
312    let Some(line) = args.get(1).uint32_value(scope) else {
313        return;
314    };
315    let Some(column) = args.get(2).uint32_value(scope) else {
316        return;
317    };
318    if let Some(location) = map_source_location(scope, &name, line, column) {
319        result.set(serde_v8::to_v8(scope, location).unwrap());
320    }
321}
322
323pub(crate) fn map_source_location(
324    scope: &mut v8::PinScope,
325    name: &str,
326    line: u32,
327    column: u32,
328) -> Option<(String, u32, u32)> {
329    scope
330        .get_slot::<Rc<ModuleMap>>()?
331        .loader
332        .as_ref()?
333        .map_source_location(name, line, column)
334}
335
336fn fail_syntax<T>(scope: &mut v8::PinScope, message: impl AsRef<str>) -> Option<T> {
337    let message = v8::String::new(scope, message.as_ref()).unwrap();
338    let error = v8::Exception::syntax_error(scope, message);
339    scope.throw_exception(error);
340    None
341}
342
343fn fail<T>(scope: &mut v8::PinScope, message: impl AsRef<str>) -> Option<T> {
344    let message = v8::String::new(scope, message.as_ref()).unwrap();
345    let error = v8::Exception::error(scope, message);
346    scope.throw_exception(error);
347    None
348}
349
350fn check_guest_name(scope: &mut v8::PinScope, name: &str) -> Option<()> {
351    if name.starts_with("ext:") {
352        fail(scope, "Extension modules are private")
353    } else {
354        Some(())
355    }
356}
357
358fn load<'s>(scope: &mut v8::PinScope<'s, '_>, name: &str) -> Option<v8::Local<'s, v8::Module>> {
359    check_guest_name(scope, name)?;
360    let Some(map) = scope.get_slot::<Rc<ModuleMap>>().cloned() else {
361        return fail(scope, "No module loader configured");
362    };
363    let is_builtin = map.is_builtin(name);
364    if !is_builtin
365        && let Some(loader) = &map.loader
366        && let Err(error) = loader.check_access(name)
367    {
368        return match error {
369            Error::Syntax(message) => fail_syntax(scope, message),
370            error => fail(scope, error.to_string()),
371        };
372    }
373    if let Some(id) = map.id_of(name) {
374        // Local::new only creates a handle; no loader or V8 callback runs
375        // while borrowed, so holding the borrow avoids a Global clone.
376        let records = map.records.borrow();
377        return Some(v8::Local::new(scope, &records[id.0 as usize].module));
378    }
379
380    let Some(loader) = map.loader.clone() else {
381        return fail(scope, "No module loader configured");
382    };
383    let format = match loader.module_format(name) {
384        Ok(format) => format,
385        Err(Error::Syntax(message)) => return fail_syntax(scope, message),
386        Err(error) => return fail(scope, error.to_string()),
387    };
388    match format {
389        ModuleFormat::Esm => {
390            let source = match loader.load(name) {
391                Ok(source) => source,
392                Err(Error::Syntax(message)) => return fail_syntax(scope, message),
393                Err(error) => return fail(scope, error.to_string()),
394            };
395            let mut source = crate::source(scope, name, &source, true, None);
396            let module = v8::script_compiler::compile_module(scope, &mut source)?;
397            let hash = module.get_identity_hash().get();
398            map.insert(name.into(), v8::Global::new(scope, module), None, hash);
399            Some(module)
400        }
401        ModuleFormat::CommonJs => commonjs::load_synthetic(scope, &map, &loader, name),
402        ModuleFormat::Json => fail(
403            scope,
404            format!("JSON modules are not supported as ESM (use require() instead): {name}"),
405        ),
406    }
407}
408
409fn resolve<'s>(
410    context: v8::Local<'s, v8::Context>,
411    specifier: v8::Local<'s, v8::String>,
412    attributes: v8::Local<'s, v8::FixedArray>,
413    referrer: v8::Local<'s, v8::Module>,
414) -> Option<v8::Local<'s, v8::Module>> {
415    // SAFETY: V8 invokes this callback with the supplied context entered.
416    v8::callback_scope!(unsafe scope, context);
417    if attributes.length() != 0 {
418        return fail(scope, "Import attributes are not supported yet");
419    }
420    let referrer_id = module_id(scope, referrer);
421    let referrer = scope.get_slot::<Rc<ModuleMap>>().unwrap().records.borrow()
422        [referrer_id.0 as usize]
423        .name
424        .to_string();
425    let specifier = specifier.to_rust_string_lossy(scope);
426    let name = resolve_name(scope, &specifier, &referrer)?;
427    let module = load(scope, &name)?;
428    let map = scope.get_slot::<Rc<ModuleMap>>().unwrap().clone();
429    // load() inserts on cache miss, so the name is always indexed here.
430    let dependency = map.id_of(&name).unwrap();
431    map.records.borrow_mut()[referrer_id.0 as usize]
432        .dependencies
433        .push(dependency);
434    Some(module)
435}
436
437fn evaluate<'s>(
438    scope: &mut v8::PinScope<'s, '_>,
439    name: &str,
440) -> Option<(v8::Local<'s, v8::Module>, v8::Local<'s, v8::Promise>)> {
441    let module = load(scope, name)?;
442    let map = scope.get_slot::<Rc<ModuleMap>>().unwrap().clone();
443    let root = map.id_of(name).unwrap();
444    if module.get_status() == v8::ModuleStatus::Uninstantiated {
445        // Clear failed attempts before linking, not on repeated resolution within a diamond or cycle.
446        // Local::new and get_status only inspect handles; no loader or V8
447        // callback runs while borrowed, so no Global clones are needed.
448        let uninstantiated = {
449            let records = map.records.borrow();
450            records
451                .iter()
452                .enumerate()
453                .filter_map(|(index, record)| {
454                    (v8::Local::new(scope, &record.module).get_status()
455                        == v8::ModuleStatus::Uninstantiated)
456                        .then_some(ModuleId(index as u32))
457                })
458                .collect::<Vec<_>>()
459        };
460        if !uninstantiated.is_empty() {
461            let mut records = map.records.borrow_mut();
462            for id in uninstantiated {
463                records[id.0 as usize].dependencies.clear();
464            }
465        }
466        if module.instantiate_module(scope, resolve) != Some(true) {
467            return None;
468        }
469    }
470    // Check the recorded links, not a fresh resolution that could identify different modules.
471    let epoch = map.next_epoch();
472    let mut pending = vec![root];
473    while let Some(id) = pending.pop() {
474        let (status, is_builtin, dependencies, record_name) = {
475            let mut records = map.records.borrow_mut();
476            let record = &mut records[id.0 as usize];
477            if record.visited == epoch {
478                continue;
479            }
480            record.visited = epoch;
481            // Local::new and get_status only inspect handles; the borrow is
482            // dropped before the loader check below, which may reenter.
483            let status = v8::Local::new(scope, &record.module).get_status();
484            (
485                status,
486                record.builtin.is_some(),
487                record.dependencies.clone(),
488                record.name.clone(),
489            )
490        };
491        // Cached CJS closures must not bypass a later pledge even when the
492        // synthetic wrapper is already evaluated: check transitively.
493        // Traverse ESM dependencies even for evaluated modules so an ESM
494        // intermediary cannot hide a revoked CJS leaf.
495        if !is_builtin
496            && let Some(loader) = &map.loader
497            && commonjs::check_closure(scope, &map, loader, &record_name).is_none()
498        {
499            return None;
500        }
501        pending.extend(dependencies);
502        if status != v8::ModuleStatus::Instantiated {
503            continue;
504        }
505        if !is_builtin
506            && let Some(loader) = &map.loader
507            && let Err(error) = loader.check_access(&record_name)
508        {
509            return match error {
510                Error::Syntax(message) => fail_syntax(scope, message),
511                error => fail(scope, error.to_string()),
512            };
513        }
514    }
515    // V8 caches the cycle root's evaluation promise, including pending top-level await.
516    let value = module.evaluate(scope)?;
517    let promise = v8::Local::<v8::Promise>::try_from(value).unwrap();
518    promise.mark_as_handled();
519    Some((module, promise))
520}
521
522pub(crate) fn execute<'s>(
523    scope: &mut v8::PinScope<'s, '_>,
524    name: &str,
525) -> Option<v8::Local<'s, v8::Promise>> {
526    let Some(map) = scope.get_slot::<Rc<ModuleMap>>().cloned() else {
527        return fail(scope, "No module loader configured");
528    };
529    check_guest_name(scope, name)?;
530    map.main.get_or_init(|| name.to_owned());
531    evaluate(scope, name).map(|(_, promise)| promise)
532}
533
534pub(crate) fn namespace<'s>(
535    scope: &mut v8::PinScope<'s, '_>,
536    name: &str,
537) -> Option<v8::Local<'s, v8::Value>> {
538    let map = scope.get_slot::<Rc<ModuleMap>>()?;
539    let id = map.id_of(name)?;
540    let records = map.records.borrow();
541    // Local::new only creates a handle; no loader or V8 callback runs while borrowed.
542    Some(v8::Local::new(scope, &records[id.0 as usize].module).get_module_namespace())
543}
544
545fn module_id(scope: &mut v8::PinScope, module: v8::Local<v8::Module>) -> ModuleId {
546    let map = scope.get_slot::<Rc<ModuleMap>>().unwrap().clone();
547    let hash = module.get_identity_hash().get();
548    let by_identity = map.by_identity.borrow();
549    let records = map.records.borrow();
550    // Local::new and handle equality only inspect handles; no loader or V8
551    // callback runs while borrowed, so the bucket and Globals need no clones.
552    if let Some(candidates) = by_identity.get(&hash) {
553        for id in candidates {
554            if v8::Local::new(scope, &records[id.0 as usize].module) == module {
555                return *id;
556            }
557        }
558    }
559    unreachable!("module not in map")
560}
561
562fn module_name(scope: &mut v8::PinScope, module: v8::Local<v8::Module>) -> String {
563    let map = scope.get_slot::<Rc<ModuleMap>>().unwrap().clone();
564    let id = module_id(scope, module);
565    map.records.borrow()[id.0 as usize].name.to_string()
566}
567
568fn resolve_name(scope: &mut v8::PinScope, specifier: &str, referrer: &str) -> Option<String> {
569    check_guest_name(scope, specifier)?;
570    let map = scope.get_slot::<Rc<ModuleMap>>().unwrap().clone();
571    if map.is_builtin(specifier) {
572        return Some(specifier.to_owned());
573    }
574    let Some(loader) = map.loader.clone() else {
575        return fail(scope, "No module loader configured");
576    };
577    match loader.resolve(specifier, referrer) {
578        Ok(name) => {
579            check_guest_name(scope, &name)?;
580            Some(name)
581        }
582        Err(error) => fail(scope, error.to_string()),
583    }
584}
585
586unsafe extern "C" fn import_meta(
587    context: v8::Local<v8::Context>,
588    module: v8::Local<v8::Module>,
589    meta: v8::Local<v8::Object>,
590) {
591    // SAFETY: V8 supplies the module's entered context.
592    v8::callback_scope!(unsafe scope, context);
593    let extension = crate::extensions::module_name(scope, module);
594    let name = extension
595        .clone()
596        .unwrap_or_else(|| module_name(scope, module));
597    let referrer = v8::String::new(scope, &name).unwrap();
598    let resolve = v8::Function::builder(resolve_import_meta)
599        .data(referrer.into())
600        .length(1)
601        .constructor_behavior(v8::ConstructorBehavior::Throw)
602        .build(scope)
603        .unwrap();
604    let function_name = v8::String::new(scope, "resolve").unwrap();
605    resolve.set_name(function_name);
606    meta.create_data_property(scope, function_name.into(), resolve.into())
607        .unwrap();
608    if extension.is_some() {
609        for (key, value) in [
610            ("main", v8::Boolean::new(scope, false).into()),
611            ("url", v8::String::new(scope, &name).unwrap().into()),
612        ] {
613            let key = v8::String::new(scope, key).unwrap();
614            meta.create_data_property(scope, key.into(), value).unwrap();
615        }
616        return;
617    }
618    let map = scope.get_slot::<Rc<ModuleMap>>().unwrap().clone();
619    let main = map.main.get() == Some(&name);
620    let key = v8::String::new(scope, "main").unwrap();
621    let value = v8::Boolean::new(scope, main);
622    meta.create_data_property(scope, key.into(), value.into())
623        .unwrap();
624    if let Some(url) = map
625        .loader
626        .as_ref()
627        .and_then(|loader| loader.module_url(&name))
628    {
629        let key = v8::String::new(scope, "url").unwrap();
630        let value = v8::String::new(scope, &url).unwrap();
631        meta.create_data_property(scope, key.into(), value.into())
632            .unwrap();
633        if let Ok(url) = url::Url::parse(&url)
634            && let Ok(path) = url.to_file_path()
635            && let Some(filename) = path.to_str()
636            && let Some(dirname) = path.parent().and_then(std::path::Path::to_str)
637        {
638            for (key, value) in [("filename", filename), ("dirname", dirname)] {
639                let key = v8::String::new(scope, key).unwrap();
640                let value = v8::String::new(scope, value).unwrap();
641                meta.create_data_property(scope, key.into(), value.into())
642                    .unwrap();
643            }
644        }
645    }
646}
647
648fn resolve_import_meta(
649    scope: &mut v8::PinScope,
650    args: v8::FunctionCallbackArguments,
651    mut result: v8::ReturnValue,
652) {
653    let Some(specifier) = args.get(0).to_string(scope) else {
654        return;
655    };
656    let specifier = specifier.to_rust_string_lossy(scope);
657    let referrer = args.data().to_rust_string_lossy(scope);
658    if referrer.starts_with("ext:") {
659        if let Some(name) = crate::extensions::resolve_name(scope, &specifier, &referrer)
660            && let Some(value) = v8::String::new(scope, &name)
661        {
662            result.set(value.into());
663        }
664        return;
665    }
666    let map = scope.get_slot::<Rc<ModuleMap>>().unwrap().clone();
667    let is_builtin = map.is_builtin(&specifier);
668    let loader = map.loader.clone();
669    let resolved = if specifier.starts_with("ext:") {
670        Err(Error::Module("Extension modules are private".into()))
671    } else if is_builtin {
672        Ok(specifier)
673    } else if let Some(loader) = &loader {
674        loader.resolve_import_meta(&specifier, &referrer)
675    } else {
676        Err(Error::Module("No module loader configured".into()))
677    };
678    match resolved {
679        Ok(name) if !name.starts_with("ext:") => {
680            if let Some(value) = v8::String::new(scope, &name) {
681                result.set(value.into());
682            }
683        }
684        other => {
685            let message = match other {
686                Err(error) => error.to_string(),
687                _ => "Extension modules are private".into(),
688            };
689            let message = v8::String::new(scope, &message).unwrap();
690            let error = v8::Exception::type_error(scope, message);
691            scope.throw_exception(error);
692        }
693    }
694}
695
696fn import_dynamic<'s>(
697    scope: &mut v8::PinScope<'s, '_>,
698    options: v8::Local<'s, v8::Data>,
699    referrer: v8::Local<'s, v8::Value>,
700    specifier: v8::Local<'s, v8::String>,
701    attributes: v8::Local<'s, v8::FixedArray>,
702) -> Option<v8::Local<'s, v8::Promise>> {
703    let resolver = v8::PromiseResolver::new(scope)?;
704    let promise = resolver.get_promise(scope);
705    v8::tc_scope!(let scope, scope);
706    let queued = (|| {
707        let specifier = specifier.to_rust_string_lossy(scope);
708        check_guest_name(scope, &specifier)?;
709        // eval inherits host options. This marker only restricts imports; it never grants access.
710        if crate::extensions::dynamic_imports_disabled(scope, options) {
711            return fail(
712                scope,
713                "not yet implemented: dynamic imports from extension modules",
714            );
715        }
716        if attributes.length() != 0 {
717            return fail(scope, "Import attributes are not supported yet");
718        }
719        let referrer = referrer.to_rust_string_lossy(scope);
720        let map = scope.get_slot::<Rc<ModuleMap>>().unwrap().clone();
721        let referrer = if referrer.is_empty() {
722            map.repl_referrer.borrow().clone().unwrap_or(referrer)
723        } else {
724            referrer
725        };
726        let is_builtin = map.is_builtin(&specifier);
727        let loader = map.loader.clone();
728        let future = if is_builtin {
729            Box::pin(futures::future::ready(Ok(specifier)))
730                as LocalBoxFuture<'static, Result<String, Error>>
731        } else {
732            let Some(loader) = loader else {
733                return fail(scope, "No module loader configured");
734            };
735            loader.prepare(specifier, referrer)
736        };
737        let resolver = v8::Global::new(scope, resolver);
738        crate::op::enqueue_import(
739            scope,
740            Box::pin(async move { ImportPreparation { resolver, result: future.await } }),
741        );
742        Some(())
743    })();
744    if queued.is_none() {
745        let error = scope.exception()?;
746        scope.reset();
747        resolver.reject(scope, error);
748    }
749    Some(promise)
750}
751
752pub(crate) fn import_prepared(scope: &mut v8::PinScope, preparation: ImportPreparation) {
753    let resolver = v8::Local::new(scope, preparation.resolver);
754    v8::tc_scope!(let scope, scope);
755    let queued = (|| {
756        let name = match preparation.result {
757            Ok(name) => name,
758            Err(Error::Syntax(message)) => {
759                let message = v8::String::new(scope, &message)?;
760                let error = v8::Exception::syntax_error(scope, message);
761                scope.throw_exception(error);
762                return None;
763            }
764            Err(error) => return fail(scope, error.to_string()),
765        };
766        check_guest_name(scope, &name)?;
767        let name = v8::String::new(scope, &name)?;
768        let data = v8::Array::new_with_elements(scope, &[name.into(), resolver.into()]);
769        let job = v8::Function::builder(import_job)
770            .data(data.into())
771            .constructor_behavior(v8::ConstructorBehavior::Throw)
772            .build(scope)?;
773        // Evaluation must not reenter a synchronously evaluating module (including itself).
774        scope.enqueue_microtask(job);
775        Some(())
776    })();
777    if queued.is_none()
778        && let Some(error) = scope.exception()
779    {
780        scope.reset();
781        resolver.reject(scope, error);
782    }
783}
784
785fn import_job<'s>(
786    scope: &mut v8::PinScope<'s, '_>,
787    args: v8::FunctionCallbackArguments<'s>,
788    _result: v8::ReturnValue,
789) {
790    let data = v8::Local::<v8::Array>::try_from(args.data()).unwrap();
791    let name = data
792        .get_index(scope, 0)
793        .unwrap()
794        .to_rust_string_lossy(scope);
795    // SAFETY: Private callback data contains the resolver created by import_dynamic.
796    // import_prepared stores it at index 1; guests cannot change that data.
797    let resolver = unsafe {
798        v8::Local::<v8::PromiseResolver>::cast_unchecked(data.get_index(scope, 1).unwrap())
799    };
800    v8::tc_scope!(let scope, scope);
801    let started = (|| {
802        let (module, evaluation) = evaluate(scope, &name)?;
803        let namespace = module.get_module_namespace();
804        let data = v8::Array::new_with_elements(scope, &[resolver.into(), namespace]);
805        let fulfilled = v8::Function::builder(import_fulfilled)
806            .data(data.into())
807            .constructor_behavior(v8::ConstructorBehavior::Throw)
808            .build(scope)?;
809        let rejected = v8::Function::builder(import_rejected)
810            .data(resolver.into())
811            .constructor_behavior(v8::ConstructorBehavior::Throw)
812            .build(scope)?;
813        evaluation
814            .then2(scope, fulfilled, rejected)?
815            .mark_as_handled();
816        Some(())
817    })();
818    if started.is_none()
819        && let Some(error) = scope.exception()
820    {
821        scope.reset();
822        resolver.reject(scope, error);
823    }
824}
825
826fn import_fulfilled<'s>(
827    scope: &mut v8::PinScope<'s, '_>,
828    args: v8::FunctionCallbackArguments<'s>,
829    _result: v8::ReturnValue,
830) {
831    let data = v8::Local::<v8::Array>::try_from(args.data()).unwrap();
832    // SAFETY: Private callback data contains the original import resolver.
833    // import_job stores it at index 0; guests cannot change that data.
834    let resolver = unsafe {
835        v8::Local::<v8::PromiseResolver>::cast_unchecked(data.get_index(scope, 0).unwrap())
836    };
837    let namespace = data.get_index(scope, 1).unwrap();
838    resolver.resolve(scope, namespace);
839}
840
841fn import_rejected<'s>(
842    scope: &mut v8::PinScope<'s, '_>,
843    args: v8::FunctionCallbackArguments<'s>,
844    _result: v8::ReturnValue,
845) {
846    // SAFETY: Private callback data contains the original import resolver.
847    // import_job installs the PromiseResolver directly; guests cannot change that data.
848    let resolver = unsafe { v8::Local::<v8::PromiseResolver>::cast_unchecked(args.data()) };
849    resolver.reject(scope, args.get(0));
850}