Skip to content

feat: add CSS theme token validation#372

Merged
mohamedmansour merged 1 commit into
mainfrom
mmansour/css-theme-token-validation
Jun 26, 2026
Merged

feat: add CSS theme token validation#372
mohamedmansour merged 1 commit into
mainfrom
mmansour/css-theme-token-validation

Conversation

@mohamedmansour

Copy link
Copy Markdown
Contributor

Summary

This PR makes CSS design-token validation a first-class build/dev-server contract across WebUI. The parser now tracks unresolved var() fallback chains, reports missing required theme tokens as structured diagnostics, and surfaces likely misspellings that are protected by literal CSS fallbacks as non-fatal warnings.

It also makes webui serve isomorphic with webui build for static component assets: --emit-component-assets is now accepted by the dev server, parsed on every rebuild, validated against the configured theme, and served from memory as browser-loadable .webui.js modules.

What changed

Theme-token validation

The parser now understands fallback chains such as:

:host {
  --foo-bar: var(--token-a, var(--token-b, var(--token-c)));
}

Each unresolved candidate is validated independently after local/ancestor definitions are removed. For example, this only requires --token-b and --token-c from the theme because --token-a is defined locally:

:host {
  --token-a: red;
  --foo-bar: var(--token-a, var(--token-b, var(--token-c)));
}

Missing required tokens now fail with structured diagnostics:

✘ build error: missing theme token [missing-theme-token]
--> asset-badge.css:8:16
  padding: var(--spacing-xss) var(--spacing-m);
help: did you mean --spacing-xs? otherwise define it locally

Literal-fallback warnings

A token used only with a literal fallback remains non-fatal because CSS has a valid runtime fallback:

:host {
  color: var(--color-brnad, #0067c0);
}

If the token is absent from every theme, WebUI now emits a warning diagnostic instead of a single-line advisory:

⚠ build warning: unthemed CSS token --color-brnad [unthemed-token]
--> my-card.css:2:10
  color: var(--color-brnad, #0067c0);
help: did you mean --color-brand? otherwise the literal fallback is used

Nested fallback handling now matches CSS semantics more closely:

/* --spacing-m is still required: calc(var(--spacing-m) * 2) is not a safe literal fallback */
margin: var(--gap, calc(var(--spacing-m) * 2));

/* both candidates are optional: the inner var() has a safe literal fallback */
margin: var(--gap, calc(var(--spacing-m, 1px) * 2));

serve --emit-component-assets

serve now accepts the same static component asset roots as build:

webui serve ./src \
  --state ./data/state.json \
  --plugin=webui \
  --theme=@microsoft/webui-examples-theme \
  --emit-component-assets lazy-panel \
  --servedir ./dist \
  --watch

This means lazily loaded components that are not present in the SSR entry tree are still parsed and validated during development:

/* lazy-panel.css */
.panel {
  background: var(--color-neutral-1000);
}
✘ build error: missing theme token [missing-theme-token]
--> lazy-panel.css:11:19
  background: var(--color-neutral-1000);
help: did you mean --color-neutral-100? otherwise define it locally

The generated component asset is served from memory with a JavaScript MIME type so the WebUI Framework loader can import it directly:

const assets = defineComponentAssets({
  'lazy-panel': {
    asset: './lazy-panel.webui.js',
    module: () => import('../lazy-panel/lazy-panel.js'),
  },
});

Dev-server reliability and UX

The dev server now:

  • persists rebuild errors so refreshes show the current failure instead of stale HTML;
  • broadcasts plain rebuild errors to the browser while keeping terminal output styled;
  • prints rebuild trigger filenames, e.g. ↻ rebuilt lazy-panel.css in 3ms;
  • suppresses byte-identical no-op saves in the clean state for speed;
  • allows no-op saves to retry while a rebuild error is active;
  • deduplicates repeated warning diagnostics across unrelated rebuilds;
  • frames errors and warnings with blank lines for readable terminal output.

Example warning/error shape under cargo xtask dev:

[server]   ↻ rebuilt asset-badge.css in 3ms 15:38:09
[server]
[server]   ⚠ build warning: unthemed CSS token --spacing-xss [unthemed-token]
[server]   --> asset-badge.css:8:16
[server]     padding: var(--spacing-xss, 4px) var(--spacing-m);
[server]   help: did you mean --spacing-xs? otherwise the literal fallback is used
[server]
[server]   ✘ build error: missing theme token [missing-theme-token]
[server]   --> asset-badge.css:7:22
[server]     border-radius: var(--borders-radius-pill);
[server]   help: did you mean --border-radius-pill? otherwise define it locally

API and docs

  • BuildResult.warnings is now a Vec<Diagnostic> in Rust.
  • Node exposes warnings as string[] and the TypeScript BuildResult type now includes warnings.
  • webui-tokens no longer exposes a no-op TokenWarning API; token resolution returns ResolvedTokens { css }.
  • CLI theme package resolution now searches from the canonical app directory, matching Node behavior.
  • Updated DESIGN.md, CLI docs, CSS token docs, integration docs, AI docs, and package README examples.

Validation

cargo xtask check

Passed locally:

✔ license-headers
✔ fmt
✔ clippy
✔ proto (drift check)
✔ deny
✔ test
✔ build
✔ build (wasm)
✔ build (examples)
✔ bench (validate)
✔ docs

@mohamedmansour mohamedmansour merged commit 6ffc7ee into main Jun 26, 2026
21 checks passed
@mohamedmansour mohamedmansour deleted the mmansour/css-theme-token-validation branch June 26, 2026 17:10
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.

2 participants