;"),
+ ExtractOption::default(),
+ false,
+ Some(&resolver),
+ None,
+ &[".mdx".to_string()],
+ ) else {
+ panic!("ordinary JavaScript forwarding extracts");
+ };
+ // Then the forwarding path still compiles to a native element, never a placeholder.
+ assert!(output.code.contains("
impl Fn(&str, &str) -> Option
+ use<> {
+ fn resolver(&self) -> impl Fn(&str, &str) -> Option + use<> {
let root = self.root.clone();
move |specifier, importer| {
let base = if specifier.starts_with('.') {
@@ -48,9 +48,12 @@ impl Project {
candidates
.into_iter()
.find(|candidate| candidate.is_file())
- .map(|path| ResolvedModule {
- code: fs::read_to_string(&path).unwrap(),
- path: path.to_string_lossy().replace('\\', "/"),
+ .map(|path| {
+ ModuleResolution::Resolved(ResolvedModule {
+ code: fs::read_to_string(&path).unwrap(),
+ path: path.to_string_lossy().replace('\\', "/"),
+ source_type: None,
+ })
})
}
}
@@ -1070,3 +1073,20 @@ fn aliases_that_are_not_package_exports_stay_declarations() {
assert!(code.contains("N1"), "{code}");
assert!(project.error("ui.tsx").contains("ui.tsx:3:"));
}
+
+#[test]
+fn unsupported_child_grammar_does_not_admit_barrel_exports() {
+ let module = ResolvedModule {
+ path: "/src/ui.data".to_string(),
+ code: "export { Box } from '@devup-ui/react'".to_string(),
+ source_type: None,
+ };
+
+ let exports = super::analyze(&module, "@devup-ui/react");
+
+ assert_eq!(exports.path, module.path);
+ assert_eq!(exports.named.len(), 0);
+ assert_eq!(exports.stars, Vec::::new());
+ assert!(exports.mentions_package);
+ assert!(!exports.trivial);
+}
diff --git a/libs/extractor/src/barrel_pipeline_tests.rs b/libs/extractor/src/barrel_pipeline_tests.rs
index 7293f7934..6c82f4499 100644
--- a/libs/extractor/src/barrel_pipeline_tests.rs
+++ b/libs/extractor/src/barrel_pipeline_tests.rs
@@ -1,15 +1,80 @@
use crate::dead_properties_test_utils::{failure, position};
-use crate::{ExtractOption, ImportAlias, ResolvedModule, extract_with_modules};
+use crate::{ExtractOption, ImportAlias, ModuleResolution, ResolvedModule, extract_with_modules};
use css::{class_map::reset_class_map, file_map::reset_file_map};
use rstest::rstest;
use serial_test::serial;
-fn resolver(specifier: &str, _: &str) -> Option {
+#[test]
+#[serial]
+fn follows_barrel_when_root_and_child_have_explicit_compiled_languages()
+-> Result<(), Box> {
+ // Given: JSX precedes the re-export in a custom-extension compiled child.
+ let _state = crate::test_state::TestStateGuard::new();
+ let resolver = |_: &str, _: &str| {
+ Some(ModuleResolution::Resolved(ResolvedModule {
+ path: "/src/ui.mdown".to_string(),
+ code: "export const before = ; export { Box } from '@devup-ui/react';"
+ .to_string(),
+ source_type: Some(crate::ExtractSourceType::CompiledMdx),
+ }))
+ };
+ // When: the root also keeps its real custom filename.
+ let output = crate::extract_with_source_type(
+ "/src/app.mdown",
+ "import * as UI from './ui'; export const before = ; export const a = ;",
+ ExtractOption::default(),
+ true,
+ Some(&resolver),
+ Some(crate::ExtractSourceType::CompiledMdx),
+ )?;
+ // Then: the barrel use compiles and the map/dependency retain real identities.
+ assert!(!output.code.contains(" Result<(), Box> {
+ // Given: an explicitly ignored re-export alongside a genuine Devup export.
+ let _state = crate::test_state::TestStateGuard::new();
+ let resolver = |specifier: &str, _: &str| match specifier {
+ "./ui" => Some(ModuleResolution::Resolved(ResolvedModule {
+ path: "/src/ui.ts".to_string(),
+ code: "export { Box } from '@devup-ui/react'; export { missing } from './ignored';"
+ .to_string(),
+ source_type: None,
+ })),
+ "./ignored" => Some(ModuleResolution::Ignored),
+ _ => None,
+ };
+ // When: both names are imported but only Box is a styling API.
+ let output = extract_with_modules(
+ "/src/app.tsx",
+ "import { Box, missing } from './ui'; export const a = ; export const b = missing;",
+ ExtractOption::default(),
+ false,
+ &resolver,
+ )?;
+ // Then: ignore is not an unreadable-module fault or a watched file.
+ assert!(!output.code.contains(" Option {
match specifier {
- "./ui" => Some(ResolvedModule {
+ "./ui" => Some(ModuleResolution::Resolved(ResolvedModule {
path: "/src/ui.ts".to_string(),
code: "export { css, Box } from '@devup-ui/react';".to_string(),
- }),
+ source_type: None,
+ })),
_ => None,
}
}
diff --git a/libs/extractor/src/build_time_values.rs b/libs/extractor/src/build_time_values.rs
index d72f62548..6b92ca0da 100644
--- a/libs/extractor/src/build_time_values.rs
+++ b/libs/extractor/src/build_time_values.rs
@@ -65,9 +65,10 @@ pub(crate) fn has_build_time_values(
filename,
&option.package,
&option.import_aliases,
+ crate::parser_source_type(filename).unwrap_or_default(),
);
let allocator = Allocator::default();
- let Some(mut program) = parse(&allocator, filename, &aliased.code) else {
+ let Some(mut program) = parse(&allocator, filename, &aliased.code, None) else {
return false;
};
let inlined = crate::imported_constants::inline_constants(
@@ -135,8 +136,13 @@ const UNCERTAIN_MEMBERS: [&str; 9] = [
"toString",
];
-fn parse<'a>(allocator: &'a Allocator, filename: &str, code: &'a str) -> Option> {
- let source_type = crate::parsing::source_type(filename).ok()?;
+fn parse<'a>(
+ allocator: &'a Allocator,
+ filename: &str,
+ code: &'a str,
+ mode: Option,
+) -> Option> {
+ let source_type = crate::source_type::parser_type(filename, mode).ok()?;
let parsed = Parser::new(allocator, code, source_type).parse();
(!parsed.fatal_error).then_some(parsed.program)
}
@@ -928,12 +934,13 @@ pub(crate) fn evaluate(
option: &ExtractOption,
resolver: Option<&ModuleResolver>,
unknown: &crate::imported_constants::Unknown,
+ mode: Option,
) -> Option<(
String,
Vec,
BTreeSet,
)> {
- let (mut values, dependencies) = compute(code, filename, option, resolver, unknown)?;
+ let (mut values, dependencies) = compute(code, filename, option, resolver, unknown, mode)?;
values.sort_unstable_by_key(|(span, _)| span.start);
let mut result = String::with_capacity(code.len());
let mut edits = Vec::with_capacity(values.len());
@@ -980,13 +987,14 @@ fn compute(
option: &ExtractOption,
resolver: Option<&ModuleResolver>,
unknown: &crate::imported_constants::Unknown,
+ mode: Option,
) -> Option<(Vec, BTreeSet)> {
use std::fmt::Write;
use boa_engine::{Context, JsObject, Source};
let allocator = Allocator::default();
- let program = parse(&allocator, filename, code)?;
+ let program = parse(&allocator, filename, code, mode)?;
let is_style = |source: &str| {
source.starts_with(option.package.as_str()) || option.import_aliases.contains_key(source)
};
@@ -1071,7 +1079,8 @@ fn compute(
.collect();
let _ = writeln!(module, "[{}];", names.join(", "));
- let script = crate::vanilla_extract::strip_typescript(&module, filename);
+ let script =
+ crate::vanilla_extract::strip_typescript_with_type(&module, filename, mode).ok()?;
let mut context = Context::default();
context
.runtime_limits_mut()
diff --git a/libs/extractor/src/class_names_binding_tests.rs b/libs/extractor/src/class_names_binding_tests.rs
index dfaa3ff86..f4f8dbff4 100644
--- a/libs/extractor/src/class_names_binding_tests.rs
+++ b/libs/extractor/src/class_names_binding_tests.rs
@@ -30,9 +30,12 @@ fn compile(code: &str) -> ExtractOutput {
},
false,
&|specifier, _| {
- (specifier == "./data").then(|| ResolvedModule {
- path: "data.ts".to_string(),
- code: "export const data = { padding: 7 }; export const space = 3;".to_string(),
+ (specifier == "./data").then(|| {
+ crate::ModuleResolution::Resolved(ResolvedModule {
+ path: "data.ts".to_string(),
+ code: "export const data = { padding: 7 }; export const space = 3;".to_string(),
+ source_type: None,
+ })
})
},
)
diff --git a/libs/extractor/src/class_selection_support.rs b/libs/extractor/src/class_selection_support.rs
index 61a0a9855..478b192b9 100644
--- a/libs/extractor/src/class_selection_support.rs
+++ b/libs/extractor/src/class_selection_support.rs
@@ -23,9 +23,12 @@ pub fn compile(files: &[(&str, &str)], option: ExtractOption) -> Modules {
sources
.iter()
.find(|(name, _)| *name == path)
- .map(|(name, code)| ResolvedModule {
- path: name.clone(),
- code: code.clone(),
+ .map(|(name, code)| {
+ crate::ModuleResolution::Resolved(ResolvedModule {
+ path: name.clone(),
+ code: code.clone(),
+ source_type: None,
+ })
})
};
files
@@ -78,6 +81,7 @@ pub fn observe(outputs: &Modules, observer: &str, globals: &str) -> serde_json::
map: output.map.clone(),
css_file: output.css_file.clone(),
dependencies: output.dependencies.clone(),
+ deferred_defaults: output.deferred_defaults.clone(),
},
)
}));
diff --git a/libs/extractor/src/compiled_guard/callable.rs b/libs/extractor/src/compiled_guard/callable.rs
new file mode 100644
index 000000000..c6cbec956
--- /dev/null
+++ b/libs/extractor/src/compiled_guard/callable.rs
@@ -0,0 +1,132 @@
+use oxc_allocator::{Address, UnstableAddress};
+use oxc_ast::ast::{
+ BindingPattern, Declaration, ExportDefaultDeclarationKind, Expression, Function, Program,
+ Statement, VariableDeclaration, VariableDeclarationKind,
+};
+use oxc_ast_visit::Visit;
+use rustc_hash::{FxHashMap, FxHashSet};
+
+use super::{traversal::Collector, writes::Writes};
+
+#[derive(Clone, PartialEq, Eq)]
+pub(super) enum Proof {
+ Declaration(String, Address),
+ Function,
+ Alias(String),
+ Other,
+}
+
+impl Proof {
+ pub fn expression(node: &Expression<'_>) -> Self {
+ match node {
+ Expression::FunctionExpression(_) | Expression::ArrowFunctionExpression(_) => {
+ Self::Function
+ }
+ Expression::Identifier(id) => Self::Alias(id.name.to_string()),
+ _ => Self::Other,
+ }
+ }
+
+ fn function(node: &Function<'_>) -> Self {
+ Self::Declaration(
+ node.id
+ .as_ref()
+ .map_or("", |id| id.name.as_str())
+ .to_owned(),
+ node.unstable_address(),
+ )
+ }
+
+ pub fn default_declaration(node: &ExportDefaultDeclarationKind<'_>) -> Self {
+ match node {
+ ExportDefaultDeclarationKind::FunctionDeclaration(function) => Self::function(function),
+ _ => node.as_expression().map_or(Self::Other, Self::expression),
+ }
+ }
+}
+
+#[derive(Default)]
+pub(super) struct Callables {
+ pub locals: FxHashMap,
+ pub default: Option,
+}
+
+impl Callables {
+ pub fn prepare(&mut self, program: &Program<'_>) {
+ for statement in &program.body {
+ match statement {
+ Statement::FunctionDeclaration(node) => self.function(node),
+ Statement::VariableDeclaration(node) => self.variables(node),
+ Statement::ExportDeclaration(node) => match &node.declaration {
+ Declaration::FunctionDeclaration(node) => self.function(node),
+ Declaration::VariableDeclaration(node) => self.variables(node),
+ _ => {}
+ },
+ Statement::ExportDefaultDeclaration(node) => {
+ if let ExportDefaultDeclarationKind::FunctionDeclaration(function) =
+ &node.declaration
+ {
+ self.function(function);
+ }
+ }
+ _ => {}
+ }
+ }
+ }
+
+ fn function(&mut self, node: &Function<'_>) {
+ let proof = Proof::function(node);
+ let name = node.id.as_ref().map_or("", |id| id.name.as_str());
+ self.locals.insert(name.to_owned(), proof);
+ }
+
+ fn variables(&mut self, node: &VariableDeclaration<'_>) {
+ if node.kind != VariableDeclarationKind::Const {
+ return;
+ }
+ for declaration in &node.declarations {
+ if let (BindingPattern::BindingIdentifier(id), Some(init)) =
+ (&declaration.id, &declaration.init)
+ {
+ self.locals
+ .insert(id.name.to_string(), Proof::expression(init));
+ }
+ }
+ }
+
+ fn callable(&self) -> bool {
+ let mut current = self.default.as_ref();
+ let mut seen = FxHashSet::default();
+ while let Some(proof) = current {
+ match proof {
+ Proof::Declaration(name, _) => {
+ return name.is_empty() || self.locals.get(name) == Some(proof);
+ }
+ Proof::Function => return true,
+ Proof::Alias(name) => {
+ if !seen.insert(name) {
+ return false;
+ }
+ current = self.locals.get(name);
+ }
+ Proof::Other => return false,
+ }
+ }
+ false
+ }
+}
+
+impl Collector {
+ pub fn finish(&mut self, program: &Program<'_>) {
+ if let Some(callables) = &mut self.callables {
+ Writes {
+ scopes: &self.scopes,
+ callables,
+ }
+ .visit_program(program);
+ if let Some(observation) = &mut self.facts.observation {
+ observation.default_callable = callables.callable();
+ }
+ }
+ }
+}
diff --git a/libs/extractor/src/compiled_guard/callable_tests.rs b/libs/extractor/src/compiled_guard/callable_tests.rs
new file mode 100644
index 000000000..5cd6579f7
--- /dev/null
+++ b/libs/extractor/src/compiled_guard/callable_tests.rs
@@ -0,0 +1,190 @@
+use std::{error::Error, fmt::Write};
+
+use rstest::rstest;
+use serde_json::json;
+
+use super::tests::facts;
+
+const OBJECT_ASSIGNMENT_SOURCE: &str =
+ "function Content(){}; ({value:Content}={value:0}); export default Content;";
+const FOR_IN_SOURCE: &str =
+ "function Content(){}; for(Content in {x:0}){}; export default Content;";
+const LOOP_PATTERN_SOURCE: &str =
+ "function Content(){}; for({value:Content} of [{value:0}]){}; export default Content;";
+
+#[rstest]
+#[case("function Content(){}; export default Content;", true)]
+#[case("export default function Content(){};", true)]
+#[case("export default function(){};", true)]
+#[case("export default ()=>0;", true)]
+#[case("const Content=(()=>0); export default Content;", true)]
+#[case("const Content=function Named(){}; export default Content;", true)]
+#[case("export const Content=()=>0; export {Content as default};", true)]
+#[case("export function Content(){}; export {Content as default};", true)]
+#[case(
+ "function Content(){}; const A=Content; const B=A; export default B;",
+ true
+)]
+#[case(
+ "function Content(){}; function change(Content){Content=0;} export default Content;",
+ true
+)]
+#[case(
+ "function Content(){}; {let Content; Content=0;} export default Content;",
+ true
+)]
+#[case(
+ "function Content(){}; try{}catch(Content){Content=0;} export default Content;",
+ true
+)]
+#[case(
+ "function Content(){}; for(let Content of [0]){Content++;} export default Content;",
+ true
+)]
+#[case(
+ "function Content(){}; function change(){var Content; Content=0;} export default Content;",
+ true
+)]
+#[case(
+ "function Content(){}; const change=function Content(){Content=0;}; export default Content;",
+ true
+)]
+#[case("function Content(){}; Content.value=0; export default Content;", true)]
+#[case(
+ "function Content(){}; [Content.value]=[0]; export default Content;",
+ true
+)]
+#[case(
+ "function Content(){}; let x; ({[Content]:x}={}); export default Content;",
+ true
+)]
+#[case(
+ "function Content(){}; function change(){function Content(){}; Content=0;} export default Content;",
+ true
+)]
+#[case(
+ "function Content(){}; function change(Content){eval('Content=0');} export default Content;",
+ true
+)]
+#[case(
+ "function Content(){}; {let Content=1; eval('Content=0');} export default Content;",
+ true
+)]
+#[case("const Content=()=>0; eval('void 0'); export default Content;", true)]
+#[case(
+ "function Content(){}; eval?.('void 0'); export default Content;",
+ true
+)]
+#[case(
+ "function Content(){}; (0,eval)('void 0'); export default Content;",
+ true
+)]
+#[case("function Content(){}; Content=0; export default Content;", false)]
+#[case(
+ "function Content(){}; Content=0; const A=Content; const B=A; export default B;",
+ false
+)]
+#[case("export default function Content(){}; Content=0;", false)]
+#[case("function Content(){}; Content=0; export {Content as default};", false)]
+#[case(OBJECT_ASSIGNMENT_SOURCE, false)]
+#[case("function Content(){}; [Content]=[0]; export default Content;", false)]
+#[case(
+ "function Content(){}; ({value:[Content=0]}={value:[]}); export default Content;",
+ false
+)]
+#[case(
+ "function Content(){}; [...Content]=[]; export default Content;",
+ false
+)]
+#[case(
+ "function Content(){}; ({...Content}={}); export default Content;",
+ false
+)]
+#[case("function Content(){}; Content++; export default Content;", false)]
+#[case(
+ "function Content(){}; for(Content of [0]){}; export default Content;",
+ false
+)]
+#[case(FOR_IN_SOURCE, false)]
+#[case("function Content(){}; Content+=0; export default Content;", false)]
+#[case(LOOP_PATTERN_SOURCE, false)]
+#[case(
+ "function Content(){}; function change(){Content=0;} export default Content;",
+ false
+)]
+#[case("function Content(){Content=0;} export default Content;", false)]
+#[case(
+ "function Content(){}; function change(x=(Content=0)){} export default Content;",
+ false
+)]
+#[case(
+ "function Content(){}; let x; ({[Content=0]:x}={}); export default Content;",
+ false
+)]
+#[case(
+ "function Content(){}; const holder={}; [holder[Content=0]]=[1]; export default Content;",
+ false
+)]
+#[case(
+ "function Content(){}; const holder={}; ({x:holder[Content=0]}={x:1}); export default Content;",
+ false
+)]
+#[case("function Content(){}; export {Content as default}; Content=0;", false)]
+#[case(
+ "function Content(){}; eval('Content=0'); export default Content;",
+ false
+)]
+#[case(
+ "function Content(){}; const code='Content=0'; eval(code); export default Content;",
+ false
+)]
+#[case(
+ "function Content(){}; eval('Content=0'); const A=Content; const B=A; export default B;",
+ false
+)]
+#[case("export default function Content(){}; eval('Content=0');", false)]
+#[case(
+ "function Content(){}; function change(){eval('Content=0');} export default Content;",
+ false
+)]
+#[case(
+ "function Content(){eval('Content=0');} export default Content;",
+ false
+)]
+#[case(
+ "function Content(){}; function change(Content){eval('Content=0');} eval('Content=0'); export default Content;",
+ false
+)]
+#[case("function Content(){};", false)]
+#[case("export default 0;", false)]
+#[case("export default missing;", false)]
+#[case("const A=B; const B=A; export default A;", false)]
+#[case("export {default} from 'content';", false)]
+#[case("let Content=()=>0; export default Content;", false)]
+#[case("export default class Content{};", false)]
+fn resolves_callable_when_local_provenance_is_intact(
+ #[case] code: &str,
+ #[case] callable: bool,
+) -> Result<(), Box> {
+ // Given: a local default proof with lexical writes or shadows.
+ // When: observation resolves aliases after collecting all writes.
+ let result = facts(code, true)?;
+ // Then: only intact local callable provenance is certified.
+ assert_eq!(result["observation"]["defaultCallable"], json!(callable));
+ Ok(())
+}
+
+#[test]
+fn resolves_iteratively_when_alias_chain_is_long() -> Result<(), Box> {
+ // Given: a long acyclic chain of local const aliases.
+ let mut aliases = String::new();
+ for index in 0..5000 {
+ write!(aliases, "const A{}=A{index};", index + 1)?;
+ }
+ let code = format!("function Content(){{}} const A0=Content;{aliases}export default A5000;");
+ // When: default proof follows the aliases.
+ let result = facts(&code, true)?;
+ // Then: the terminal function is reached without recursive alias evaluation.
+ assert_eq!(result["observation"]["defaultCallable"], json!(true));
+ Ok(())
+}
diff --git a/libs/extractor/src/compiled_guard/coverage_helper_tests.rs b/libs/extractor/src/compiled_guard/coverage_helper_tests.rs
new file mode 100644
index 000000000..3aa963314
--- /dev/null
+++ b/libs/extractor/src/compiled_guard/coverage_helper_tests.rs
@@ -0,0 +1,177 @@
+use std::error::Error;
+
+use oxc_allocator::Allocator;
+use oxc_ast::ast::{PropertyDefinition, Statement};
+use oxc_ast_visit::Visit;
+use oxc_parser::Parser;
+use oxc_span::SourceType;
+use rstest::rstest;
+use serde_json::{Value, json};
+
+use super::{
+ compiled_guard_facts_internal, diagnostics,
+ origins::{export_name, local_name, property_name},
+ scopes::Scopes,
+ text::Text,
+ traversal::Collector,
+ wire::{Facts, Origin, Positions},
+};
+
+#[rstest]
+#[case("a\r\n😀x", (2, 4))]
+#[case("a\rb\nc\u{2028}d\u{2029}😀", (5, 3))]
+#[case("a\r\n\n", (3, 1))]
+fn location_when_line_endings_and_astral_characters_are_mixed(
+ #[case] code: &str,
+ #[case] expected: (usize, usize),
+) -> Result<(), Box> {
+ // Given: byte positions at the end of text with JS line terminators.
+ let offset = u32::try_from(code.len())?;
+ // When: diagnostics convert bytes to line and UTF-16 column.
+ let result = diagnostics::location(code, offset);
+ // Then: CRLF counts once and astral characters count two columns.
+ assert_eq!(result, expected);
+ Ok(())
+}
+
+#[rstest]
+#[case("\r\n")]
+#[case("\r")]
+#[case("\n")]
+#[case("\u{2028}")]
+#[case("\u{2029}")]
+fn diagnostic_when_invalid_js_follows_a_line_terminator(#[case] ending: &str) {
+ // Given: valid astral text followed by a syntactically invalid declaration.
+ let code = format!("let emoji='😀';{ending}const value = ;");
+ // When: the public facts API rejects the actual parser error.
+ let result = compiled_guard_facts_internal(&code, false);
+ // Then: the diagnostic locates the bad token on the second line.
+ assert!(matches!(result, Err(error) if error.to_string().starts_with("compiled output:2:15:")));
+}
+
+#[test]
+fn property_names_when_parser_produces_private_and_computed_keys() {
+ #[derive(Default)]
+ struct Keys(Vec