Skip to main content

brep_script_core/
modules.rs

1//! In-memory ESM execution, separate from the legacy global-function hook API.
2use crate::{Failure, Logs};
3use boa_engine::builtins::promise::PromiseState;
4use boa_engine::module::{ModuleLoader, ModuleRequest, Referrer};
5use boa_engine::{Context, JsNativeError, JsResult, JsValue, Module, Source};
6use std::{cell::RefCell, collections::BTreeMap, path::Path, rc::Rc};
7
8struct Loader {
9    sources: BTreeMap<String, String>,
10    parsed: RefCell<BTreeMap<String, Module>>,
11}
12impl ModuleLoader for Loader {
13    async fn load_imported_module(
14        self: Rc<Self>,
15        referrer: Referrer,
16        request: ModuleRequest,
17        context: &RefCell<&mut Context>,
18    ) -> JsResult<Module> {
19        let spec = request.specifier().to_std_string_escaped();
20        if !(spec.starts_with("./") || spec.starts_with("../"))
21            || spec.contains('\\')
22            || !request.attributes().is_empty()
23        {
24            return Err(JsNativeError::typ()
25                .with_message(format!("unsupported package import {spec}"))
26                .into());
27        }
28        // These are virtual package keys, not filesystem paths. In particular,
29        // wasm32-unknown-unknown has no absolute filesystem path semantics.
30        let importer = referrer
31            .path()
32            .and_then(Path::to_str)
33            .ok_or_else(|| JsNativeError::typ().with_message("missing package referrer"))?;
34        let mut parts: Vec<&str> = importer.split('/').collect();
35        parts.pop();
36        for part in spec.split('/') {
37            match part {
38                "" | "." => {}
39                ".." => {
40                    if parts.pop().is_none() {
41                        return Err(JsNativeError::typ()
42                            .with_message("module outside package")
43                            .into());
44                    }
45                }
46                part => parts.push(part),
47            }
48        }
49        let mut cx = context.borrow_mut();
50        self.parse(&parts.join("/"), &mut cx)
51    }
52}
53impl Loader {
54    fn parse(&self, key: &str, cx: &mut Context) -> JsResult<Module> {
55        if key.is_empty()
56            || key
57                .split('/')
58                .any(|p| p.is_empty() || p == "." || p == "..")
59            || key.contains('\\')
60            || key.contains(':')
61        {
62            return Err(JsNativeError::typ()
63                .with_message("invalid package module path")
64                .into());
65        }
66        if let Some(m) = self.parsed.borrow().get(key) {
67            return Ok(m.clone());
68        }
69        let source = self.sources.get(key).ok_or_else(|| {
70            JsNativeError::typ().with_message(format!("missing package module {key}"))
71        })?;
72        let module = Module::parse(
73            Source::from_bytes(source).with_path(Path::new(key)),
74            None,
75            cx,
76        )
77        .map_err(|e| JsNativeError::syntax().with_message(format!("{key}: {e}")))?;
78        self.parsed
79            .borrow_mut()
80            .insert(key.to_owned(), module.clone());
81        Ok(module)
82    }
83}
84/// Reject asynchronous callbacks rather than serializing a Promise as `{}`.
85pub fn synchronous(value: JsValue) -> JsResult<JsValue> {
86    if value
87        .as_object()
88        .is_some_and(|o| o.is::<boa_engine::builtins::promise::Promise>())
89    {
90        Err(JsNativeError::typ()
91            .with_message("Promise results are unsupported in synchronous plugin callbacks")
92            .into())
93    } else {
94        Ok(value)
95    }
96}
97/// Load and evaluate a package-local ESM graph in a fresh, bounded context.
98/// Host setup and callback invocation occur within the same owning context.
99pub fn with_module<T>(
100    sources: &BTreeMap<String, String>,
101    entry: &str,
102    setup: impl FnOnce(&mut Context, &Logs) -> JsResult<()>,
103    invoke: impl FnOnce(&Module, &mut Context) -> JsResult<T>,
104) -> Result<T, Failure> {
105    let loader = Rc::new(Loader {
106        sources: sources.clone(),
107        parsed: RefCell::new(BTreeMap::new()),
108    });
109    let mut cx = Context::builder()
110        .module_loader(loader.clone())
111        .build()
112        .map_err(|e| Failure {
113            message: e.to_string(),
114            logs: vec![],
115        })?;
116    cx.runtime_limits_mut()
117        .set_loop_iteration_limit(crate::LOOP_ITERATION_LIMIT);
118    cx.runtime_limits_mut()
119        .set_recursion_limit(crate::RECURSION_LIMIT);
120    let logs = Rc::new(RefCell::new(Vec::new()));
121    let result = (|| {
122        crate::install_console(&mut cx, &logs)?;
123        setup(&mut cx, &logs)?;
124        let module = loader.parse(entry, &mut cx)?;
125        let promise = module.load_link_evaluate(&mut cx);
126        cx.run_jobs()?;
127        match promise.state() {
128            PromiseState::Fulfilled(_) => {}
129            PromiseState::Rejected(value) => return Err(boa_engine::JsError::from_opaque(value)),
130            PromiseState::Pending => {
131                return Err(JsNativeError::typ()
132                    .with_message("pending module evaluation is unsupported")
133                    .into())
134            }
135        }
136        invoke(&module, &mut cx)
137    })();
138    result.map_err(|e| {
139        let trace = e.to_string();
140        let stack = trace.find("\n    at ").map(|i| &trace[i..]).unwrap_or("");
141        Failure {
142            message: format!("{entry}: {}{stack}", crate::describe(e, &mut cx)),
143            logs: logs.borrow().clone(),
144        }
145    })
146}