1use std::path::Path;
8use std::ptr;
9
10use mozjs::jsapi::{
11 CompileJsonModule, CompileModule, CreateModuleRequest, GetModuleRequestSpecifier, GetModuleRequestType, Handle,
12 JS_GetRuntime, JSContext, JSObject, ModuleEvaluate, ModuleIsLinked, ModuleLink, ModuleType as JSModuleType,
13 SetModuleMetadataHook, SetModulePrivate, SetModuleResolveHook,
14};
15use mozjs::jsval::JSVal;
16use mozjs::rust::{CompileOptionsWrapper, transform_u16_to_source_text};
17
18use crate::conversions::{FromValue as _, ToValue as _};
19use crate::{Context, Error, ErrorReport, Local, Object, Promise, ThrowException as _, Value};
20
21#[derive(Clone, Debug)]
23pub struct ModuleData {
24 pub path: Option<String>,
25}
26
27impl ModuleData {
28 pub fn from_private(cx: &Context, private: &Value) -> Option<ModuleData> {
30 private.handle().is_object().then(|| {
31 let private = private.to_object(cx);
32 let path: Option<String> = private.get_as(cx, "path", true, ()).unwrap();
33 ModuleData { path }
34 })
35 }
36
37 pub fn to_object<'cx>(&self, cx: &'cx Context) -> Object<'cx> {
39 let object = Object::new(cx);
40 object.set_as(cx, "path", &self.path);
41 object
42 }
43}
44
45#[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq)]
46pub enum ModuleType {
47 #[default]
48 JavaScript,
49 Json,
50}
51
52impl From<JSModuleType> for ModuleType {
53 fn from(ty: JSModuleType) -> ModuleType {
54 match ty {
55 JSModuleType::Unknown => panic!("Invalid Module Type"),
56 JSModuleType::JavaScript => ModuleType::JavaScript,
57 JSModuleType::JSON => ModuleType::Json,
58 }
59 }
60}
61
62impl From<ModuleType> for JSModuleType {
63 fn from(ty: ModuleType) -> JSModuleType {
64 match ty {
65 ModuleType::JavaScript => JSModuleType::JavaScript,
66 ModuleType::Json => JSModuleType::JSON,
67 }
68 }
69}
70
71#[derive(Debug)]
73pub struct ModuleRequest<'r>(Object<'r>);
74
75impl<'r> ModuleRequest<'r> {
76 pub fn new<S: AsRef<str>>(cx: &'r Context, specifier: S, ty: ModuleType) -> ModuleRequest<'r> {
78 let specifier = crate::String::copy_from_str(cx, specifier.as_ref()).unwrap();
79 ModuleRequest(
80 cx.root(unsafe { CreateModuleRequest(cx.as_ptr(), specifier.handle().into(), ty.into()) })
81 .into(),
82 )
83 }
84
85 pub unsafe fn from_raw_request(request: Handle<*mut JSObject>) -> ModuleRequest<'r> {
90 ModuleRequest(Object::from(unsafe { Local::from_raw_handle(request) }))
91 }
92
93 pub fn specifier<'cx>(&self, cx: &'cx Context) -> crate::String<'cx> {
95 cx.root(unsafe { GetModuleRequestSpecifier(cx.as_ptr(), self.0.handle().into()) }).into()
96 }
97
98 pub fn kind(&self, cx: &Context) -> ModuleType {
99 unsafe { GetModuleRequestType(cx.as_ptr(), self.0.handle().into()) }.into()
100 }
101}
102
103#[derive(Clone, Debug, PartialEq, Eq)]
105pub enum ModuleErrorKind {
106 Compilation,
107 Instantiation,
108 Evaluation,
109}
110
111#[derive(Clone, Debug)]
113pub struct ModuleError {
114 pub kind: ModuleErrorKind,
115 pub report: ErrorReport,
116}
117
118impl ModuleError {
119 fn new(report: ErrorReport, kind: ModuleErrorKind) -> ModuleError {
121 ModuleError { kind, report }
122 }
123
124 pub fn format(&self, cx: &Context) -> String {
126 self.report.format(cx)
127 }
128}
129
130#[derive(Debug)]
132pub struct Module<'m>(pub Object<'m>);
133
134impl<'cx> Module<'cx> {
135 #[expect(clippy::result_large_err)]
137 pub fn compile(
138 cx: &'cx Context, filename: &str, path: Option<&Path>, script: &str, kind: ModuleType,
139 ) -> Result<Module<'cx>, ModuleError> {
140 let script: Vec<_> = script.encode_utf16().collect();
141 let mut source = transform_u16_to_source_text(script.as_slice());
142 let filename = path.and_then(Path::to_str).unwrap_or(filename);
143 let options = unsafe { CompileOptionsWrapper::new(cx.as_ptr(), filename, 1) };
144 unsafe {
145 (*options.ptr)._base.prefableOptions_.set_importAttributes_(true);
146 }
147
148 let module = match kind {
149 ModuleType::JavaScript => unsafe { CompileModule(cx.as_ptr(), options.ptr.cast_const(), &raw mut source) },
150 ModuleType::Json => unsafe { CompileJsonModule(cx.as_ptr(), options.ptr.cast_const(), &raw mut source) },
151 };
152
153 if !module.is_null() {
154 let module = Module(Object::from(cx.root(module)));
155
156 let data = ModuleData {
157 path: path.and_then(Path::to_str).map(String::from),
158 };
159
160 unsafe {
161 let private = data.to_object(cx).as_value(cx);
162 SetModulePrivate(module.0.handle().get(), &raw const *private.handle());
163 }
164
165 Ok(module)
166 } else {
167 Err(ModuleError::new(
168 ErrorReport::new(cx).unwrap().unwrap(),
169 ModuleErrorKind::Compilation,
170 ))
171 }
172 }
173
174 #[expect(clippy::result_large_err)]
178 pub fn compile_and_evaluate(
179 cx: &'cx Context, filename: &str, path: Option<&Path>, script: &str,
180 ) -> Result<(Module<'cx>, Option<Promise<'cx>>), ModuleError> {
181 let module = Module::compile(cx, filename, path, script, ModuleType::JavaScript)?;
182
183 if let Err(error) = module.link(cx) {
184 return Err(ModuleError::new(error, ModuleErrorKind::Instantiation));
185 }
186
187 match module.evaluate(cx) {
188 Ok(val) => {
189 let promise = Promise::from_value(cx, &val, true, ()).ok();
190 Ok((module, promise))
191 }
192 Err(error) => Err(ModuleError::new(error, ModuleErrorKind::Evaluation)),
193 }
194 }
195
196 pub fn link(&self, cx: &Context) -> Result<(), ErrorReport> {
198 if unsafe { ModuleLink(cx.as_ptr(), self.0.handle().into()) } {
199 Ok(())
200 } else {
201 Err(ErrorReport::new(cx)?.unwrap())
202 }
203 }
204
205 pub fn evaluate(&self, cx: &'cx Context) -> Result<Value<'cx>, ErrorReport> {
207 let mut rval = Value::undefined(cx);
208 if unsafe { ModuleEvaluate(cx.as_ptr(), self.0.handle().into(), rval.handle_mut().into()) } {
209 Ok(rval)
210 } else {
211 Err(ErrorReport::new_with_exception_stack(cx)?.unwrap())
212 }
213 }
214
215 pub fn is_linked(&self) -> bool {
217 unsafe { ModuleIsLinked(self.0.handle().get()) }
218 }
219}
220
221pub trait ModuleLoader {
223 fn resolve<'cx>(
226 &mut self, cx: &'cx Context, private: &Value, request: &ModuleRequest,
227 ) -> crate::Result<Module<'cx>>;
228
229 fn register(&mut self, cx: &Context, module: *mut JSObject, request: &ModuleRequest) -> crate::Result<()>;
231
232 fn metadata(&self, cx: &Context, private: &Value, meta: &Object) -> crate::Result<()>;
234}
235
236impl ModuleLoader for () {
237 fn resolve<'cx>(&mut self, _: &'cx Context, _: &Value, _: &ModuleRequest) -> crate::Result<Module<'cx>> {
238 Err(Error::new("Modules are unsupported by this loader.", None))
239 }
240
241 fn register(&mut self, _: &Context, _: *mut JSObject, _: &ModuleRequest) -> crate::Result<()> {
242 Ok(())
243 }
244
245 fn metadata(&self, _: &Context, _: &Value, _: &Object) -> crate::Result<()> {
246 Ok(())
247 }
248}
249
250pub fn init_module_loader<ML: ModuleLoader + 'static>(cx: &Context, loader: ML) {
252 unsafe extern "C" fn resolve(
253 cx: *mut JSContext, private: Handle<JSVal>, request: Handle<*mut JSObject>,
254 ) -> *mut JSObject {
255 let cx = &unsafe { Context::new_unchecked(cx) };
256
257 let loader = unsafe { &mut (*cx.get_inner_data().as_ptr()).module_loader };
258 loader
259 .as_mut()
260 .and_then(|loader| {
261 let private = unsafe { Value::from(Local::from_raw_handle(private)) };
262 let request = unsafe { ModuleRequest::from_raw_request(request) };
263 match loader.resolve(cx, &private, &request) {
264 Ok(module) => Some(module.0.handle().get()),
265 Err(e) => {
266 e.throw(cx);
267 None
268 }
269 }
270 })
271 .unwrap_or_else(ptr::null_mut)
272 }
273
274 unsafe extern "C" fn metadata(
275 cx: *mut JSContext, private_data: Handle<JSVal>, metadata: Handle<*mut JSObject>,
276 ) -> bool {
277 let cx = &unsafe { Context::new_unchecked(cx) };
278
279 let loader = unsafe { &mut (*cx.get_inner_data().as_ptr()).module_loader };
280 loader.as_mut().is_none_or(|loader| {
281 let private = Value::from(unsafe { Local::from_raw_handle(private_data) });
282 let metadata = Object::from(unsafe { Local::from_raw_handle(metadata) });
283 match loader.metadata(cx, &private, &metadata) {
284 Ok(_) => true,
285 Err(e) => {
286 e.throw(cx);
287 false
288 }
289 }
290 })
291 }
292
293 unsafe {
294 (*cx.get_inner_data().as_ptr()).module_loader = Some(Box::new(loader));
295
296 let rt = JS_GetRuntime(cx.as_ptr());
297 SetModuleResolveHook(rt, Some(resolve));
298 SetModuleMetadataHook(rt, Some(metadata));
299 }
300}