diff --git a/.changepacks/changepack_log_stylex_unsupported_api.json b/.changepacks/changepack_log_stylex_unsupported_api.json new file mode 100644 index 000000000..8a2a7d0e9 --- /dev/null +++ b/.changepacks/changepack_log_stylex_unsupported_api.json @@ -0,0 +1,8 @@ +{ + "changes": { + "bindings/devup-ui-wasm/package.json": "Patch", + "bindings/devup-ui-binding/package.json": "Patch" + }, + "note": "A StyleX API Devup UI does not compile is a located build error naming it, instead of being dropped from the compiled set while its calls stay StyleX calls that throw at runtime. New build error: an export of `@stylexjs/stylex` outside `create`, `props`, `attrs`, `keyframes`, `defineVars`, `createTheme`, `createThemeContract`, `defineConsts`, `positionTry`, `viewTransitionClass`, `firstThatWorks`, `include` and `types` (such as `when`, `env` or `defaultMarker`), imported by name or read as a member of the package imported whole", + "date": "2026-11-15T00:00:00.000Z" +} diff --git a/apps/landing/src/app/(detail)/docs/migration/stylex/page.mdx b/apps/landing/src/app/(detail)/docs/migration/stylex/page.mdx index c454cf3fa..dda8454c7 100644 --- a/apps/landing/src/app/(detail)/docs/migration/stylex/page.mdx +++ b/apps/landing/src/app/(detail)/docs/migration/stylex/page.mdx @@ -155,3 +155,7 @@ const transition = stylex.viewTransitionClass({ animationDuration: '300ms' }) ## Value helpers `firstThatWorks()` emits its fallbacks in CSS order, `types.*()` unwraps to the inner value, and `include()` merges another namespace's classes. Values may be plain literals, condition objects (`{ default, ':hover', '@media ...' }`), or arrow functions, which become CSS variables bound at the call site. + +## Unsupported APIs + +Devup UI compiles `create`, `props`, `attrs`, `keyframes`, `defineVars`, `createTheme`, `createThemeContract`, `defineConsts`, `positionTry`, `viewTransitionClass`, `firstThatWorks`, `include` and `types`. Any other export of `@stylexjs/stylex` (`when`, `env`, `defaultMarker`, ...) would stay a StyleX call that throws at runtime, so importing it or reading it from the package is a build error naming it. diff --git a/libs/extractor/src/lib.rs b/libs/extractor/src/lib.rs index 310075c7f..1fb704f8b 100644 --- a/libs/extractor/src/lib.rs +++ b/libs/extractor/src/lib.rs @@ -17902,6 +17902,30 @@ const styles = create({ base: { color: 'green' } });", )); } + // A StyleX API Devup UI does not compile would stay a StyleX call, which + // throws at runtime, so it is a located error naming it + #[test] + #[serial] + fn test_stylex_unsupported_api_is_located_error() { + let errors: Vec<(&str, Option)> = [ + "import { create, when, type StyleXStyles } from '@stylexjs/stylex';\nexport const a = when.ancestor(':hover');", + "import { env as e } from '@stylexjs/stylex';\nexport const a = e.tokens;", + "import stylex from '@stylexjs/stylex';\nexport const a = stylex.when.ancestor(':hover');\nexport const b = stylex.env;\nexport const c = stylex.defaultMarker();", + "import * as sx from '@stylexjs/stylex';\nconst styles = sx.create({ a: { color: { default: 'red', [sx.when.ancestor(':hover')]: 'blue' } } });\nexport const p = sx.props(styles.a);", + ] + .into_iter() + .map(|code| { + reset_class_map(); + reset_file_map(); + let error = extract("test.tsx", code, ExtractOption::default()) + .err() + .map(|error| error.to_string()); + (code, error) + }) + .collect(); + assert_debug_snapshot!(errors); + } + #[test] #[serial] fn test_stylex_create_single_namespace_static() { @@ -19626,21 +19650,20 @@ const composed = stylex.create({ fancy: { ...stylex.include(base.dynamic), backg fn test_stylex_import_unknown_named() { reset_class_map(); reset_file_map(); - assert_debug_snapshot!(ToBTreeSet::from( - extract( - "test.tsx", - r"import { create, unknownFunction } from '@stylexjs/stylex'; + // It stayed imported from `@stylexjs/stylex`, which then ran at runtime + let error = extract( + "test.tsx", + r"import { create, unknownFunction } from '@stylexjs/stylex'; const styles = create({ base: { color: 'red' } });", - ExtractOption { - package: "@devup-ui/react".to_string(), - css_dir: "@devup-ui/react".to_string(), - single_css: true, - import_main_css: false, - import_aliases: HashMap::new() - }, - ) - .unwrap() - )); + ExtractOption::default(), + ) + .unwrap_err() + .to_string(); + assert!( + error + .starts_with("test.tsx:1:18: `@stylexjs/stylex` `unknownFunction` is not compiled"), + "{error}" + ); } // ========================================== diff --git a/libs/extractor/src/scope/lookup.rs b/libs/extractor/src/scope/lookup.rs index 4449138dc..072834778 100644 --- a/libs/extractor/src/scope/lookup.rs +++ b/libs/extractor/src/scope/lookup.rs @@ -213,6 +213,12 @@ impl Bindings { } } + /// The member of the `StyleX` package imported whole that `expression` + /// reads, as `when` in `stylex.when` + pub fn stylex_member<'e>(&self, expression: &'e Expression<'_>) -> Option<&'e str> { + self.member_of(&self.stylex_namespaces, expression) + } + /// The bindings the names `object` reads all stand for, by name pub fn visible(&self, object: &ObjectExpression<'_>) -> FxHashMap { let mut reads = visits::Names::new(self); diff --git a/libs/extractor/src/snapshots/extractor__tests__stylex_import_unknown_named.snap b/libs/extractor/src/snapshots/extractor__tests__stylex_import_unknown_named.snap deleted file mode 100644 index 3bee8cc3b..000000000 --- a/libs/extractor/src/snapshots/extractor__tests__stylex_import_unknown_named.snap +++ /dev/null @@ -1,19 +0,0 @@ ---- -source: libs/extractor/src/lib.rs -expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import { create, unknownFunction } from '@stylexjs/stylex';\nconst styles = create({ base: { color: 'red' } });\"#,\nExtractOption\n{\n package: \"@devup-ui/react\".to_string(), css_dir:\n \"@devup-ui/react\".to_string(), single_css: true, import_main_css: false,\n import_aliases: HashMap::new()\n},).unwrap())" ---- -ToBTreeSet { - styles: { - Static( - ExtractStaticStyle { - property: "color", - value: "red", - level: 0, - selector: None, - style_order: None, - layer: None, - }, - ), - }, - code: "import \"@devup-ui/react/devup-ui.css\";\nimport { create, unknownFunction } from \"@stylexjs/stylex\";\nconst styles = { \"base\": \"a\" };\n", -} diff --git a/libs/extractor/src/snapshots/extractor__tests__stylex_unsupported_api_is_located_error.snap b/libs/extractor/src/snapshots/extractor__tests__stylex_unsupported_api_is_located_error.snap new file mode 100644 index 000000000..0baa6f8b9 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__stylex_unsupported_api_is_located_error.snap @@ -0,0 +1,30 @@ +--- +source: libs/extractor/src/lib.rs +expression: errors +--- +[ + ( + "import { create, when, type StyleXStyles } from '@stylexjs/stylex';\nexport const a = when.ancestor(':hover');", + Some( + "test.tsx:1:18: `@stylexjs/stylex` `when` is not compiled by Devup UI, so it would stay a StyleX call at runtime, which throws with no StyleX compiler run. Fix: use only `create`, `props`, `attrs`, `keyframes`, `defineVars`, `createTheme`, `createThemeContract`, `defineConsts`, `positionTry`, `viewTransitionClass`, `firstThatWorks`, `include`, `types` from `@stylexjs/stylex`", + ), + ), + ( + "import { env as e } from '@stylexjs/stylex';\nexport const a = e.tokens;", + Some( + "test.tsx:1:10: `@stylexjs/stylex` `env` is not compiled by Devup UI, so it would stay a StyleX call at runtime, which throws with no StyleX compiler run. Fix: use only `create`, `props`, `attrs`, `keyframes`, `defineVars`, `createTheme`, `createThemeContract`, `defineConsts`, `positionTry`, `viewTransitionClass`, `firstThatWorks`, `include`, `types` from `@stylexjs/stylex`", + ), + ), + ( + "import stylex from '@stylexjs/stylex';\nexport const a = stylex.when.ancestor(':hover');\nexport const b = stylex.env;\nexport const c = stylex.defaultMarker();", + Some( + "test.tsx:2:18: `@stylexjs/stylex` `when` is not compiled by Devup UI, so it would stay a StyleX call at runtime, which throws with no StyleX compiler run. Fix: use only `create`, `props`, `attrs`, `keyframes`, `defineVars`, `createTheme`, `createThemeContract`, `defineConsts`, `positionTry`, `viewTransitionClass`, `firstThatWorks`, `include`, `types` from `@stylexjs/stylex`\ntest.tsx:3:18: `@stylexjs/stylex` `env` is not compiled by Devup UI, so it would stay a StyleX call at runtime, which throws with no StyleX compiler run. Fix: use only `create`, `props`, `attrs`, `keyframes`, `defineVars`, `createTheme`, `createThemeContract`, `defineConsts`, `positionTry`, `viewTransitionClass`, `firstThatWorks`, `include`, `types` from `@stylexjs/stylex`\ntest.tsx:4:18: `@stylexjs/stylex` `defaultMarker` is not compiled by Devup UI, so it would stay a StyleX call at runtime, which throws with no StyleX compiler run. Fix: use only `create`, `props`, `attrs`, `keyframes`, `defineVars`, `createTheme`, `createThemeContract`, `defineConsts`, `positionTry`, `viewTransitionClass`, `firstThatWorks`, `include`, `types` from `@stylexjs/stylex`", + ), + ), + ( + "import * as sx from '@stylexjs/stylex';\nconst styles = sx.create({ a: { color: { default: 'red', [sx.when.ancestor(':hover')]: 'blue' } } });\nexport const p = sx.props(styles.a);", + Some( + "test.tsx:2:59: `@stylexjs/stylex` `when` is not compiled by Devup UI, so it would stay a StyleX call at runtime, which throws with no StyleX compiler run. Fix: use only `create`, `props`, `attrs`, `keyframes`, `defineVars`, `createTheme`, `createThemeContract`, `defineConsts`, `positionTry`, `viewTransitionClass`, `firstThatWorks`, `include`, `types` from `@stylexjs/stylex`\ntest.tsx:2:59: `stylex.create()` cannot use `[sx.when.ancestor(\":hover\")]` at build time: its keys must be known", + ), + ), +] diff --git a/libs/extractor/src/stylex.rs b/libs/extractor/src/stylex.rs index 0d3d7f7d1..2bd428112 100644 --- a/libs/extractor/src/stylex.rs +++ b/libs/extractor/src/stylex.rs @@ -85,6 +85,21 @@ impl StylexFunction { } } +/// `name`, an export of `@stylexjs/stylex` Devup UI does not compile, as +/// `when` or `env`: it would stay a `StyleX` call at runtime, which throws +/// with no `StyleX` compiler run +#[must_use] +pub fn unsupported_stylex_error(name: &str) -> String { + let supported: Vec = STYLEX_EXPORTS + .iter() + .map(|(name, _)| format!("`{name}`")) + .collect(); + format!( + "`@stylexjs/stylex` `{name}` is not compiled by Devup UI, so it would stay a StyleX call at runtime, which throws with no StyleX compiler run. Fix: use only {} from `@stylexjs/stylex`", + supported.join(", ") + ) +} + /// The custom property `stylex.defineVars()` in `filename` declares for `key`; /// a module importing it computes the same name #[must_use] diff --git a/libs/extractor/src/visit.rs b/libs/extractor/src/visit.rs index 3bc78081f..e218428cf 100644 --- a/libs/extractor/src/visit.rs +++ b/libs/extractor/src/visit.rs @@ -59,7 +59,8 @@ use crate::scope::{Bindings, NAMESPACE_STYLED}; use crate::style_collector::StyleCollector; use crate::stylex::{ StylexDynamicInfo, StylexFunction, StylexNamespaceValue, create_theme_class, - css_variable_block, css_variable_rules, define_vars_variable, variable_values, + css_variable_block, css_variable_rules, define_vars_variable, unsupported_stylex_error, + variable_values, }; use crate::util_type::UtilType; use crate::{ExtractStyleProp, ExtractStyleValue}; @@ -3363,6 +3364,12 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { if self.compile_own_rules(it) { return; } + if let Some(name) = self.bindings.stylex_member(it) + && StylexFunction::from_export_name(name).is_none() + { + self.errors + .push((it.span().start, unsupported_stylex_error(name))); + } if let Expression::CallExpression(call) = &*it && self.is_css(&call.callee) { @@ -5143,6 +5150,11 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { if let Some(func) = StylexFunction::from_export_name(&imported) { self.bindings .stylex_import(named_spec.local.symbol_id.get(), func); + } else { + self.errors.push(( + named_spec.span.start, + unsupported_stylex_error(&imported), + )); } } ImportSpecifier(_) => {} @@ -5785,6 +5797,15 @@ mod tests { ); assert!(code.contains("export const animation = \""), "{code}"); assert!(code.contains("import value from \"other\";"), "{code}"); + // `unknown` would stay a StyleX call at runtime + assert_eq!(visitor.errors.len(), 1, "{:?}", visitor.errors); + assert!( + visitor.errors[0] + .1 + .starts_with("`@stylexjs/stylex` `unknown` is not compiled"), + "{:?}", + visitor.errors + ); } #[test]