use oxc_ast::ast::Program;
use oxc_span::{GetSpan, Span};
use fallow_types::extract::{ImportInfo, ImportedName};
use super::super::ModuleInfoExtractor;
const JSX_RUNTIME_SUBPATH: &str = "jsx-runtime";
const JSX_RUNTIME_BINDINGS: [&str; 3] = ["jsx", "jsxs", "Fragment"];
#[derive(Clone, Copy, PartialEq, Eq)]
enum JsxPragma {
ImportSource,
Runtime,
}
struct PragmaValue<'s> {
value: &'s str,
start: u32,
}
impl ModuleInfoExtractor {
pub(super) fn record_jsx_import_source_pragma(&mut self, program: &Program<'_>) {
if !self.has_jsx {
return;
}
let source_text = program.source_text;
let leading_end = program
.body
.first()
.map_or(program.span.end, |statement| statement.span().start);
let mut import_source: Option<(PragmaValue<'_>, Span)> = None;
let mut classic_runtime = false;
for comment in program
.comments
.iter()
.take_while(|comment| comment.span.end <= leading_end)
{
let content_span = comment.content_span();
let content = content_span.source_text(source_text);
for (pragma, value) in jsx_pragmas(content, content_span.start) {
match pragma {
JsxPragma::ImportSource => import_source = Some((value, comment.span)),
JsxPragma::Runtime => classic_runtime = value.value == "classic",
}
}
}
if classic_runtime {
return;
}
let Some((value, comment_span)) = import_source else {
self.jsx_runtime_from_config = true;
return;
};
let source = jsx_runtime_specifier(value.value);
let value_end = value.start + u32::try_from(value.value.len()).unwrap_or(0);
let source_span = Span::new(value.start, value_end);
for binding in JSX_RUNTIME_BINDINGS {
self.imports.push(ImportInfo {
source: source.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: comment_span,
source_span,
});
}
}
}
fn jsx_runtime_specifier(import_source: &str) -> String {
let base = import_source.trim_end_matches('/');
if base.is_empty() {
return format!("/{JSX_RUNTIME_SUBPATH}");
}
format!("{base}/{JSX_RUNTIME_SUBPATH}")
}
fn jsx_pragmas(content: &str, content_start: u32) -> Vec<(JsxPragma, PragmaValue<'_>)> {
let mut pragmas = Vec::new();
let bytes = content.as_bytes();
let mut index = 0;
while let Some(found) = content[index..].find("@jsx") {
let at = index + found;
index = at + 1;
if at > 0 && !matches!(bytes[at - 1], b' ' | b'\t' | b'\r' | b'\n' | b'*') {
continue;
}
let rest = &content[at + "@jsx".len()..];
let Some(keyword_end) = rest.find([' ', '\t']) else {
break;
};
let pragma = match &rest[..keyword_end] {
"ImportSource" => JsxPragma::ImportSource,
"Runtime" => JsxPragma::Runtime,
_ => continue,
};
let after_keyword = &rest[keyword_end..];
let value_text = after_keyword.trim_start_matches([' ', '\t']);
let value_len = value_text
.find(|c: char| c.is_ascii_whitespace() || c == '\u{0B}')
.unwrap_or(value_text.len());
let value = &value_text[..value_len];
if value.is_empty() {
continue;
}
let value_offset = content.len() - value_text.len();
index = value_offset + value_len;
pragmas.push((
pragma,
PragmaValue {
value,
start: content_start + u32::try_from(value_offset).unwrap_or(0),
},
));
}
pragmas
}
#[cfg(test)]
mod tests {
use super::*;
fn values(content: &str) -> Vec<&str> {
jsx_pragmas(content, 0)
.into_iter()
.filter(|(pragma, _)| *pragma == JsxPragma::ImportSource)
.map(|(_, value)| value.value)
.collect()
}
#[test]
fn reads_import_source_values() {
assert_eq!(values("* @jsxImportSource ../../jsx "), vec!["../../jsx"]);
assert_eq!(values("* @jsxImportSource . *"), vec!["."]);
assert_eq!(values(" @jsxImportSource\tpreact"), vec!["preact"]);
assert_eq!(
values("*\n * @jsx h\n * @jsxImportSource @emotion/react\n "),
vec!["@emotion/react"]
);
}
#[test]
fn ignores_inline_mentions_and_unknown_pragmas() {
assert!(values("see `@jsxImportSource foo` in docs").is_empty());
assert!(values("mail@jsxImportSource foo").is_empty());
assert!(values("@jsxImportSourceX foo").is_empty());
assert!(values("@jsxImportSource").is_empty());
assert!(values("@jsxImportSource ").is_empty());
}
#[test]
fn value_offset_points_into_the_source_text() {
let content = "* @jsxImportSource ./jsx ";
let pragmas = jsx_pragmas(content, 10);
let start = pragmas[0].1.start as usize - 10;
assert_eq!(&content[start..start + "./jsx".len()], "./jsx");
}
#[test]
fn runtime_specifier_has_one_separator() {
assert_eq!(jsx_runtime_specifier("preact"), "preact/jsx-runtime");
assert_eq!(jsx_runtime_specifier("hono/jsx"), "hono/jsx/jsx-runtime");
assert_eq!(jsx_runtime_specifier("../"), "../jsx-runtime");
assert_eq!(jsx_runtime_specifier("./"), "./jsx-runtime");
assert_eq!(jsx_runtime_specifier("."), "./jsx-runtime");
}
}