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
54pub struct TsModuleLoader {
58 import_map: ImportMap,
59}
60
61impl TsModuleLoader {
62 pub fn new() -> Self {
63 Self {
64 import_map: ImportMap::new(),
65 }
66 }
67
68 pub fn with_import_map(import_map: ImportMap) -> Self {
69 Self { import_map }
70 }
71}
72
73impl Default for TsModuleLoader {
74 fn default() -> Self {
75 Self::new()
76 }
77}
78
79impl ModuleLoader for TsModuleLoader {
80 fn resolve(
81 &self,
82 specifier: &str,
83 referrer: &str,
84 _kind: deno_core::ResolutionKind,
85 ) -> Result<ModuleSpecifier, deno_core::error::ModuleLoaderError> {
86 let resolved_specifier = self.resolve_with_import_map(specifier, referrer)?;
88
89 deno_core::resolve_import(&resolved_specifier, referrer).map_err(JsErrorBox::from_err)
90 }
91
92 fn load(
93 &self,
94 module_specifier: &ModuleSpecifier,
95 _maybe_referrer: Option<&deno_core::ModuleLoadReferrer>,
96 _options: deno_core::ModuleLoadOptions,
97 ) -> ModuleLoadResponse {
98 let module_specifier = module_specifier.clone();
99
100 ModuleLoadResponse::Sync(load_module(&module_specifier))
101 }
102}
103
104impl TsModuleLoader {
105 fn resolve_with_import_map(
107 &self,
108 specifier: &str,
109 referrer: &str,
110 ) -> Result<String, deno_core::error::ModuleLoaderError> {
111 if specifier.starts_with("./")
113 || specifier.starts_with("../")
114 || specifier.starts_with('/')
115 || specifier.starts_with("file:")
116 || specifier.starts_with("http:")
117 || specifier.starts_with("https:")
118 {
119 return Ok(specifier.to_string());
120 }
121
122 if let Some(mapped) = self.import_map.imports.get(specifier) {
124 return Ok(mapped.clone());
125 }
126
127 for (key, value) in &self.import_map.imports {
129 if key.ends_with('/') && specifier.starts_with(key) {
130 let suffix = &specifier[key.len()..];
131 let resolved = format!("{}{}", value, suffix);
132 return Ok(resolved);
133 }
134 }
135
136 Ok(specifier.to_string())
138 }
139}
140
141fn load_module(
142 specifier: &ModuleSpecifier,
143) -> Result<deno_core::ModuleSource, deno_core::error::ModuleLoaderError> {
144 let path = specifier.to_file_path().map_err(|_| {
145 JsErrorBox::generic(format!(
146 "Cannot convert module specifier to file path: {specifier}"
147 ))
148 })?;
149
150 let media_type = MediaType::from_path(&path);
151
152 let (module_type, should_transpile) = match media_type {
153 MediaType::JavaScript | MediaType::Mjs | MediaType::Cjs => {
154 (deno_core::ModuleType::JavaScript, false)
155 }
156 MediaType::Jsx => (deno_core::ModuleType::JavaScript, true),
157 MediaType::TypeScript
158 | MediaType::Mts
159 | MediaType::Cts
160 | MediaType::Dts
161 | MediaType::Dmts
162 | MediaType::Dcts
163 | MediaType::Tsx => (deno_core::ModuleType::JavaScript, true),
164 MediaType::Json => (deno_core::ModuleType::Json, false),
165 _ => {
166 return Err(JsErrorBox::generic(format!(
167 "Unsupported file type: {}",
168 path.display()
169 )));
170 }
171 };
172
173 let code = std::fs::read_to_string(&path).map_err(|e| {
174 JsErrorBox::generic(format!("Failed to read {}: {e}", path.display()))
175 })?;
176
177 let code = if should_transpile {
178 let parsed = deno_ast::parse_module(ParseParams {
179 specifier: specifier.clone(),
180 text: code.into(),
181 media_type,
182 capture_tokens: false,
183 scope_analysis: false,
184 maybe_syntax: None,
185 })
186 .map_err(|e| JsErrorBox::generic(format!("Parse error: {e}")))?;
187
188 let transpiled = parsed
189 .transpile(
190 &deno_ast::TranspileOptions::default(),
191 &TranspileModuleOptions::default(),
192 &deno_ast::EmitOptions::default(),
193 )
194 .map_err(|e| JsErrorBox::generic(format!("Transpile error: {e}")))?;
195
196 transpiled.into_source().text
197 } else {
198 code
199 };
200
201 let module = deno_core::ModuleSource::new(
202 module_type,
203 ModuleSourceCode::String(code.into()),
204 specifier,
205 None,
206 );
207
208 Ok(module)
209}