From 7866e4f314857d06378714d4dc659d6e53fff14f Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Fri, 2 Oct 2026 14:02:33 +0900 Subject: [PATCH] fix(extractor): report vanilla-extract recipes and sprinkles imports under the css alias Refs #690 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- ...gepack_log_vanilla_extract_companions.json | 7 ++ bindings/devup-ui-wasm/src/lib.rs | 8 +-- libs/css/src/theme_tokens.rs | 2 +- libs/extractor/src/import_alias_visit.rs | 65 +++++++++++++++++++ libs/extractor/src/lib.rs | 42 ++++++++++-- libs/extractor/src/tailwind.rs | 4 +- 6 files changed, 117 insertions(+), 11 deletions(-) create mode 100644 .changepacks/changepack_log_vanilla_extract_companions.json diff --git a/.changepacks/changepack_log_vanilla_extract_companions.json b/.changepacks/changepack_log_vanilla_extract_companions.json new file mode 100644 index 000000000..4031f66aa --- /dev/null +++ b/.changepacks/changepack_log_vanilla_extract_companions.json @@ -0,0 +1,7 @@ +{ + "changes": { + "bindings/devup-ui-wasm/package.json": "Patch" + }, + "note": "While @vanilla-extract/css is aliased, importing @vanilla-extract/recipes or @vanilla-extract/sprinkles is a build error naming the file, line and column: those packages generate CSS Devup UI does not compile, so their rules used to be silently missing from the stylesheet. The error says to write the styles with css(), or to disable the alias and build with the vanilla-extract plugin. Type-only imports are allowed", + "date": "2026-10-01T00:00:00.000Z" +} diff --git a/bindings/devup-ui-wasm/src/lib.rs b/bindings/devup-ui-wasm/src/lib.rs index 4560410f3..0a2347ac2 100644 --- a/bindings/devup-ui-wasm/src/lib.rs +++ b/bindings/devup-ui-wasm/src/lib.rs @@ -1551,7 +1551,7 @@ mod tests { ); // Test getters - assert!(!output.code().is_empty()); + assert_ne!(output.code(), ""); assert_eq!(output.css_file(), Some("devup-ui-0.css".to_string())); assert_eq!(output.map(), Some("//# sourceMappingURL=test".to_string())); assert!(output.css().is_some()); @@ -1951,7 +1951,7 @@ mod tests { assert!(result.is_ok()); let output = result.unwrap(); - assert!(!output.code().is_empty()); + assert_ne!(output.code(), ""); assert!(output.map().is_some()); } @@ -1975,7 +1975,7 @@ mod tests { assert!(result.is_ok()); let output = result.unwrap(); - assert!(!output.code().is_empty()); + assert_ne!(output.code(), ""); assert!(output.map().is_none()); } @@ -2000,7 +2000,7 @@ mod tests { assert!(result.is_err()); if let Err(error) = result { - assert!(!error.is_empty()); + assert_ne!(error, ""); } } diff --git a/libs/css/src/theme_tokens.rs b/libs/css/src/theme_tokens.rs index d8d8fb021..3b2346aed 100644 --- a/libs/css/src/theme_tokens.rs +++ b/libs/css/src/theme_tokens.rs @@ -119,7 +119,7 @@ mod tests { set_typography_keys(vec!["body".to_string(), "title".to_string()]); assert_eq!(get_typography_keys(), vec!["body", "title"]); set_typography_keys(vec![]); - assert!(get_typography_keys().is_empty()); + assert_eq!(get_typography_keys(), Vec::::new()); } #[test] diff --git a/libs/extractor/src/import_alias_visit.rs b/libs/extractor/src/import_alias_visit.rs index 158d0f420..b0deb91c2 100644 --- a/libs/extractor/src/import_alias_visit.rs +++ b/libs/extractor/src/import_alias_visit.rs @@ -229,6 +229,45 @@ pub fn transform_import_aliases<'a>( transform_import_aliases_with_edits(code, filename, package, import_aliases).0 } +/// Packages generating CSS on top of vanilla-extract's, which Devup UI does not +/// compile +const VANILLA_EXTRACT_COMPANIONS: [&str; 2] = + ["@vanilla-extract/recipes", "@vanilla-extract/sprinkles"]; + +/// The value imports of vanilla-extract's companion packages, as errors at +/// their offsets: while `@vanilla-extract/css` compiles to Devup UI, the +/// rules they generate would never reach the stylesheet +pub fn companion_errors(code: &str, filename: &str) -> Vec<(u32, String)> { + if !VANILLA_EXTRACT_COMPANIONS + .iter() + .any(|package| code.contains(package)) + { + return Vec::new(); + } + let allocator = Allocator::default(); + let source_type = SourceType::from_path(filename).unwrap_or_default(); + let program = Parser::new(&allocator, code, source_type).parse().program; + program + .body + .iter() + .filter_map(|statement| match statement { + Statement::ImportDeclaration(import) + if !import.import_kind.is_type() + && VANILLA_EXTRACT_COMPANIONS.contains(&import.source.value.as_str()) => + { + Some(( + import.span.start, + format!( + "`{}` generates CSS that Devup UI does not compile, so with the `@vanilla-extract/css` alias its rules would never reach the stylesheet. Fix: write the styles with `css()`, or set `importAliases: {{ '@vanilla-extract/css': false }}` and build with the vanilla-extract plugin", + import.source.value + ), + )) + } + _ => None, + }) + .collect() +} + /// A replacement of `code[start..end]` by text of `length` bytes pub type Edit = (usize, usize, usize); @@ -728,6 +767,32 @@ mod tests { )); } + #[test] + fn test_vanilla_extract_companions_are_reported() { + let errors = companion_errors( + "import type { RecipeVariants } from '@vanilla-extract/recipes' +import { recipe } from '@vanilla-extract/recipes' +import { style } from '@vanilla-extract/css' +import { createSprinkles } from '@vanilla-extract/sprinkles'", + "a.css.ts", + ); + assert_eq!(errors.len(), 2, "{errors:?}"); + assert!( + errors[0] + .1 + .starts_with("`@vanilla-extract/recipes` generates CSS") + ); + assert!( + errors[1] + .1 + .starts_with("`@vanilla-extract/sprinkles` generates CSS") + ); + assert_eq!( + companion_errors("import { style } from '@vanilla-extract/css'", "a.css.ts"), + vec![] + ); + } + #[test] fn test_no_matching_alias() { assert_snapshot!(transform_import_aliases( diff --git a/libs/extractor/src/lib.rs b/libs/extractor/src/lib.rs index 68981f36c..301a032f8 100644 --- a/libs/extractor/src/lib.rs +++ b/libs/extractor/src/lib.rs @@ -294,6 +294,12 @@ fn extract_source( source_map: bool, resolver: Option<&ModuleResolver>, ) -> Result> { + if evaluated.is_none() && option.import_aliases.contains_key("@vanilla-extract/css") { + let errors = import_alias_visit::companion_errors(code, filename); + if !errors.is_empty() { + return Err(located_errors(filename, code, &[], errors).into()); + } + } // Step 1: Transform import aliases // e.g., `import styled from '@emotion/styled'` → `import { styled } from '@devup-ui/react'` // e.g., `import { style } from '@vanilla-extract/css'` → `import { style } from '@devup-ui/react'` @@ -836,8 +842,8 @@ mod tests { alternate: None, }; - assert!(empty.extract().is_empty()); - assert!(empty.into_extract().is_empty()); + assert_eq!(empty.extract(), vec![]); + assert_eq!(empty.into_extract(), vec![]); } #[test] @@ -11201,6 +11207,34 @@ let color = "red"; )); } + #[test] + #[serial] + fn test_vanilla_extract_companion_import_is_an_error() { + let option = |aliased: bool| ExtractOption { + package: "@devup-ui/react".to_string(), + css_dir: "@devup-ui/react".to_string(), + single_css: true, + import_main_css: false, + import_aliases: if aliased { + HashMap::from([( + "@vanilla-extract/css".to_string(), + ImportAlias::NamedToNamed, + )]) + } else { + HashMap::new() + }, + }; + let code = "import { recipe } from '@vanilla-extract/recipes'\nexport const b = recipe({})"; + let error = extract("a.css.ts", code, option(true)) + .unwrap_err() + .to_string(); + assert!( + error.starts_with("a.css.ts:1:1: `@vanilla-extract/recipes`"), + "{error}" + ); + assert!(extract("a.css.ts", code, option(false)).is_ok()); + } + #[test] #[serial] fn test_keyframes_no_args() { @@ -13598,7 +13632,7 @@ globalCss({ ); assert!(result.is_ok()); let output = result.unwrap(); - assert!(!output.code.is_empty()); + assert_ne!(output.code, ""); } #[test] @@ -18778,7 +18812,7 @@ export const k = styled('div')({ color: SIZE });", &memory_resolver(CONSTANT_MODULES), ) .unwrap(); - assert!(without_imports.dependencies.is_empty()); + assert_eq!(without_imports.dependencies.len(), 0); let without_constants = extract_with_modules( "/src/Handler.tsx", "import { Box } from '@devup-ui/react';\nimport { handler } from './handler';\nexport const a = ;", diff --git a/libs/extractor/src/tailwind.rs b/libs/extractor/src/tailwind.rs index 87e9f5178..70643aa7c 100644 --- a/libs/extractor/src/tailwind.rs +++ b/libs/extractor/src/tailwind.rs @@ -284,7 +284,7 @@ pub struct TailwindClass { /// non-overlapping) but mutates the existing buffer instead of allocating a new /// `String`. `needle` must be non-empty. fn remove_all_substr(haystack: &mut String, needle: &str) { - debug_assert!(!needle.is_empty()); + debug_assert_ne!(needle, ""); let mut search_from = 0; while let Some(rel) = haystack[search_from..].find(needle) { let at = search_from + rel; @@ -3934,7 +3934,7 @@ mod tests { #[test] fn test_empty_string() { let styles = parse_tailwind_to_styles(""); - assert!(styles.is_empty()); + assert_eq!(styles, vec![]); } #[test]