Skip to content
Merged
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"
}
3 changes: 3 additions & 0 deletions 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 @@ -743,6 +745,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
65 changes: 65 additions & 0 deletions libs/extractor/src/imported_styled/relevance.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use oxc_allocator::Allocator;
use oxc_ast::builder::AstBuilder;
use oxc_ast_visit::Visit;
use rustc_hash::FxHashSet;
use std::rc::Rc;

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(source_type) = crate::parsing::source_type(filename) else {
return Reading::default();
};
let Ok(program) = crate::parsing::checked(allocator, code, source_type) else {
return Reading::default();
};
let scoping = Rc::new(
oxc_semantic::SemanticBuilder::new()
.build(&program)
.semantic
.into_scoping(),
);
let names = {
let mut used = Used {
styled: FxHashSet::default(),
css_prop: false,
css_takers: CssTakers::new(&program, &scoping, CssProp::Off, ""),
names: FxHashSet::default(),
scoping: &scoping,
};
used.visit_program(&program);
used.names
};
if names.is_empty() {
return Reading::default();
}
let mut scoping = Some(scoping);
let mut reading = super::read(
&AstBuilder::new(allocator),
&program,
filename,
option,
resolver,
CssProp::Off,
&mut scoping,
);
reading
.components
.retain(|name, component| names.contains(name) && component.definition.is_some());
reading
}
216 changes: 216 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,216 @@
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(),
"./types" => "export type { 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 { Missing } from './base'; export const A = Missing.withComponent('a');")]
#[case("import { Dyn } from './types'; export const A = Dyn.withComponent('a');")]
#[case(
"import './base'; const Dyn = { withComponent: tag => tag }; 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
);
}

#[rstest]
#[case("")]
#[case("import { styled } from '@devup-ui/react';")]
#[serial]
fn literal_namespace_retargets_match_dot_with_and_without_trigger(#[case] trigger: &str) {
let dot = format!(
"{trigger}import * as UI from './base'; export const A = (UI.Dyn).withComponent('a');"
);
reset();
let expected = extract_with_modules("/src/app.tsx", &dot, option(), false, &resolve).unwrap();
let computed = dot.replace("UI.Dyn", "(UI['Dyn'] as typeof UI.Dyn)!");
let option = option();
assert!(has_devup_ui_with(
"/src/app.tsx",
&dot,
&option.package,
&option.import_aliases,
Some(&resolve),
));
assert!(has_devup_ui_with(
"/src/app.tsx",
&computed,
&option.package,
&option.import_aliases,
Some(&resolve),
));
reset();
let actual = extract_with_modules("/src/app.tsx", &computed, option, false, &resolve).unwrap();
// Staged rendering keeps the authored receiver. Normalize only that one
// expression, not imports, forwarding, target selection or render stages.
let receiver = "((UI[\"Dyn\"] as typeof UI.Dyn)!)";
assert_eq!(actual.code.matches(receiver).count(), 1);
assert_eq!(expected.code.matches("(UI.Dyn)").count(), 1);
assert_eq!(actual.code.replace(receiver, "(UI.Dyn)"), expected.code);
assert!(!actual.code.contains(".withComponent("));
assert!(actual.code.contains("import * as UI from \"./base\";"));
assert_eq!(actual.styles, expected.styles);
assert_eq!(actual.dependencies, expected.dependencies);
assert_eq!(actual.dependencies, ["/src/base.tsx"]);
assert!(actual.css_file.is_some());
}

#[rstest]
#[case("import { Dyn } from './base';", "Dyn", "Dyn")]
#[case("import * as UI from './base';", "UI", "UI.Dyn")]
#[case("import * as UI from './base';", "UI", "UI['Dyn']")]
#[serial]
fn admitted_retargets_preserve_shadowed_occurrences(
#[case] import: &str,
#[case] shadow: &str,
#[case] expression: &str,
) {
let code = format!(
"{import} export function untouched({shadow}) {{ return {expression}.withComponent('section'); }} export const A = {expression}.withComponent('a');"
);
let option = option();
assert!(has_devup_ui_with(
"/src/app.tsx",
&code,
&option.package,
&option.import_aliases,
Some(&resolve),
));
reset();
let output = extract_with_modules("/src/app.tsx", &code, option, false, &resolve).unwrap();
assert_eq!(output.code.matches(".withComponent(").count(), 1);
let receiver = expression.replace('\'', "\"");
assert!(output.code.contains(&format!(
"export function untouched({shadow}) {{\n\treturn {receiver}.withComponent(\"section\");\n}}"
)));
assert!(output.code.contains(&import.replace('\'', "\"")));
// The two receivers remain, plus any named import and parameter bindings.
// The admitted call must not copy the origin's dynamic callback.
assert_eq!(
output.code.matches(&receiver).count(),
2 + import.matches(expression).count() + shadow.matches(expression).count()
);
assert!(!output.code.contains("20px"));
assert!(output.css_file.is_some());
assert_eq!(output.dependencies, ["/src/base.tsx"]);
}

#[test]
#[serial]
fn unsupported_retarget_source_types_remain_irrelevant() {
let allocator = oxc_allocator::Allocator::default();
let source = "import { Dyn } from './base'; export const A = Dyn.withComponent('a');";

let reading = crate::imported_styled::relevance::read(
&allocator,
"/src/app.unknown",
source,
&option(),
Some(&resolve),
);

assert_eq!(reading.components.len(), 0);
assert_eq!(reading.dependencies.len(), 0);
}
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 { forwardRef as __devupForwardRef } from \"react\";\nimport { Dyn } from \"./base\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (__devupRecord0=>{const {__devupCss0:__devupRecord1,__devupForwarded0:__devupRecord12,...__devupRecord2}=__devupRecord0;const __devupRecord14=()=>(\"a\");const __devupRecord15=(__devupRecord1&&__devupRecord1[3])??__devupRecord14;const __devupRecord13=__devupRecord1?__devupRecord1[2]:false;const __devupRecord8=(__devupRecord2,__devupRecord7,__devupRecord13)=>{const __devupRecord8=(__devupRecord2);return __devupRecord1?(0,__devupRecord1[0])(__devupRecord8,__devupRecord7,__devupRecord13):__devupRecord7(__devupRecord8);};const __devupRecord9=(__devupRecord3,__devupRecord4)=>{const {style,className,as:DevupAs=__devupRecord4[3],forwardedAs}=__devupRecord3;const rest=__devupRecord4[5];const __devupRecord5=[];const __devupRecord6=[[__devupRecord4[0][0]],__devupRecord4[1],[__devupRecord4[2],\"b--a\",].filter(Boolean).join(' '),__devupRecord4[3],__devupRecord4[4],rest];return __devupRecord1?(0,__devupRecord1[1])(__devupRecord3,__devupRecord6):__devupRecord6[4]({...Object.fromEntries(Object.entries((({ \"big\": __devupOmit0, \"theme\": __devupOmit1, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\")),as:__devupRecord6[3],forwardedAs:__devupRecord3.forwardedAs,className:(__devupRecord6[0].length||__devupRecord6[2]!==void 0)?[...__devupRecord6[0],__devupRecord6[2],__devupRecord3.className].filter(Boolean).join(' '):__devupRecord3.className,style:__devupRecord6[1]===void 0?__devupRecord3.style:{...__devupRecord6[1],...__devupRecord3.style}});};return ((DevupStyled) => (<DevupStyled {...({...__devupRecord2,__devupCss0:[__devupRecord8,__devupRecord9,__devupRecord13,__devupRecord15]})}/>))(Dyn);})({\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 { forwardRef as __devupForwardRef } from \"react\";\nimport * as UI from \"./base\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (__devupRecord0=>{const {__devupCss0:__devupRecord1,__devupForwarded0:__devupRecord12,...__devupRecord2}=__devupRecord0;const __devupRecord14=()=>(\"a\");const __devupRecord15=(__devupRecord1&&__devupRecord1[3])??__devupRecord14;const __devupRecord13=__devupRecord1?__devupRecord1[2]:false;const __devupRecord8=(__devupRecord2,__devupRecord7,__devupRecord13)=>{const __devupRecord8=(__devupRecord2);return __devupRecord1?(0,__devupRecord1[0])(__devupRecord8,__devupRecord7,__devupRecord13):__devupRecord7(__devupRecord8);};const __devupRecord9=(__devupRecord3,__devupRecord4)=>{const {style,className,as:DevupAs=__devupRecord4[3],forwardedAs}=__devupRecord3;const rest=__devupRecord4[5];const __devupRecord5=[];const __devupRecord6=[[__devupRecord4[0][0]],__devupRecord4[1],[__devupRecord4[2],\"b--a\",].filter(Boolean).join(' '),__devupRecord4[3],__devupRecord4[4],rest];return __devupRecord1?(0,__devupRecord1[1])(__devupRecord3,__devupRecord6):__devupRecord6[4]({...Object.fromEntries(Object.entries((({ \"big\": __devupOmit0, \"theme\": __devupOmit1, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\")),as:__devupRecord6[3],forwardedAs:__devupRecord3.forwardedAs,className:(__devupRecord6[0].length||__devupRecord6[2]!==void 0)?[...__devupRecord6[0],__devupRecord6[2],__devupRecord3.className].filter(Boolean).join(' '):__devupRecord3.className,style:__devupRecord6[1]===void 0?__devupRecord3.style:{...__devupRecord6[1],...__devupRecord3.style}});};return ((DevupStyled) => (<DevupStyled {...({...__devupRecord2,__devupCss0:[__devupRecord8,__devupRecord9,__devupRecord13,__devupRecord15]})}/>))(UI.Dyn);})({\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 { forwardRef as __devupForwardRef } from \"react\";\nimport { Dyn } from \"./forward\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (__devupRecord0=>{const {__devupCss0:__devupRecord1,__devupForwarded0:__devupRecord12,...__devupRecord2}=__devupRecord0;const __devupRecord14=()=>(\"a\");const __devupRecord15=(__devupRecord1&&__devupRecord1[3])??__devupRecord14;const __devupRecord13=__devupRecord1?__devupRecord1[2]:false;const __devupRecord8=(__devupRecord2,__devupRecord7,__devupRecord13)=>{const __devupRecord8=(__devupRecord2);return __devupRecord1?(0,__devupRecord1[0])(__devupRecord8,__devupRecord7,__devupRecord13):__devupRecord7(__devupRecord8);};const __devupRecord9=(__devupRecord3,__devupRecord4)=>{const {style,className,as:DevupAs=__devupRecord4[3],forwardedAs}=__devupRecord3;const rest=__devupRecord4[5];const __devupRecord5=[];const __devupRecord6=[[__devupRecord4[0][0]],__devupRecord4[1],[__devupRecord4[2],\"c--a\",].filter(Boolean).join(' '),__devupRecord4[3],__devupRecord4[4],rest];return __devupRecord1?(0,__devupRecord1[1])(__devupRecord3,__devupRecord6):__devupRecord6[4]({...Object.fromEntries(Object.entries((({ \"big\": __devupOmit0, \"theme\": __devupOmit1, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\")),as:__devupRecord6[3],forwardedAs:__devupRecord3.forwardedAs,className:(__devupRecord6[0].length||__devupRecord6[2]!==void 0)?[...__devupRecord6[0],__devupRecord6[2],__devupRecord3.className].filter(Boolean).join(' '):__devupRecord3.className,style:__devupRecord6[1]===void 0?__devupRecord3.style:{...__devupRecord6[1],...__devupRecord3.style}});};return ((DevupStyled) => (<DevupStyled {...({...__devupRecord2,__devupCss0:[__devupRecord8,__devupRecord9,__devupRecord13,__devupRecord15]})}/>))(Dyn);})({\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 { forwardRef as __devupForwardRef } from \"react\";\nimport { Dyn } from \"./base\";\nexport const A = __devupForwardRef((__devupRefProps, __devupRef) => (__devupRecord0=>{const {__devupCss0:__devupRecord1,__devupForwarded0:__devupRecord12,...__devupRecord2}=__devupRecord0;const __devupRecord14=()=>(\"a\");const __devupRecord15=(__devupRecord1&&__devupRecord1[3])??__devupRecord14;const __devupRecord13=__devupRecord1?__devupRecord1[2]:false;const __devupRecord8=(__devupRecord2,__devupRecord7,__devupRecord13)=>{const __devupRecord8=(__devupRecord2);return __devupRecord1?(0,__devupRecord1[0])(__devupRecord8,__devupRecord7,__devupRecord13):__devupRecord7(__devupRecord8);};const __devupRecord9=(__devupRecord3,__devupRecord4)=>{const {style,className,as:DevupAs=__devupRecord4[3],forwardedAs}=__devupRecord3;const rest=__devupRecord4[5];const __devupRecord5=[];const __devupRecord6=[[__devupRecord4[0][0]],__devupRecord4[1],[__devupRecord4[2],\"b--a\",].filter(Boolean).join(' '),__devupRecord4[3],__devupRecord4[4],rest];return __devupRecord1?(0,__devupRecord1[1])(__devupRecord3,__devupRecord6):__devupRecord6[4]({...Object.fromEntries(Object.entries((({ \"big\": __devupOmit0, \"theme\": __devupOmit1, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== \"$\")),as:__devupRecord6[3],forwardedAs:__devupRecord3.forwardedAs,className:(__devupRecord6[0].length||__devupRecord6[2]!==void 0)?[...__devupRecord6[0],__devupRecord6[2],__devupRecord3.className].filter(Boolean).join(' '):__devupRecord3.className,style:__devupRecord6[1]===void 0?__devupRecord3.style:{...__devupRecord6[1],...__devupRecord3.style}});};return ((DevupStyled) => (<DevupStyled {...({...__devupRecord2,__devupCss0:[__devupRecord8,__devupRecord9,__devupRecord13,__devupRecord15]})}/>))(Dyn);})({\n\t...__devupRefProps,\n\tref: __devupRef\n}));\n\n[\"/src/base.tsx\"]"
Loading
Loading