fallow_graph/resolve/
jsx_runtime.rs1use std::path::{Component, Path, PathBuf};
10
11use fallow_config::JsxImportSourceRule;
12use fallow_types::extract::{ImportInfo, ImportedName, ModuleInfo};
13use globset::{GlobBuilder, GlobSet, GlobSetBuilder};
14use oxc_span::Span;
15use rustc_hash::FxHashSet;
16
17use super::specifier::resolve_import_specifier;
18use super::types::{ResolveContext, ResolveResult, ResolvedImport};
19
20const JSX_DEV_RUNTIME_SUBPATH: &str = "jsx-dev-runtime";
22
23const JSX_DEV_RUNTIME_BINDINGS: [&str; 2] = ["jsxDEV", "Fragment"];
26
27pub struct CompiledJsxRule<'a> {
29 rule: &'a JsxImportSourceRule,
30 include: GlobSet,
31 exclude: GlobSet,
32}
33
34impl<'a> CompiledJsxRule<'a> {
35 #[must_use]
37 pub const fn rule(&self) -> &'a JsxImportSourceRule {
38 self.rule
39 }
40
41 #[must_use]
45 pub fn matches(&self, file_path: &Path) -> bool {
46 file_path
47 .strip_prefix(&self.rule.config_dir)
48 .is_ok_and(|relative| {
49 self.include.is_match(relative) && !self.exclude.is_match(relative)
50 })
51 }
52}
53
54#[must_use]
58pub fn compile_jsx_rules(rules: &[JsxImportSourceRule]) -> Vec<CompiledJsxRule<'_>> {
59 rules
60 .iter()
61 .filter_map(|rule| {
62 let (include, added) = compile_globs(&rule.include);
63 if added == 0 {
64 return None;
65 }
66 let (exclude, _) = compile_globs(&rule.exclude);
67 Some(CompiledJsxRule {
68 rule,
69 include,
70 exclude,
71 })
72 })
73 .collect()
74}
75
76fn compile_globs(patterns: &[String]) -> (GlobSet, usize) {
79 let mut builder = GlobSetBuilder::new();
80 let mut added = 0;
81 for pattern in patterns {
82 if let Ok(glob) = GlobBuilder::new(pattern).literal_separator(true).build() {
83 builder.add(glob);
84 added += 1;
85 }
86 }
87 match builder.build() {
88 Ok(set) => (set, added),
89 Err(_) => (GlobSet::empty(), 0),
90 }
91}
92
93pub(super) fn resolve_config_jsx_runtime_imports(
103 ctx: &ResolveContext<'_>,
104 file_path: &Path,
105 module: &ModuleInfo,
106 rules: &[CompiledJsxRule<'_>],
107) -> Vec<ResolvedImport> {
108 if !module.jsx_runtime_from_config || rules.is_empty() {
109 return Vec::new();
110 }
111 let mut seen: FxHashSet<&str> = FxHashSet::default();
112 let mut imports = Vec::new();
113 for compiled in rules {
114 let rule = compiled.rule;
115 if !compiled.matches(file_path) || !seen.insert(rule.source.as_str()) {
116 continue;
117 }
118 let specifier = jsx_dev_runtime_specifier(&rule.source);
119 let Some(target) = resolve_runtime(ctx, file_path, rule, &specifier) else {
120 continue;
121 };
122 imports.extend(
123 JSX_DEV_RUNTIME_BINDINGS
124 .iter()
125 .map(|binding| ResolvedImport {
126 info: ImportInfo {
127 source: specifier.clone(),
128 imported_name: ImportedName::Named((*binding).to_string()),
129 local_name: String::new(),
130 is_type_only: false,
131 is_type_only_star: false,
132 from_style: false,
133 span: Span::default(),
134 source_span: Span::default(),
135 },
136 target: target.clone(),
137 }),
138 );
139 }
140 imports
141}
142
143fn resolve_runtime(
144 ctx: &ResolveContext<'_>,
145 file_path: &Path,
146 rule: &JsxImportSourceRule,
147 specifier: &str,
148) -> Option<ResolveResult> {
149 let from_file = resolve_import_specifier(ctx, file_path, specifier, false);
150 if !matches!(from_file, ResolveResult::Unresolvable(_)) {
151 return Some(from_file);
152 }
153 if !is_relative(&rule.source) {
154 return None;
155 }
156 let from_config = config_relative_specifier(file_path.parent()?, &rule.config_dir, specifier)?;
159 match resolve_import_specifier(ctx, file_path, &from_config, false) {
160 ResolveResult::Unresolvable(_) => None,
161 target => Some(target),
162 }
163}
164
165fn is_relative(source: &str) -> bool {
166 matches!(source, "." | "..") || source.starts_with("./") || source.starts_with("../")
167}
168
169fn jsx_dev_runtime_specifier(import_source: &str) -> String {
172 let base = import_source.trim_end_matches('/');
173 if base.is_empty() {
174 return format!("/{JSX_DEV_RUNTIME_SUBPATH}");
175 }
176 format!("{base}/{JSX_DEV_RUNTIME_SUBPATH}")
177}
178
179fn config_relative_specifier(
183 file_dir: &Path,
184 config_dir: &Path,
185 specifier: &str,
186) -> Option<String> {
187 let target = lexical_join(config_dir, specifier);
188 let from: Vec<Component<'_>> = file_dir.components().collect();
189 let to: Vec<Component<'_>> = target.components().collect();
190 let common = from.iter().zip(&to).take_while(|(a, b)| a == b).count();
191 if common == 0 {
192 return None;
193 }
194 let mut parts: Vec<String> = vec!["..".to_string(); from.len() - common];
195 if parts.is_empty() {
196 parts.push(".".to_string());
197 }
198 for component in &to[common..] {
199 parts.push(component.as_os_str().to_str()?.to_string());
200 }
201 Some(parts.join("/"))
202}
203
204fn lexical_join(dir: &Path, relative: &str) -> PathBuf {
207 let mut joined = dir.to_path_buf();
208 for component in Path::new(relative).components() {
209 match component {
210 Component::CurDir => {}
211 Component::ParentDir => {
212 joined.pop();
213 }
214 other => joined.push(other.as_os_str()),
215 }
216 }
217 joined
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn dev_runtime_specifier_has_one_separator() {
226 assert_eq!(
227 jsx_dev_runtime_specifier("preact"),
228 "preact/jsx-dev-runtime"
229 );
230 assert_eq!(
231 jsx_dev_runtime_specifier("./src/jsx/"),
232 "./src/jsx/jsx-dev-runtime"
233 );
234 assert_eq!(jsx_dev_runtime_specifier("."), "./jsx-dev-runtime");
235 }
236
237 #[test]
238 fn lexical_join_removes_dot_segments() {
239 assert_eq!(
240 lexical_join(Path::new("/project/app"), "../src/./jsx/jsx-dev-runtime"),
241 PathBuf::from("/project/src/jsx/jsx-dev-runtime")
242 );
243 }
244
245 #[test]
246 fn config_relative_specifier_starts_from_the_module_directory() {
247 assert_eq!(
248 config_relative_specifier(
249 Path::new("/project/src/deep"),
250 Path::new("/project"),
251 "./src/jsx/jsx-dev-runtime"
252 )
253 .as_deref(),
254 Some("../jsx/jsx-dev-runtime")
255 );
256 assert_eq!(
257 config_relative_specifier(Path::new("/project"), Path::new("/project"), "./jsx/x")
258 .as_deref(),
259 Some("./jsx/x")
260 );
261 }
262
263 #[test]
264 fn relative_sources() {
265 assert!(is_relative("./src/jsx"));
266 assert!(is_relative("../jsx"));
267 assert!(is_relative("."));
268 assert!(!is_relative("preact"));
269 assert!(!is_relative("@emotion/react"));
270 }
271}