use deno_ast::MediaType;
use deno_ast::ParseParams;
use deno_ast::TranspileModuleOptions;
use deno_core::ModuleLoadResponse;
use deno_core::ModuleLoader;
use deno_core::ModuleSourceCode;
use deno_core::ModuleSpecifier;
use deno_error::JsErrorBox;
use std::collections::HashMap;
#[derive(Debug, Clone, Default)]
pub struct ImportMap {
pub imports: HashMap<String, String>,
}
impl ImportMap {
pub fn new() -> Self {
Self {
imports: HashMap::new(),
}
}
pub fn add(&mut self, specifier: String, path: String) {
self.imports.insert(specifier, path);
}
pub fn resolve(&self, specifier: &str) -> Option<&str> {
if let Some(mapped) = self.imports.get(specifier) {
return Some(mapped.as_str());
}
for (key, _value) in &self.imports {
if key.ends_with('/') && specifier.starts_with(key) {
let _suffix = &specifier[key.len()..];
continue;
}
}
None
}
}
#[cfg(test)]
mod import_map_tests {
use super::*;
#[test]
fn empty_import_map_resolves_nothing() {
let map = ImportMap::new();
assert_eq!(map.resolve("foo"), None);
assert_eq!(map.resolve("@arcane/runtime"), None);
}
#[test]
fn exact_match_resolves() {
let mut map = ImportMap::new();
map.add("@arcane/runtime".to_string(), "file:///path/to/runtime/index.ts".to_string());
assert_eq!(map.resolve("@arcane/runtime"), Some("file:///path/to/runtime/index.ts"));
}
#[test]
fn prefix_match_is_not_implemented_yet() {
let mut map = ImportMap::new();
map.add("@arcane/runtime/".to_string(), "file:///path/to/runtime/".to_string());
assert_eq!(map.resolve("@arcane/runtime/state"), None);
}
#[test]
fn multiple_mappings_work() {
let mut map = ImportMap::new();
map.add("foo".to_string(), "file:///foo.ts".to_string());
map.add("bar".to_string(), "file:///bar.ts".to_string());
map.add("baz".to_string(), "file:///baz.ts".to_string());
assert_eq!(map.resolve("foo"), Some("file:///foo.ts"));
assert_eq!(map.resolve("bar"), Some("file:///bar.ts"));
assert_eq!(map.resolve("baz"), Some("file:///baz.ts"));
assert_eq!(map.resolve("qux"), None);
}
#[test]
fn last_add_wins_for_same_specifier() {
let mut map = ImportMap::new();
map.add("foo".to_string(), "file:///first.ts".to_string());
map.add("foo".to_string(), "file:///second.ts".to_string());
assert_eq!(map.resolve("foo"), Some("file:///second.ts"));
}
#[test]
fn clone_preserves_mappings() {
let mut map = ImportMap::new();
map.add("foo".to_string(), "file:///foo.ts".to_string());
let cloned = map.clone();
assert_eq!(cloned.resolve("foo"), Some("file:///foo.ts"));
}
#[test]
fn default_is_empty() {
let map = ImportMap::default();
assert_eq!(map.imports.len(), 0);
}
}
pub struct TsModuleLoader {
import_map: ImportMap,
}
impl TsModuleLoader {
pub fn new() -> Self {
Self {
import_map: ImportMap::new(),
}
}
pub fn with_import_map(import_map: ImportMap) -> Self {
Self { import_map }
}
}
impl Default for TsModuleLoader {
fn default() -> Self {
Self::new()
}
}
impl ModuleLoader for TsModuleLoader {
fn resolve(
&self,
specifier: &str,
referrer: &str,
_kind: deno_core::ResolutionKind,
) -> Result<ModuleSpecifier, deno_core::error::ModuleLoaderError> {
let resolved_specifier = self.resolve_with_import_map(specifier, referrer)?;
deno_core::resolve_import(&resolved_specifier, referrer).map_err(JsErrorBox::from_err)
}
fn load(
&self,
module_specifier: &ModuleSpecifier,
_maybe_referrer: Option<&deno_core::ModuleLoadReferrer>,
_options: deno_core::ModuleLoadOptions,
) -> ModuleLoadResponse {
let module_specifier = module_specifier.clone();
ModuleLoadResponse::Sync(load_module(&module_specifier))
}
}
impl TsModuleLoader {
fn resolve_with_import_map(
&self,
specifier: &str,
_referrer: &str,
) -> Result<String, deno_core::error::ModuleLoaderError> {
if specifier.starts_with("./")
|| specifier.starts_with("../")
|| specifier.starts_with('/')
|| specifier.starts_with("file:")
|| specifier.starts_with("http:")
|| specifier.starts_with("https:")
{
return Ok(specifier.to_string());
}
if let Some(mapped) = self.import_map.imports.get(specifier) {
return Ok(mapped.clone());
}
for (key, value) in &self.import_map.imports {
if key.ends_with('/') && specifier.starts_with(key) {
let suffix = &specifier[key.len()..];
let resolved = format!("{}{}", value, suffix);
return Ok(resolved);
}
}
Ok(specifier.to_string())
}
}
fn load_module(
specifier: &ModuleSpecifier,
) -> Result<deno_core::ModuleSource, deno_core::error::ModuleLoaderError> {
let path = specifier.to_file_path().map_err(|_| {
JsErrorBox::generic(format!(
"Cannot convert module specifier to file path: {specifier}"
))
})?;
let media_type = MediaType::from_path(&path);
let (module_type, should_transpile) = match media_type {
MediaType::JavaScript | MediaType::Mjs | MediaType::Cjs => {
(deno_core::ModuleType::JavaScript, false)
}
MediaType::Jsx => (deno_core::ModuleType::JavaScript, true),
MediaType::TypeScript
| MediaType::Mts
| MediaType::Cts
| MediaType::Dts
| MediaType::Dmts
| MediaType::Dcts
| MediaType::Tsx => (deno_core::ModuleType::JavaScript, true),
MediaType::Json => (deno_core::ModuleType::Json, false),
_ => {
return Err(JsErrorBox::generic(format!(
"Unsupported file type: {}",
path.display()
)));
}
};
let code = std::fs::read_to_string(&path).map_err(|e| {
JsErrorBox::generic(format!("Failed to read {}: {e}", path.display()))
})?;
let code = if should_transpile {
let parsed = deno_ast::parse_module(ParseParams {
specifier: specifier.clone(),
text: code.into(),
media_type,
capture_tokens: false,
scope_analysis: false,
maybe_syntax: None,
})
.map_err(|e| JsErrorBox::generic(format!("Parse error: {e}")))?;
let transpiled = parsed
.transpile(
&deno_ast::TranspileOptions::default(),
&TranspileModuleOptions::default(),
&deno_ast::EmitOptions::default(),
)
.map_err(|e| JsErrorBox::generic(format!("Transpile error: {e}")))?;
transpiled.into_source().text
} else {
code
};
let module = deno_core::ModuleSource::new(
module_type,
ModuleSourceCode::String(code.into()),
specifier,
None,
);
Ok(module)
}