Skip to content

docs(skill): say what decides static extraction, and never author a CSS file - #665

Merged
owjs3901 merged 2 commits into
mainfrom
docs/skill-structure-and-extraction-rules
Sep 15, 2026
Merged

owjs3901 merged 2 commits into
mainfrom
docs/skill-structure-and-extraction-rules

Conversation

@owjs3901

Copy link
Copy Markdown
Contributor

Closes #663.

Why

Three sections of SKILL.md 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.

#663 asked exactly that question, and the answer there is the missing rule.

The rule, verified against the extractor

Not assumed — read out of libs/extractor/src/extractor/extract_style_from_member_expression.rs:

mem.object line result
ArrayExpression (inline) 46 static extraction
ObjectExpression (inline) 145 static extraction
Identifier (declared elsewhere, reached by name) 218–235 → dynamic_style() CSS variable

Stated in the skill as:

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.

What this adds

The detection signal, from the issue body. Nothing previously told a reader how to check which path they got:

<!-- extracted -->      <div class="m n o p">
<!-- not extracted -->  <div class="m n o p" style="--q:red;--s:12px">

The flat external-object case. The existing AVOID example was sizeStyles[size].h — nested, with a property access. The issue's actual code was colors[color] — flat. Same trap, different shape.

The css() exception, which resolves a contradiction the rule creates without it. An external object of css() results is fine: css() already extracted at its own call site, and className is never a style-extraction source — it appears nowhere in extract_style_from_jsx.rs, and prop_modify_utils.rs only merges it ([className, "class-name"].filter(Boolean).join()). Without saying so, "no external style objects" reads as forbidding the standard variant-map pattern.

Never author a CSS file. The anti-pattern table forbade style={{}}, styled() and stylex.create() — so writing a stylesheet and importing it was simply never on the list. A hand-written stylesheet is invisible to extraction and competes silently with the generated classes.

@devup-ui/reset-css, which had zero mentions anywhere despite needing include plus two Vite resolver settings before it emits anything. That combination currently has to be reverse-engineered out of node_modules:

plugins: [DevupUI({ include: ["@devup-ui/reset-css"] })],
optimizeDeps: { exclude: ["@devup-ui/reset-css"] },
ssr: { noExternal: ["@devup-ui/reset-css"] },

A vinext note on the Vite section. 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. The Vite section being listed first made that misreading easy to confirm, and it has already produced a hand-rolled SPA in a real App Router project.

Scope

Docs only — SKILL.md, +93 lines. .changepacks/config.json tracks packages/*/*, so no changepack applies.

…SS 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.
@owjs3901

Copy link
Copy Markdown
Contributor Author

CI note: the three failing jobs (publish, landing-next-e2e, benchmark) all fail at the same step, and it is not this change.

@devup-ui/react build: error: Generated CSS properties are stale. Run bun run generate:css-types.
packages/react/scripts/generate-css-properties.ts:58
error: script "test:types" exited with code 1

This branch differs from main by exactly one file:

$ git diff --stat origin/main...HEAD
 SKILL.md | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 93 insertions(+)

A markdown file cannot make generated CSS property types stale. The generated file is behind whatever csstype a fresh CI install now resolves to, which is a condition of main rather than of this branch.

I have deliberately not regenerated it here — that is a build fix and does not belong inside a docs-only PR — but it will block anything else opened against main until someone runs bun run generate:css-types and commits the result.

@github-actions

Copy link
Copy Markdown
Contributor

Changepacks

@devup-ui/components@0.1.55 → 0.1.56 - packages/components/package.json

Patch

  • Auto-update: depends on '@devup-ui/react' via a local workspace dependency

@devup-ui/react@1.0.41 → 1.0.42 - packages/react/package.json

Patch

  • Use csstype-extra properties directly to restore missing timeline trigger props and remove stale CSS type generation checks

@devup-ui/reset-css@1.0.28 → 1.0.29 - packages/reset-css/package.json

Patch

  • Auto-update: depends on '@devup-ui/react' via a local workspace dependency

@codecov

codecov Bot commented Sep 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@owjs3901
owjs3901 merged commit 3b1c3bf into main Sep 15, 2026
4 checks passed
@owjs3901
owjs3901 deleted the docs/skill-structure-and-extraction-rules branch September 15, 2026 01:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Is there a recommended way to write variants?

1 participant