use std::path::{Component, Path, PathBuf};
use fallow_config::JsxImportSourceRule;
use fallow_types::extract::{ImportInfo, ImportedName, ModuleInfo};
use globset::{GlobBuilder, GlobSet, GlobSetBuilder};
use oxc_span::Span;
use rustc_hash::FxHashSet;
use super::specifier::resolve_import_specifier;
use super::types::{ResolveContext, ResolveResult, ResolvedImport};
const JSX_DEV_RUNTIME_SUBPATH: &str = "jsx-dev-runtime";
const JSX_DEV_RUNTIME_BINDINGS: [&str; 2] = ["jsxDEV", "Fragment"];
pub(super) struct CompiledJsxRule<'a> {
rule: &'a JsxImportSourceRule,
include: GlobSet,
}
pub(super) fn compile_jsx_rules(rules: &[JsxImportSourceRule]) -> Vec<CompiledJsxRule<'_>> {
rules
.iter()
.filter_map(|rule| {
let mut builder = GlobSetBuilder::new();
let mut added = false;
for pattern in &rule.include {
if let Ok(glob) = GlobBuilder::new(pattern).literal_separator(true).build() {
builder.add(glob);
added = true;
}
}
let include = builder.build().ok().filter(|_| added)?;
Some(CompiledJsxRule { rule, include })
})
.collect()
}
pub(super) fn resolve_config_jsx_runtime_imports(
ctx: &ResolveContext<'_>,
file_path: &Path,
module: &ModuleInfo,
rules: &[CompiledJsxRule<'_>],
) -> Vec<ResolvedImport> {
if !module.jsx_runtime_from_config || rules.is_empty() {
return Vec::new();
}
let mut seen: FxHashSet<&str> = FxHashSet::default();
let mut imports = Vec::new();
for compiled in rules {
let rule = compiled.rule;
let Ok(relative) = file_path.strip_prefix(&rule.config_dir) else {
continue;
};
if !compiled.include.is_match(relative) || !seen.insert(rule.source.as_str()) {
continue;
}
let specifier = jsx_dev_runtime_specifier(&rule.source);
let Some(target) = resolve_runtime(ctx, file_path, rule, &specifier) else {
continue;
};
imports.extend(
JSX_DEV_RUNTIME_BINDINGS
.iter()
.map(|binding| ResolvedImport {
info: ImportInfo {
source: specifier.clone(),
imported_name: ImportedName::Named((*binding).to_string()),
local_name: String::new(),
is_type_only: false,
is_type_only_star: false,
from_style: false,
span: Span::default(),
source_span: Span::default(),
},
target: target.clone(),
}),
);
}
imports
}
fn resolve_runtime(
ctx: &ResolveContext<'_>,
file_path: &Path,
rule: &JsxImportSourceRule,
specifier: &str,
) -> Option<ResolveResult> {
let from_file = resolve_import_specifier(ctx, file_path, specifier, false);
if !matches!(from_file, ResolveResult::Unresolvable(_)) {
return Some(from_file);
}
if !is_relative(&rule.source) {
return None;
}
let from_config = config_relative_specifier(file_path.parent()?, &rule.config_dir, specifier)?;
match resolve_import_specifier(ctx, file_path, &from_config, false) {
ResolveResult::Unresolvable(_) => None,
target => Some(target),
}
}
fn is_relative(source: &str) -> bool {
matches!(source, "." | "..") || source.starts_with("./") || source.starts_with("../")
}
fn jsx_dev_runtime_specifier(import_source: &str) -> String {
let base = import_source.trim_end_matches('/');
if base.is_empty() {
return format!("/{JSX_DEV_RUNTIME_SUBPATH}");
}
format!("{base}/{JSX_DEV_RUNTIME_SUBPATH}")
}
fn config_relative_specifier(
file_dir: &Path,
config_dir: &Path,
specifier: &str,
) -> Option<String> {
let target = lexical_join(config_dir, specifier);
let from: Vec<Component<'_>> = file_dir.components().collect();
let to: Vec<Component<'_>> = target.components().collect();
let common = from.iter().zip(&to).take_while(|(a, b)| a == b).count();
if common == 0 {
return None;
}
let mut parts: Vec<String> = vec!["..".to_string(); from.len() - common];
if parts.is_empty() {
parts.push(".".to_string());
}
for component in &to[common..] {
parts.push(component.as_os_str().to_str()?.to_string());
}
Some(parts.join("/"))
}
fn lexical_join(dir: &Path, relative: &str) -> PathBuf {
let mut joined = dir.to_path_buf();
for component in Path::new(relative).components() {
match component {
Component::CurDir => {}
Component::ParentDir => {
joined.pop();
}
other => joined.push(other.as_os_str()),
}
}
joined
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dev_runtime_specifier_has_one_separator() {
assert_eq!(
jsx_dev_runtime_specifier("preact"),
"preact/jsx-dev-runtime"
);
assert_eq!(
jsx_dev_runtime_specifier("./src/jsx/"),
"./src/jsx/jsx-dev-runtime"
);
assert_eq!(jsx_dev_runtime_specifier("."), "./jsx-dev-runtime");
}
#[test]
fn lexical_join_removes_dot_segments() {
assert_eq!(
lexical_join(Path::new("/project/app"), "../src/./jsx/jsx-dev-runtime"),
PathBuf::from("/project/src/jsx/jsx-dev-runtime")
);
}
#[test]
fn config_relative_specifier_starts_from_the_module_directory() {
assert_eq!(
config_relative_specifier(
Path::new("/project/src/deep"),
Path::new("/project"),
"./src/jsx/jsx-dev-runtime"
)
.as_deref(),
Some("../jsx/jsx-dev-runtime")
);
assert_eq!(
config_relative_specifier(Path::new("/project"), Path::new("/project"), "./jsx/x")
.as_deref(),
Some("./jsx/x")
);
}
#[test]
fn relative_sources() {
assert!(is_relative("./src/jsx"));
assert!(is_relative("../jsx"));
assert!(is_relative("."));
assert!(!is_relative("preact"));
assert!(!is_relative("@emotion/react"));
}
}