use std::path::{Path, PathBuf};
use std::collections::HashMap;
use std::fs;
use crate::builtins;
use crate::error::{Result, DumplingError};
use crate::typescript::{TypeScriptTranspiler, TypeScriptConfig, is_typescript_file};
use crate::import_map::ImportMapResolver;
pub const BUILTIN_PREFIX: &str = "node:";
#[derive(Debug, Clone)]
pub struct Module {
pub id: String,
pub path: PathBuf,
pub source: String,
pub dependencies: Vec<String>,
pub is_entry: bool,
pub module_type: String,
}
impl Module {
pub fn is_css(&self) -> bool {
self.module_type == "css"
}
pub fn new(id: String, path: PathBuf, source: String, module_type: &str) -> Self {
Self {
id,
path,
source,
dependencies: Vec::new(),
is_entry: false,
module_type: module_type.to_string(),
}
}
}
pub struct ModuleResolver {
root: PathBuf,
cache: HashMap<String, Module>,
node_modules: PathBuf,
ts_transpiler: TypeScriptTranspiler,
import_map_resolver: ImportMapResolver,
}
impl ModuleResolver {
pub fn new(root: PathBuf) -> Self {
let node_modules = root.join("node_modules");
let ts_config = TypeScriptConfig::default();
let ts_transpiler = TypeScriptTranspiler::new(ts_config);
let import_map_resolver = ImportMapResolver::new(&root);
Self {
root,
cache: HashMap::new(),
node_modules,
ts_transpiler,
import_map_resolver,
}
}
pub async fn resolve(&mut self, specifier: &str, parent: &Path) -> Result<PathBuf> {
if let Some(resolved_specifier) = self.import_map_resolver.resolve(specifier) {
if resolved_specifier.starts_with("http://") || resolved_specifier.starts_with("https://") {
return Err(DumplingError::ModuleResolution(
format!("URL imports not yet supported: {}", resolved_specifier)
));
}
return self.resolve_file_path(Path::new(&resolved_specifier)).await;
}
if builtins::is_builtin(specifier) {
let name = specifier.split('/').next().unwrap_or(specifier);
return Ok(PathBuf::from(format!("{}{}", BUILTIN_PREFIX, name)));
}
if specifier.starts_with("./") || specifier.starts_with("../") {
let path = parent.join(specifier);
return self.resolve_file_path(&path).await;
}
if specifier.starts_with("/") {
return self.resolve_file_path(Path::new(specifier)).await;
}
self.resolve_node_modules(specifier).await
}
async fn resolve_file_path(&self, path: &Path) -> Result<PathBuf> {
if path.is_file() {
return Ok(path.to_path_buf());
}
let js_path = path.with_extension("js");
if js_path.is_file() {
return Ok(js_path);
}
let mjs_path = path.with_extension("mjs");
if mjs_path.is_file() {
return Ok(mjs_path);
}
let json_path = path.with_extension("json");
if json_path.is_file() {
return Ok(json_path);
}
let ts_path = path.with_extension("ts");
if ts_path.is_file() {
return Ok(ts_path);
}
let tsx_path = path.with_extension("tsx");
if tsx_path.is_file() {
return Ok(tsx_path);
}
let css_path = path.with_extension("css");
if css_path.is_file() {
return Ok(css_path);
}
if path.is_dir() {
let package_json = path.join("package.json");
if package_json.is_file() {
let content = fs::read_to_string(&package_json)?;
if let Ok(package) = serde_json::from_str::<serde_json::Value>(&content) {
if let Some(main) = package.get("main").and_then(|v| v.as_str()) {
let main_path = path.join(main);
if main_path.is_file() {
return Ok(main_path);
}
}
}
}
let index_js = path.join("index.js");
if index_js.is_file() {
return Ok(index_js);
}
let index_mjs = path.join("index.mjs");
if index_mjs.is_file() {
return Ok(index_mjs);
}
let index_ts = path.join("index.ts");
if index_ts.is_file() {
return Ok(index_ts);
}
let index_tsx = path.join("index.tsx");
if index_tsx.is_file() {
return Ok(index_tsx);
}
}
Err(DumplingError::ModuleResolution(format!("Cannot resolve module: {}", path.display())))
}
async fn resolve_package_entry(
&self,
package_path: &Path,
package: &str,
subpath: Option<&str>,
) -> Result<PathBuf> {
let package_json = package_path.join("package.json");
if !package_json.is_file() {
return self.fallback_package_resolution(package_path, package).await;
}
let content = fs::read_to_string(&package_json)?;
let pkg_json: serde_json::Value = serde_json::from_str(&content)
.map_err(|_| DumplingError::ModuleResolution("Invalid package.json".to_string()))?;
let export_key = match subpath {
Some(sp) => format!("./{}", sp),
None => ".".to_string(),
};
if let Some(exports) = pkg_json.get("exports") {
if let Some(resolved) = self.resolve_exports_field(exports, &export_key, package_path)? {
return Ok(resolved);
}
}
if subpath.is_none() {
if let Some(main) = pkg_json.get("main").and_then(|v| v.as_str()) {
let main_path = package_path.join(main);
if main_path.is_file() {
return Ok(main_path);
}
}
if let Some(module) = pkg_json.get("module").and_then(|v| v.as_str()) {
let module_path = package_path.join(module);
if module_path.is_file() {
return Ok(module_path);
}
}
}
if let Some(sp) = subpath {
let subpath_path = package_path.join(sp);
if let Ok(resolved) = self.resolve_file_path(&subpath_path).await {
return Ok(resolved);
}
}
self.fallback_package_resolution(package_path, package).await
}
fn resolve_exports_field(
&self,
exports: &serde_json::Value,
key: &str,
package_path: &Path,
) -> Result<Option<PathBuf>> {
if let Some(entry) = exports.get(key) {
if let Some(path) = self.resolve_export_entry(entry, package_path)? {
return Ok(Some(path));
}
}
if key.contains('/') {
let parts: Vec<&str> = key.splitn(2, '/').collect();
if parts.len() == 2 {
let prefix = parts[0];
let suffix = parts[1];
let wildcard_key = format!("{}/*", prefix);
if let Some(entry) = exports.get(&wildcard_key) {
if let Some(template) = entry.as_str() {
let resolved = template.replace("*", suffix);
let path = package_path.join(resolved.trim_start_matches("./"));
if path.is_file() {
return Ok(Some(path));
}
}
}
}
}
Ok(None)
}
fn resolve_export_entry(
&self,
entry: &serde_json::Value,
package_path: &Path,
) -> Result<Option<PathBuf>> {
if let Some(s) = entry.as_str() {
let path = package_path.join(s.trim_start_matches("./"));
return Ok(if path.is_file() { Some(path) } else { None });
}
if let Some(obj) = entry.as_object() {
let conditions = if self.is_browser_environment() {
vec!["browser", "import", "require", "node", "default"]
} else {
vec!["require", "node", "import", "default"]
};
for condition in conditions {
if let Some(val) = obj.get(condition) {
if let Some(path) = self.resolve_export_entry(val, package_path)? {
return Ok(Some(path));
}
}
}
}
Ok(None)
}
fn is_browser_environment(&self) -> bool {
true
}
async fn fallback_package_resolution(
&self,
package_path: &Path,
package: &str,
) -> Result<PathBuf> {
let index_js = package_path.join("index.js");
if index_js.is_file() {
return Ok(index_js);
}
let index_mjs = package_path.join("index.mjs");
if index_mjs.is_file() {
return Ok(index_mjs);
}
Err(DumplingError::ModuleResolution(format!(
"Cannot resolve entry point for package '{}'",
package
)))
}
async fn resolve_node_modules(&self, specifier: &str) -> Result<PathBuf> {
let (package, subpath) = if let Some((pkg, sub)) = specifier.split_once('/') {
(pkg, Some(sub))
} else {
(specifier, None)
};
let package_path = self.node_modules.join(package);
if !package_path.exists() {
return Err(DumplingError::ModuleResolution(format!("Package '{}' not found", package)));
}
self.resolve_package_entry(&package_path, package, subpath).await
}
pub async fn load_module(&mut self, path: PathBuf) -> Result<Module> {
let path_str = path.display().to_string();
if let Some(module) = self.cache.get(&path_str) {
return Ok(module.clone());
}
let (source, module_type) = if path_str.starts_with(BUILTIN_PREFIX) {
let name = path_str.trim_start_matches(BUILTIN_PREFIX);
(builtins::get_builtin_stub(name).to_string(), "js")
} else if path_str.ends_with(".json") {
let content = fs::read_to_string(&path)?;
let parsed: serde_json::Value = serde_json::from_str(&content)?;
(
format!("module.exports = {};", serde_json::to_string(&parsed).unwrap_or_else(|_| "{}".to_string())),
"json",
)
} else if path_str.ends_with(".css") {
let content = fs::read_to_string(&path)?;
(content, "css")
} else if is_typescript_file(&path) {
let ts_content = fs::read_to_string(&path)?;
let js_content = self.ts_transpiler.transpile(&path, &ts_content)?;
(js_content, "js")
} else {
(fs::read_to_string(&path)?, "js")
};
let mut module = Module::new(path_str.clone(), path.clone(), source, module_type);
if !path_str.starts_with(BUILTIN_PREFIX) && module_type != "css" {
module.dependencies = self.extract_dependencies(&module.source);
}
self.cache.insert(path_str, module.clone());
Ok(module)
}
fn extract_dependencies(&self, source: &str) -> Vec<String> {
let mut dependencies = Vec::new();
let source_no_comments: String = source
.lines()
.filter(|line| !line.trim_start().starts_with("//"))
.collect::<Vec<_>>()
.join("\n");
let import_regex = regex::Regex::new(r#"import.*from\s+['"]([^'"]+)['"]"#).unwrap();
let require_regex = regex::Regex::new(r#"require\s*\(\s*['"]([^'"]+)['"]\s*\)"#).unwrap();
let dynamic_import_regex = regex::Regex::new(r#"import\s*\(\s*['"]([^'"]+)['"]\s*(?:,\s*\{\s*[^}]*\}\s*\)?"#).unwrap();
let import_assertion_regex = regex::Regex::new(r#"import\s+(['"])([^'"]+)\1\s+(?:assert\s+)?\{\s*type:\s*['"]([^'"]+)['"][^}]*\}"#).unwrap();
for cap in import_regex.captures_iter(&source_no_comments) {
dependencies.push(cap[1].to_string());
}
for cap in require_regex.captures_iter(&source_no_comments) {
dependencies.push(cap[1].to_string());
}
for cap in dynamic_import_regex.captures_iter(&source_no_comments) {
dependencies.push(cap[2].to_string());
}
for cap in import_assertion_regex.captures_iter(&source_no_comments) {
dependencies.push(cap[2].to_string());
}
dependencies
}
}