ion/
module.rs

1/*
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 */
6
7use 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/// Represents private module data
22#[derive(Clone, Debug)]
23pub struct ModuleData {
24	pub path: Option<String>,
25}
26
27impl ModuleData {
28	/// Creates [ModuleData] based on the private data of a module
29	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	/// Converts [ModuleData] to an [Object] for storage.
38	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/// Represents a request by the runtime for a module.
72#[derive(Debug)]
73pub struct ModuleRequest<'r>(Object<'r>);
74
75impl<'r> ModuleRequest<'r> {
76	/// Creates a new [ModuleRequest] with a given specifier.
77	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	/// Creates a new [ModuleRequest] from a raw handle.
86	///
87	/// ### Safety
88	/// `request` must be a valid module request object.
89	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	/// Returns the specifier of the request.
94	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/// Represents phases of running modules.
104#[derive(Clone, Debug, PartialEq, Eq)]
105pub enum ModuleErrorKind {
106	Compilation,
107	Instantiation,
108	Evaluation,
109}
110
111/// Represents errors that may occur when running modules.
112#[derive(Clone, Debug)]
113pub struct ModuleError {
114	pub kind: ModuleErrorKind,
115	pub report: ErrorReport,
116}
117
118impl ModuleError {
119	/// Creates a [ModuleError] with a given report and phase.
120	fn new(report: ErrorReport, kind: ModuleErrorKind) -> ModuleError {
121		ModuleError { kind, report }
122	}
123
124	/// Formats the [ModuleError] for printing.
125	pub fn format(&self, cx: &Context) -> String {
126		self.report.format(cx)
127	}
128}
129
130/// Represents a compiled module.
131#[derive(Debug)]
132pub struct Module<'m>(pub Object<'m>);
133
134impl<'cx> Module<'cx> {
135	/// Compiles a [Module] with the given source and filename.
136	#[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	/// Compiles and evaluates a [Module] with the given source and filename.
175	/// On success, returns the compiled module object and a promise. The promise resolves with the return value of the module.
176	/// The promise is a byproduct of enabling top-level await.
177	#[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	/// Links a [Module]. Generally called by [Module::compile_and_evaluate].
197	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	/// Evaluates a [Module]. Generally called by [Module::compile].
206	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	/// Returns `true` if the module has been linked.
216	pub fn is_linked(&self) -> bool {
217		unsafe { ModuleIsLinked(self.0.handle().get()) }
218	}
219}
220
221/// Represents an ES module loader.
222pub trait ModuleLoader {
223	/// Given a request and private data of a module, resolves the request into a compiled module object.
224	/// Should return the same module object for a given request.
225	fn resolve<'cx>(
226		&mut self, cx: &'cx Context, private: &Value, request: &ModuleRequest,
227	) -> crate::Result<Module<'cx>>;
228
229	/// Registers a new module in the module registry. Useful for native modules.
230	fn register(&mut self, cx: &Context, module: *mut JSObject, request: &ModuleRequest) -> crate::Result<()>;
231
232	/// Returns metadata of a module, used to populate `import.meta`.
233	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
250/// Initialises a module loader in the current runtime.
251pub 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}