arcane_engine/scripting/
module_loader.rs1use deno_ast::MediaType;
2use deno_ast::ParseParams;
3use deno_ast::TranspileModuleOptions;
4use deno_core::ModuleLoadResponse;
5use deno_core::ModuleLoader;
6use deno_core::ModuleSourceCode;
7use deno_core::ModuleSpecifier;
8use deno_error::JsErrorBox;
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, Default)]
13pub struct ImportMap {
14 pub imports: HashMap<String, String>,
15}
16
17impl ImportMap {
18 pub fn new() -> Self {
20 Self {
21 imports: HashMap::new(),
22 }
23 }
24
25 pub fn add(&mut self, specifier: String, path: String) {
27 self.imports.insert(specifier, path);
28 }
29
30 pub fn resolve(&self, specifier: &str) -> Option<&str> {
33 if let Some(mapped) = self.imports.get(specifier) {
35 return Some(mapped.as_str());
36 }
37
38 for (key, _value) in &self.imports {
40 if key.ends_with('/') && specifier.starts_with(key) {
41 let _suffix = &specifier[key.len()..];
43 continue;
47 }
48 }
49
50 None
51 }
52}
53
54#[cfg(test)]
55mod import_map_tests {
56 use super::*;
57
58 #[test]
59 fn empty_import_map_resolves_nothing() {
60 let map = ImportMap::new();
61 assert_eq!(map.resolve("foo"), None);
62 assert_eq!(map.resolve("@arcane/runtime"), None);
63 }
64
65 #[test]
66 fn exact_match_resolves() {
67 let mut map = ImportMap::new();
68 map.add("@arcane/runtime".to_string(), "file:///path/to/runtime/index.ts".to_string());
69
70 assert_eq!(map.resolve("@arcane/runtime"), Some("file:///path/to/runtime/index.ts"));
71 }
72
73 #[test]
74 fn prefix_match_is_not_implemented_yet() {
75 let mut map = ImportMap::new();
76 map.add("@arcane/runtime/".to_string(), "file:///path/to/runtime/".to_string());
77
78 assert_eq!(map.resolve("@arcane/runtime/state"), None);
81 }
82
83 #[test]
84 fn multiple_mappings_work() {
85 let mut map = ImportMap::new();
86 map.add("foo".to_string(), "file:///foo.ts".to_string());
87 map.add("bar".to_string(), "file:///bar.ts".to_string());
88 map.add("baz".to_string(), "file:///baz.ts".to_string());
89
90 assert_eq!(map.resolve("foo"), Some("file:///foo.ts"));
91 assert_eq!(map.resolve("bar"), Some("file:///bar.ts"));
92 assert_eq!(map.resolve("baz"), Some("file:///baz.ts"));
93 assert_eq!(map.resolve("qux"), None);
94 }
95
96 #[test]
97 fn last_add_wins_for_same_specifier() {
98 let mut map = ImportMap::new();
99 map.add("foo".to_string(), "file:///first.ts".to_string());
100 map.add("foo".to_string(), "file:///second.ts".to_string());
101
102 assert_eq!(map.resolve("foo"), Some("file:///second.ts"));
103 }
104
105 #[test]
106 fn clone_preserves_mappings() {
107 let mut map = ImportMap::new();
108 map.add("foo".to_string(), "file:///foo.ts".to_string());
109
110 let cloned = map.clone();
111 assert_eq!(cloned.resolve("foo"), Some("file:///foo.ts"));
112 }
113
114 #[test]
115 fn default_is_empty() {
116 let map = ImportMap::default();
117 assert_eq!(map.imports.len(), 0);
118 }
119}
120
121pub struct TsModuleLoader {
125 import_map: ImportMap,
126}
127
128impl TsModuleLoader {
129 pub fn new() -> Self {
130 Self {
131 import_map: ImportMap::new(),
132 }
133 }
134
135 pub fn with_import_map(import_map: ImportMap) -> Self {
136 Self { import_map }
137 }
138}
139
140impl Default for TsModuleLoader {
141 fn default() -> Self {
142 Self::new()
143 }
144}
145
146impl ModuleLoader for TsModuleLoader {
147 fn resolve(
148 &self,
149 specifier: &str,
150 referrer: &str,
151 _kind: deno_core::ResolutionKind,
152 ) -> Result<ModuleSpecifier, deno_core::error::ModuleLoaderError> {
153 let resolved_specifier = self.resolve_with_import_map(specifier, referrer)?;
155
156 deno_core::resolve_import(&resolved_specifier, referrer).map_err(JsErrorBox::from_err)
157 }
158
159 fn load(
160 &self,
161 module_specifier: &ModuleSpecifier,
162 _maybe_referrer: Option<&deno_core::ModuleLoadReferrer>,
163 _options: deno_core::ModuleLoadOptions,
164 ) -> ModuleLoadResponse {
165 let module_specifier = module_specifier.clone();
166
167 ModuleLoadResponse::Sync(load_module(&module_specifier))
168 }
169}
170
171impl TsModuleLoader {
172 fn resolve_with_import_map(
174 &self,
175 specifier: &str,
176 _referrer: &str,
177 ) -> Result<String, deno_core::error::ModuleLoaderError> {
178 if specifier.starts_with("./")
180 || specifier.starts_with("../")
181 || specifier.starts_with('/')
182 || specifier.starts_with("file:")
183 || specifier.starts_with("http:")
184 || specifier.starts_with("https:")
185 {
186 return Ok(specifier.to_string());
187 }
188
189 if let Some(mapped) = self.import_map.imports.get(specifier) {
191 return Ok(mapped.clone());
192 }
193
194 for (key, value) in &self.import_map.imports {
196 if key.ends_with('/') && specifier.starts_with(key) {
197 let suffix = &specifier[key.len()..];
198 let resolved = format!("{}{}", value, suffix);
199 return Ok(resolved);
200 }
201 }
202
203 Ok(specifier.to_string())
205 }
206}
207
208fn load_module(
209 specifier: &ModuleSpecifier,
210) -> Result<deno_core::ModuleSource, deno_core::error::ModuleLoaderError> {
211 let path = specifier.to_file_path().map_err(|_| {
212 JsErrorBox::generic(format!(
213 "Cannot convert module specifier to file path: {specifier}"
214 ))
215 })?;
216
217 let media_type = MediaType::from_path(&path);
218
219 let (module_type, should_transpile) = match media_type {
220 MediaType::JavaScript | MediaType::Mjs | MediaType::Cjs => {
221 (deno_core::ModuleType::JavaScript, false)
222 }
223 MediaType::Jsx => (deno_core::ModuleType::JavaScript, true),
224 MediaType::TypeScript
225 | MediaType::Mts
226 | MediaType::Cts
227 | MediaType::Dts
228 | MediaType::Dmts
229 | MediaType::Dcts
230 | MediaType::Tsx => (deno_core::ModuleType::JavaScript, true),
231 MediaType::Json => (deno_core::ModuleType::Json, false),
232 _ => {
233 return Err(JsErrorBox::generic(format!(
234 "Unsupported file type: {}",
235 path.display()
236 )));
237 }
238 };
239
240 let code = std::fs::read_to_string(&path).map_err(|e| {
241 JsErrorBox::generic(format!("Failed to read {}: {e}", path.display()))
242 })?;
243
244 let code = if should_transpile {
245 let parsed = deno_ast::parse_module(ParseParams {
246 specifier: specifier.clone(),
247 text: code.into(),
248 media_type,
249 capture_tokens: false,
250 scope_analysis: false,
251 maybe_syntax: None,
252 })
253 .map_err(|e| JsErrorBox::generic(format!("Parse error: {e}")))?;
254
255 let transpiled = parsed
256 .transpile(
257 &deno_ast::TranspileOptions::default(),
258 &TranspileModuleOptions::default(),
259 &deno_ast::EmitOptions::default(),
260 )
261 .map_err(|e| JsErrorBox::generic(format!("Transpile error: {e}")))?;
262
263 transpiled.into_source().text
264 } else {
265 code
266 };
267
268 let module = deno_core::ModuleSource::new(
269 module_type,
270 ModuleSourceCode::String(code.into()),
271 specifier,
272 None,
273 );
274
275 Ok(module)
276}