use std::path::{Path, PathBuf};
use serde_json::Value;
use skif_core::FileSystem;
use skif_js_syntax::ModuleLoader;
use skif_workspace::Workspace;
use crate::manifest_paths::{ENTRY_FIELDS, is_out_dir, map_to_source, resolve_exports};
use crate::resolve::{is_code_file, join_normalized};
use crate::specifier::{package_name, subpath};
const SCRIPT_EXTENSIONS: &[&str] = &["ts", "mts", "cts", "tsx", "js", "mjs", "cjs", "jsx"];
const EXTENSION_ALIASES: &[(&str, &[&str])] = &[
("js", &["ts", "tsx"]),
("mjs", &["mts"]),
("cjs", &["cts"]),
("jsx", &["tsx"]),
];
pub(crate) struct ConfigModules<'a> {
fs: &'a dyn FileSystem,
workspace: &'a Workspace,
}
impl<'a> ConfigModules<'a> {
pub(crate) fn new(fs: &'a dyn FileSystem, workspace: &'a Workspace) -> Self {
Self { fs, workspace }
}
fn package_module(&self, specifier: &str) -> Option<PathBuf> {
let name = package_name(specifier)?;
let package = self
.workspace
.all_packages()
.find(|package| package.name.as_deref() == Some(name.as_str()))?;
let dir = self.workspace.root.join(&package.dir);
let manifest = package.manifest.shared_value();
let rest = subpath(specifier, &name);
let target = match (manifest.get("exports"), rest) {
(Some(exports), rest) => {
let subpath = rest.map_or_else(|| ".".to_owned(), |rest| format!("./{rest}"));
resolve_exports(&without_types(exports), &subpath, &[])?
}
(None, Some(rest)) => rest.to_owned(),
(None, None) => ENTRY_FIELDS
.iter()
.filter(|field| !matches!(**field, "types" | "typings"))
.find_map(|field| manifest.get(*field).and_then(Value::as_str))
.unwrap_or("index.js")
.to_owned(),
};
map_to_source(self.fs, &dir, &target).or_else(|| {
let (first, rest) = target.trim_start_matches("./").split_once('/')?;
is_out_dir(first).then(|| map_to_source(self.fs, &dir, rest)).flatten()
})
}
}
impl ModuleLoader for ConfigModules<'_> {
fn load(&self, from: &Path, specifier: &str) -> Option<(PathBuf, String)> {
let path = if specifier.starts_with("./") || specifier.starts_with("../") {
relative_module(self.fs, &join_normalized(from.parent()?, specifier))?
} else {
self.package_module(specifier)?
};
let inside =
path.starts_with(&self.workspace.root) && !path.components().any(|part| part.as_os_str() == "node_modules");
if !inside || !is_code_file(&path) {
return None;
}
let source = self.fs.read_to_string(&path).ok()?;
Some((path, source.to_string()))
}
}
fn relative_module(fs: &dyn FileSystem, base: &Path) -> Option<PathBuf> {
let extension = base.extension().and_then(|e| e.to_str()).unwrap_or_default();
let aliases = EXTENSION_ALIASES
.iter()
.find(|(written, _)| *written == extension)
.map(|(_, sources)| *sources)
.unwrap_or_default();
let mut candidates = vec![base.to_path_buf()];
candidates.extend(aliases.iter().map(|alias| base.with_extension(alias)));
let name = base.file_name()?.to_string_lossy().into_owned();
candidates.extend(
SCRIPT_EXTENSIONS
.iter()
.map(|ext| base.with_file_name(format!("{name}.{ext}"))),
);
candidates.extend(SCRIPT_EXTENSIONS.iter().map(|ext| base.join(format!("index.{ext}"))));
candidates.into_iter().find(|candidate| fs.is_file(candidate))
}
fn without_types(exports: &Value) -> Value {
match exports {
Value::Object(map) => Value::Object(
map.iter()
.filter(|(key, _)| !matches!(key.as_str(), "types" | "typings"))
.map(|(key, value)| (key.clone(), without_types(value)))
.collect(),
),
Value::Array(items) => Value::Array(items.iter().map(without_types).collect()),
other => other.clone(),
}
}