Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changepacks/changepack_log_imported_retarget_relevance.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch",
"packages/next-plugin/package.json": "Patch"
},
"note": "Compile known imported styled withComponent calls without requiring an unused styling-package import; preserve semantic import boundaries and share relevance with plugin filtering. No new build errors.",
"date": "2026-10-24T00:00:00.000Z"
}
29 changes: 28 additions & 1 deletion libs/extractor/src/imported_styled.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ use crate::utils::{is_vanilla_extract_file, unwrap_syntax_only};
use crate::visit::DevupVisitor;
use crate::{ExtractOption, ModuleResolver};

pub(crate) mod relevance;

/// What a module exports, which marks the components other files select
#[derive(Default)]
pub(crate) struct Exports {
Expand Down Expand Up @@ -347,6 +349,7 @@ impl<'a> Reader<'_, 'a> {
&format!("{}/compat", self.option.package),
),
names: FxHashSet::default(),
scoping: &scoping,
};
used.visit_program(program);
crate::style_values::selectors(program, &mut |expression| {
Expand Down Expand Up @@ -460,6 +463,29 @@ struct Used<'s> {
css_prop: bool,
css: CssTakers<'s>,
names: FxHashSet<String>,
scoping: &'s oxc_semantic::Scoping,
}

impl Used<'_> {
fn retarget(&self, expression: &Expression<'_>) -> Option<String> {
let root = match unwrap_syntax_only(expression) {
Expression::Identifier(identifier) => identifier,
Expression::StaticMemberExpression(member) => {
let Expression::Identifier(identifier) = &member.object else {
return None;
};
identifier
}
_ => return None,
};
let symbol = self
.scoping
.get_reference(root.reference_id.get()?)
.symbol_id()?;
(self.scoping.get_root_binding(root.name) == Some(symbol))
.then(|| written_name(expression))
.flatten()
}
}

impl<'a> Visit<'a> for Used<'_> {
Expand All @@ -473,7 +499,7 @@ impl<'a> Visit<'a> for Used<'_> {
Expression::StaticMemberExpression(member)
if member.property.name == "withComponent" =>
{
written_name(&member.object)
self.retarget(&member.object)
}
_ if self.css.property(call, |_| true).is_some() => call
.arguments
Expand Down Expand Up @@ -503,6 +529,7 @@ impl<'a> Visit<'a> for Used<'_> {
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
mod relevance_tests;
use std::collections::{BTreeSet, HashMap};

use insta::assert_debug_snapshot;
Expand Down
54 changes: 54 additions & 0 deletions libs/extractor/src/imported_styled/relevance.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use oxc_allocator::Allocator;
use oxc_ast::builder::AstBuilder;
use oxc_ast_visit::Visit;
use rustc_hash::FxHashSet;

use super::{Reading, Used};
use crate::css_prop::{CssProp, CssTakers};
use crate::{ExtractOption, ModuleResolver};

pub(crate) fn candidate(code: &str) -> bool {
code.contains("withComponent") && code.contains("import")
}

pub(crate) fn read<'a>(
allocator: &'a Allocator,
filename: &str,
code: &'a str,
option: &ExtractOption,
resolver: Option<&ModuleResolver>,
) -> Reading<'a> {
if resolver.is_none() || !candidate(code) {
return Reading::default();
}
let Ok(program) = crate::parsing::parse(allocator, filename, code) else {
return Reading::default();
};
let scoping = oxc_semantic::SemanticBuilder::new()
.build(&program)
.semantic
.into_scoping();
let mut used = Used {
styled: FxHashSet::default(),
css_prop: false,
css: CssTakers::new(&program, &scoping, CssProp::Off, ""),
names: FxHashSet::default(),
scoping: &scoping,
};
used.visit_program(&program);
if used.names.is_empty() {
return Reading::default();
}
let mut reading = super::read(
&AstBuilder::new(allocator),
&program,
filename,
option,
resolver,
CssProp::Off,
);
reading
.components
.retain(|name, component| used.names.contains(name) && component.definition.is_some());
reading
}
110 changes: 110 additions & 0 deletions libs/extractor/src/imported_styled/tests/relevance_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
use super::*;
use crate::has_devup_ui_with;
use rstest::rstest;

fn resolve(specifier: &str, _: &str) -> Option<ResolvedModule> {
let code = match specifier {
"./base" => base_module(),
"./forward" => "export { Dyn } from './base';".to_string(),
"./object" => "export const Dyn = { withComponent: (tag) => tag };".to_string(),
"./unreadable" => {
"import styled from '@emotion/styled'; export const Dyn = styled(unknown)`color:red;`;"
.to_string()
}
_ => return None,
};
Some(ResolvedModule {
path: format!("/src/{}.tsx", specifier.trim_start_matches("./")),
code,
})
}

#[rstest]
#[case(
"named",
"import { Dyn } from './base'; export const A = Dyn.withComponent('a');"
)]
#[case(
"reexport",
"import { Dyn } from './forward'; export const A = Dyn.withComponent('a');"
)]
#[case(
"namespace",
"import * as UI from './base'; export const A = UI.Dyn.withComponent('a');"
)]
#[case(
"trigger",
"import { styled } from '@devup-ui/react'; import { Dyn } from './base'; export const A = Dyn.withComponent('a');"
)]
#[serial]
fn imported_retargets_are_relevant_without_package_imports(#[case] name: &str, #[case] code: &str) {
reset();
let option = option();
assert!(has_devup_ui_with(
"/src/app.tsx",
code,
&option.package,
&option.import_aliases,
Some(&resolve),
));
let output = extract_with_modules("/src/app.tsx", code, option, false, &resolve).unwrap();
assert_ne!(output.css_file, None);
assert_debug_snapshot!(name, format!("{}\n{:?}", output.code, output.dependencies));
}

#[rstest]
#[case(
"import { Dyn } from './base'; export function retarget(Dyn) { return Dyn.withComponent('a'); }"
)]
#[case(
"import * as UI from './base'; export function retarget(UI) { return UI.Dyn.withComponent('a'); }"
)]
#[case("import type { Dyn } from './base'; export const A = Dyn.withComponent('a');")]
#[case("import { type Dyn } from './base'; export const A = Dyn.withComponent('a');")]
#[case("import { Dyn } from './object'; export const A = Dyn.withComponent('a');")]
#[case("import { Dyn } from './missing'; export const A = Dyn.withComponent('a');")]
#[case("import { Dyn } from './unreadable'; export const A = Dyn.withComponent('a');")]
#[case("import * as UI from './base'; export const A = UI[key].withComponent('a');")]
#[case("import * as UI from './base'; export const A = UI.more.Dyn.withComponent('a');")]
#[case("import './base'; export const A = unknown.withComponent('a');")]
#[case("import './base'; export const A = factory().withComponent('a');")]
#[case("import { Dyn } from './base'; export const A = Dyn;")]
#[case("const A = 'withComponent';")]
#[case("import { Dyn } from './base'; // withComponent\nexport const A = Dyn;")]
#[case("import { Dyn } from './base'; export const A = ; // withComponent")]
#[serial]
fn unrelated_retargets_remain_unchanged(#[case] code: &str) {
reset();
let option = option();
assert!(!has_devup_ui_with(
"/src/app.tsx",
code,
&option.package,
&option.import_aliases,
Some(&resolve),
));
let output = extract_with_modules("/src/app.tsx", code, option, false, &resolve).unwrap();
assert_eq!(output.code, code);
assert_eq!(output.dependencies, Vec::<String>::new());
assert_eq!(output.css_file, None);
}

#[test]
#[serial]
fn retargets_without_a_resolver_remain_unchanged() {
let code = "import { Dyn } from './base'; export const A = Dyn.withComponent('a');";
let option = option();
assert!(!has_devup_ui_with(
"/src/app.tsx",
code,
&option.package,
&option.import_aliases,
None,
));
assert_eq!(
crate::extract_without_source_map("/src/app.tsx", code, option)
.unwrap()
.code,
code
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
source: libs/extractor/src/imported_styled/tests/relevance_tests.rs
expression: "format!(\"{}\\n{:?}\", output.code, output.dependencies)"
---
"import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport { Dyn } from \"./base\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"a\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"big\": __devupOmit0, \"theme\": __devupOmit1, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"b--a a--b d\", className].filter(Boolean).join(\" \")} style={{\n\t...{ \"--c\": (p=>p.big?`20px`:`10px`)(rest) },\n\t...style\n}} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n[\"/src/base.tsx\"]"
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
source: libs/extractor/src/imported_styled/tests/relevance_tests.rs
expression: "format!(\"{}\\n{:?}\", output.code, output.dependencies)"
---
"import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport * as UI from \"./base\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"a\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"big\": __devupOmit0, \"theme\": __devupOmit1, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"b--a a--b d\", className].filter(Boolean).join(\" \")} style={{\n\t...{ \"--c\": (p=>p.big?`20px`:`10px`)(rest) },\n\t...style\n}} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n[\"/src/base.tsx\"]"
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
source: libs/extractor/src/imported_styled/tests/relevance_tests.rs
expression: "format!(\"{}\\n{:?}\", output.code, output.dependencies)"
---
"import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport { Dyn } from \"./forward\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"a\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"big\": __devupOmit0, \"theme\": __devupOmit1, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"c--a a--b d\", className].filter(Boolean).join(\" \")} style={{\n\t...{ \"--c\": (p=>p.big?`20px`:`10px`)(rest) },\n\t...style\n}} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n[\"/src/base.tsx\", \"/src/forward.tsx\"]"
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
source: libs/extractor/src/imported_styled/tests/relevance_tests.rs
expression: "format!(\"{}\\n{:?}\", output.code, output.dependencies)"
---
"import \"@devup-ui/react/devup-ui.css\";\nimport { forwardRef as __devupForwardRef } from \"react\";\nimport { Dyn } from \"./base\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (({ style, className, as: DevupAs = \"a\", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries((({ \"big\": __devupOmit0, \"theme\": __devupOmit1, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\"))} as={forwardedAs} className={[\"b--a a--b c\", className].filter(Boolean).join(\" \")} style={{\n\t...{ \"--d\": (p=>p.big?`20px`:`10px`)(rest) },\n\t...style\n}} />)({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n[\"/src/base.tsx\"]"
28 changes: 24 additions & 4 deletions libs/extractor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,13 @@ fn extract_source(
// gives an element a `css` prop, or had an import rewritten
let has_relevant_import = transformed_code.contains(option.package.as_str())
|| transformed_code.contains(STYLEX_PACKAGE);
let allocator = Allocator::default();
let retargeted = if !has_relevant_import && css_prop == css_prop::CssProp::Off {
imported_styled::relevance::read(&allocator, filename, &transformed_code, &option, resolver)
} else {
imported_styled::Reading::default()
};
let has_imported_retarget = !retargeted.components.is_empty();
let unchanged = || ExtractOutput {
styles: FxHashSet::default(),
code: code.to_string(),
Expand All @@ -450,6 +457,7 @@ fn extract_source(
.iter()
.any(|(start, end, _)| &namespace_code[*start..*end] == "@emotion/react");
if !has_relevant_import
&& !has_imported_retarget
&& css_prop == css_prop::CssProp::Off
&& (alias_edits.is_empty() || pragma_rewritten)
{
Expand Down Expand Up @@ -587,8 +595,6 @@ fn extract_source(
css_files.push(main_css_path(&option.css_dir));
}
css_files.push(css_file.clone());
let allocator = Allocator::default();

let mut program = parsing::parse(&allocator, filename, code_to_parse)?;
let header_pragmas = jsx_pragmas::HeaderPragmas::capture(&program);
let styled_capability = extractor::styled_capability::StyledCapability::reserve(&program);
Expand All @@ -612,7 +618,9 @@ fn extract_source(
failures.dedup();
return Err(failures.join("\n").into());
}
let imported = if processed_code.is_none() {
let imported = if has_imported_retarget {
retargeted
} else if processed_code.is_none() {
imported_styled::read(
&oxc_ast::builder::AstBuilder::new(&allocator),
&program,
Expand Down Expand Up @@ -645,6 +653,7 @@ fn extract_source(
visitor.reserve_styled(styled_capability);
visitor.visit_program(&mut program);
if !has_relevant_import
&& !has_imported_retarget
&& alias_edits.is_empty()
&& namespace_edits.is_empty()
&& !visitor.compiled_css_prop
Expand Down Expand Up @@ -1022,7 +1031,18 @@ pub fn has_devup_ui_with(
import_aliases,
);
!aliased.edits.is_empty() || aliased.css_prop != css_prop::CssProp::Off
})
}
|| (imported_styled::relevance::candidate(code) && {
let allocator = Allocator::default();
let option = ExtractOption {
package: package.to_string(),
import_aliases: import_aliases.clone(),
..ExtractOption::default()
};
!imported_styled::relevance::read(&allocator, filename, code, &option, resolver)
.components
.is_empty()
}))
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
Expand Down
1 change: 1 addition & 0 deletions libs/extractor/src/visit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1151,6 +1151,7 @@ impl<'a> DevupVisitor<'a> {
|| import.source.value == "@stylexjs/stylex")
});
(imports_api
|| !self.imported_styled.is_empty()
|| self.css_prop != CssProp::Off
|| crate::scope::requires(program, &self.package))
.then(|| {
Expand Down
Loading
Loading