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/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 a5853d5b5..62cb15c38 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'` @@ -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() {