use std::cell::RefCell;
use std::collections::HashMap;
use indexmap::IndexMap;
#[cfg(feature = "swc")]
use swc_core::ecma::ast::{
ImportDecl, ImportSpecifier, Module, ModuleDecl, ModuleExportName, ModuleItem,
};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SourceImport {
pub source_module: String,
pub original_name: Option<String>,
pub is_type_only: bool,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SourceImportEntry {
pub local_name: String,
pub source_module: String,
pub original_name: Option<String>,
pub is_type_only: bool,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct GeneratedImport {
pub local_name: String,
pub source_module: String,
pub original_name: Option<String>,
pub is_type_only: bool,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ImportRegistry {
source_imports: HashMap<String, SourceImport>,
pub config_imports: HashMap<String, String>,
generated: IndexMap<String, GeneratedImport>,
}
impl Default for ImportRegistry {
fn default() -> Self {
Self::new()
}
}
impl ImportRegistry {
pub fn new() -> Self {
Self {
source_imports: HashMap::new(),
config_imports: HashMap::new(),
generated: IndexMap::new(),
}
}
#[cfg(feature = "swc")]
pub fn from_module(module: &Module, source: &str) -> Self {
let mut source_imports = HashMap::new();
source_imports.extend(collect_macro_import_comments(source).into_iter().map(
|(name, module_src)| {
(
name,
SourceImport {
source_module: module_src,
original_name: None,
is_type_only: false,
},
)
},
));
for item in &module.body {
if let ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl {
specifiers,
src,
type_only: import_type_only,
..
})) = item
{
let module_source = src.value.to_string_lossy().to_string();
for specifier in specifiers {
match specifier {
ImportSpecifier::Named(named) => {
let local_name = named.local.sym.to_string();
let is_type_only = *import_type_only || named.is_type_only;
let original_name = named.imported.as_ref().and_then(|imported| {
let orig = match imported {
ModuleExportName::Ident(ident) => ident.sym.to_string(),
ModuleExportName::Str(s) => {
String::from_utf8_lossy(s.value.as_bytes()).to_string()
}
};
if orig != local_name { Some(orig) } else { None }
});
source_imports.insert(
local_name,
SourceImport {
source_module: module_source.clone(),
original_name,
is_type_only,
},
);
}
ImportSpecifier::Default(default) => {
let local_name = default.local.sym.to_string();
source_imports.insert(
local_name,
SourceImport {
source_module: module_source.clone(),
original_name: None,
is_type_only: *import_type_only,
},
);
}
ImportSpecifier::Namespace(ns) => {
let local_name = ns.local.sym.to_string();
source_imports.insert(
local_name,
SourceImport {
source_module: module_source.clone(),
original_name: None,
is_type_only: *import_type_only,
},
);
}
}
}
}
}
Self {
source_imports,
config_imports: HashMap::new(),
generated: IndexMap::new(),
}
}
pub fn is_available(&self, name: &str) -> bool {
self.source_imports.contains_key(name) || self.generated.contains_key(name)
}
pub fn is_type_only(&self, name: &str) -> bool {
self.source_imports
.get(name)
.map(|si| si.is_type_only)
.unwrap_or(false)
}
pub fn get_source(&self, name: &str) -> Option<&str> {
self.source_imports
.get(name)
.map(|si| si.source_module.as_str())
}
pub fn resolve_alias(&self, name: &str) -> Option<&str> {
self.source_imports
.get(name)
.and_then(|si| si.original_name.as_deref())
}
pub fn source_map(&self) -> &HashMap<String, SourceImport> {
&self.source_imports
}
pub fn source_modules(&self) -> HashMap<String, String> {
self.source_imports
.iter()
.map(|(name, si)| (name.clone(), si.source_module.clone()))
.collect()
}
pub fn aliases(&self) -> HashMap<String, String> {
self.source_imports
.iter()
.filter_map(|(name, si)| {
si.original_name
.as_ref()
.map(|orig| (name.clone(), orig.clone()))
})
.collect()
}
pub fn source_import_entries(&self) -> Vec<SourceImportEntry> {
self.source_imports
.iter()
.map(|(name, si)| SourceImportEntry {
local_name: name.clone(),
source_module: si.source_module.clone(),
original_name: si.original_name.clone(),
is_type_only: si.is_type_only,
})
.collect()
}
pub fn install_source_imports(&mut self, entries: Vec<SourceImportEntry>) {
for entry in entries {
self.source_imports.insert(
entry.local_name,
SourceImport {
source_module: entry.source_module,
original_name: entry.original_name,
is_type_only: entry.is_type_only,
},
);
}
}
pub fn type_only_map(&self) -> HashMap<String, bool> {
self.source_imports
.iter()
.map(|(name, si)| (name.clone(), si.is_type_only))
.collect()
}
pub fn request_import(
&mut self,
local_name: &str,
original_name: Option<&str>,
module: &str,
is_type_only: bool,
) {
if local_name.contains('.') {
return;
}
if self.source_imports.contains_key(local_name) {
return;
}
if self.generated.contains_key(local_name) {
return;
}
self.generated.insert(
local_name.to_string(),
GeneratedImport {
local_name: local_name.to_string(),
source_module: module.to_string(),
original_name: original_name.map(|s| s.to_string()),
is_type_only,
},
);
}
pub fn request_namespace_import(&mut self, namespace: &str, module: &str, alias: &str) {
if let Some(si) = self.source_imports.get(namespace)
&& !si.is_type_only
{
return;
}
if self.generated.contains_key(alias) {
return;
}
self.generated.insert(
alias.to_string(),
GeneratedImport {
local_name: alias.to_string(),
source_module: module.to_string(),
original_name: Some(namespace.to_string()),
is_type_only: false,
},
);
}
pub fn request_type_import(&mut self, name: &str, module: &str) {
self.request_import(name, None, module, true);
}
pub fn generated_imports(&self) -> impl Iterator<Item = &GeneratedImport> {
self.generated.values()
}
pub fn take_generated_imports(&mut self) -> Vec<GeneratedImport> {
std::mem::take(&mut self.generated).into_values().collect()
}
pub fn merge_imports(&mut self, imports: Vec<GeneratedImport>) {
for import in imports {
if !self.source_imports.contains_key(&import.local_name)
&& !self.generated.contains_key(&import.local_name)
{
self.generated.insert(import.local_name.clone(), import);
}
}
}
pub fn emit_generated_imports(&self) -> String {
if self.generated.is_empty() {
return String::new();
}
let mut lines = String::new();
for (_alias, import) in &self.generated {
let keyword = if import.is_type_only {
"import type"
} else {
"import"
};
let specifier = if let Some(ref original) = import.original_name {
format!("{} as {}", original, import.local_name)
} else {
import.local_name.clone()
};
lines.push_str(&format!(
"{} {{ {} }} from \"{}\";\n",
keyword, specifier, import.source_module
));
}
lines
}
}
thread_local! {
static IMPORT_REGISTRY: RefCell<ImportRegistry> = RefCell::new(ImportRegistry::new());
}
pub fn with_registry<R>(f: impl FnOnce(&ImportRegistry) -> R) -> R {
IMPORT_REGISTRY.with(|r| f(&r.borrow()))
}
pub fn with_registry_mut<R>(f: impl FnOnce(&mut ImportRegistry) -> R) -> R {
IMPORT_REGISTRY.with(|r| f(&mut r.borrow_mut()))
}
pub fn install_registry(registry: ImportRegistry) {
IMPORT_REGISTRY.with(|r| {
*r.borrow_mut() = registry;
});
}
pub fn take_registry() -> ImportRegistry {
IMPORT_REGISTRY.with(|r| std::mem::take(&mut *r.borrow_mut()))
}
pub fn clear_registry() {
IMPORT_REGISTRY.with(|r| {
*r.borrow_mut() = ImportRegistry::new();
});
}
fn collect_macro_import_comments(source: &str) -> HashMap<String, String> {
let mut out = HashMap::new();
let mut search_start = 0usize;
while let Some(idx) = source[search_start..].find("/**") {
let abs_idx = search_start + idx;
let remaining = &source[abs_idx + 3..];
let Some(end_rel) = remaining.find("*/") else {
break;
};
let body = &remaining[..end_rel];
let normalized = normalize_macro_import_body(body);
let normalized_lower = normalized.to_ascii_lowercase();
if normalized_lower.contains("import macro")
&& let (Some(open_brace), Some(close_brace)) =
(normalized.find('{'), normalized.find('}'))
&& close_brace > open_brace
&& let Some(from_idx) = normalized_lower[close_brace..].find("from")
{
let names_src = normalized[open_brace + 1..close_brace].trim();
let from_section = &normalized[close_brace + from_idx + "from".len()..];
if let Some(module_src) = extract_quoted_string(from_section) {
for name in names_src.split(',') {
let trimmed = name.trim();
if !trimmed.is_empty() {
out.insert(trimmed.to_string(), module_src.clone());
}
}
}
}
search_start = abs_idx + 3 + end_rel + 2;
}
out
}
fn normalize_macro_import_body(body: &str) -> String {
let mut normalized = String::new();
for line in body.lines() {
let mut trimmed = line.trim();
if let Some(stripped) = trimmed.strip_prefix('*') {
trimmed = stripped.trim();
}
if trimmed.is_empty() {
continue;
}
if !normalized.is_empty() {
normalized.push(' ');
}
normalized.push_str(trimmed);
}
normalized
}
fn extract_quoted_string(input: &str) -> Option<String> {
for (idx, ch) in input.char_indices() {
if ch == '"' || ch == '\'' {
let start = idx + 1;
let rest = &input[start..];
if let Some(end) = rest.find(ch) {
return Some(rest[..end].trim().to_string());
}
break;
}
}
None
}