From ac3555d54bf7e20bb35e566f4d84b681bb97c281 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Mon, 14 Sep 2026 21:17:50 +0900 Subject: [PATCH] docs(skill): say what decides static extraction, and never author a CSS file Three sections already told parts of one rule without ever stating it - `Dynamic Values = CSS Variables`, `$token Scope` and `Inline Variant Pattern` - and none of them said why. An agent that only has the examples pattern matches; one that has the rule generalises to the cases the examples do not cover. The rule, verified against `extract_style_from_member_expression.rs`: an inline `ObjectExpression` or `ArrayExpression` indexed at a style prop extracts to static classes, while an `Identifier` - an object declared elsewhere and reached by name - takes the `dynamic_style` path and becomes a CSS variable. This is the answer given in #663, with the reason attached: a value reached through a variable may have been mutated before it runs, and TypeScript's types are not a runtime guarantee. Adds the detection signal too, from the same issue. Inline CSS variables on the rendered element are how you tell which path you got, and nothing here previously said so. Adds the `css()` exception, which resolves a contradiction the rule creates without it: an external object of `css()` results is fine, because `css()` already extracted at its own call site and `className` is never a style-extraction source. Without stating that, "no external style objects" reads as forbidding the standard variant-map pattern. Adds a section saying not to author `.css`/`.scss` at all. The anti-pattern table forbade `style={{}}`, `styled()` and `stylex.create()`, so writing a stylesheet and importing it was never on the list - and a hand-written stylesheet is invisible to extraction, competing silently with the generated classes. `@devup-ui/reset-css` had no mention anywhere despite needing `include` plus two Vite resolver settings to emit anything, which had to be reverse-engineered out of `node_modules`. Finally, notes that the Vite plugin does not mean the project is a Vite SPA. `vinext` runs Next App Router on Vite, so an App Router project has a `vite.config.ts`, no `next.config.ts`, and uses this plugin - and the Vite section being first made that misreading easy to confirm. --- SKILL.md | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/SKILL.md b/SKILL.md index 7f3c80051..13803ac28 100644 --- a/SKILL.md +++ b/SKILL.md @@ -407,6 +407,17 @@ import DevupUI from "@devup-ui/vite-plugin"; export default defineConfig({ plugins: [react(), DevupUI()] }); ``` +> **The Vite plugin does not mean the project is a Vite SPA.** `vinext` runs +> Next.js App Router *on* Vite, so an App Router project has a `vite.config.ts`, +> no `next.config.ts`, and uses this plugin. Decide from `package.json`: a +> `vinext` or `next` dependency means file routing under `src/app/`, and there +> is no `main.tsx` or `index.html` to create. + +```ts +// vinext project - both plugins belong here +plugins: [DevupUI(), vinext({ nextConfig: { output: "export" } })]; +``` + ### Next.js ```ts @@ -451,6 +462,77 @@ DevupUI({ }) ``` +## Never Author a CSS File + +Devup UI extracts styling at build time. A hand-written stylesheet is invisible +to it: it cannot be checked, themed, or ordered in the cascade, and it competes +silently with the classes the plugin generated. + +| Need | Use | +|------|-----| +| Reset / normalize | `resetCss()` from `@devup-ui/reset-css` | +| Document-level rules (`body`, `*`, `@font-face`) | `globalCss({ ... })` | +| Component styling | Style props, or `css({ ... })` | +| A genuinely runtime value | A style prop - the plugin emits a CSS variable | + +The only acceptable CSS import is a stylesheet **shipped by an installed +package you do not author**, such as an offline webfont package. + +### `@devup-ui/reset-css` + +It is a package, so the plugin has to be told to process it or its classes are +never emitted. With Vite the two resolver settings are needed as well: + +```ts +plugins: [DevupUI({ include: ["@devup-ui/reset-css"] })], +optimizeDeps: { exclude: ["@devup-ui/reset-css"] }, +ssr: { noExternal: ["@devup-ui/reset-css"] }, +``` + +## What Decides Static Extraction + +One rule explains `Dynamic Values = CSS Variables`, `$token Scope` and +`Inline Variant Pattern` below: + +> Devup UI extracts at build time only what it can prove is constant **at the +> JSX prop site**. A value reached through a variable is treated as possibly +> mutated at runtime - TypeScript's types are not a runtime guarantee - so it +> falls back to a CSS variable. + +| Form | Result | +|------|--------| +| `` | Static class | +| `` | Static class per value - **preferred** | +| `` where `colors` is declared elsewhere | CSS variable | +| `` | CSS variable (genuinely dynamic - correct) | +| `const s = { a: css({ ... }) }` then `className={s[v]}` | Neither - see below | + +**How to tell which one you got:** inspect the rendered element. Inline CSS +variables mean the values were not extracted. + +```html +
+
+``` + +### The `css()` exception + +An external object holding **`css()` results** is not the same thing and is +fine. `css()` runs at build time and returns a className string, so extraction +already happened at the `css()` call; the object carries strings and no style +prop is involved. + +```tsx +// FINE - extraction happened inside css(); this object holds classNames +const variantStyles = { + primary: css({ bg: "$primary", color: "#FFF" }), + secondary: css({ bg: "$gray100", color: "$text" }), +}; +; +``` + +The rule is about **style prop values**, not about objects. + ## $token Scope `$token` values (colors, length, shadow) only work in **JSX props**. Use `var(--token)` in external objects. @@ -485,8 +567,16 @@ Use inline object indexing instead of external config objects: // AVOID - external config object (becomes dynamic, uses CSS variables) const sizeStyles = { lg: { h: '48px' }, md: { h: '40px' } } + +// AVOID - the flat form is the same trap. The compiler cannot prove `colors` +// was not mutated before this runs, so every value becomes a CSS variable. +const colors = { red: '#f00', blue: '#00f' } + ``` +Why, and how to detect it: see [What Decides Static Extraction](#what-decides-static-extraction). +An external object of `css()` results is a different thing and is fine. + ## Anti-Patterns (NEVER do) | Wrong | Right | Why | @@ -503,3 +593,6 @@ const sizeStyles = { lg: { h: '48px' }, md: { h: '40px' } } | `width="100%"` | `w="100%"` | Always use shorthands | | `styled("div", {...})` | `` | Use Box component with props, not styled() | | `stylex.create({...})` | `` | Use Box component with props, not stylex | +| Authoring `styles.css` / `styles.scss` | `globalCss({...})` or style props | A hand-written stylesheet is invisible to extraction | +| `import "./styles.css"` | `resetCss()` / `globalCss()` | Only a stylesheet shipped by an installed package is acceptable | +| `` (external object) | `` | Reached through a variable, so it becomes a CSS variable |