Skip to content

feat(plugin-builder): always emit both css payloads with cssMeta#1699

Draft
cmorman89 wants to merge 2 commits into
mainfrom
feat-plugin-css-payloads
Draft

feat(plugin-builder): always emit both css payloads with cssMeta#1699
cmorman89 wants to merge 2 commits into
mainfrom
feat-plugin-css-payloads

Conversation

@cmorman89

@cmorman89 cmorman89 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

TL;DR

  • Plugins built for css isolation were silently losing a large share of their styling: the inlined CSS export was corrupted when the module was evaluated (backslash escapes in Tailwind selectors were swallowed), so browsers dropped every variant, fraction, opacity and arbitrary-value rule. The payload was also incomplete — only un-layered utility rules, missing the theme variables, property registrations, keyframes and @import rules the stylesheet depends on.
  • The builder now embeds the CSS so it survives verbatim (JSON serialization) and keeps the whole compiled stylesheet except the preflight (the host document already provides one).
  • The build no longer makes a css-mode decision: every build emits both payloads — the full stylesheet file (linked under shadow isolation) and the inlined export (injected under css isolation) — plus a cssMeta capability list (supports: ['shadow', 'css']) so hosts know the inline payload is safe. inlineCss is deprecated and ignored; the isolation mode is purely a manifest choice.

Details

Corrupted embedding. The extracted CSS was written into the plugin module as a raw template literal, and template-literal evaluation swallows the backslash escapes Tailwind selectors rely on — roughly 40% of rules arrived invalid and were discarded by the browser, and some CSS could make the module fail to load. The export is now emitted through JSON serialization and reaches the runtime byte-for-byte.

Lossy extraction. Only the last utilities layer was kept, unwrapped — so injected rules escaped their cascade layers (overriding host styles document-wide) and lost their supporting theme variables, property registrations, keyframes and @import rules. Extraction now keeps the compiled stylesheet in its original order and structure and drops only the base layer (preflight).

One build, both modes. Previously the build's inlineCss setting had to agree with the manifest's isolation mode, and a mismatch shipped silently broken styling. The build now always emits both payloads and advertises them (export const cssMeta = { supports: ['shadow', 'css'] }): the manifest picks the mode, the artifact states which modes it can honor, and there is nothing left to mismatch. inlineCss remains accepted as a deprecated no-op. The plugin template drops its vite-side isolation wiring — the scaffold prompt writes a single value, uiConfig.isolation in the tool server config — and the README documents that a registered manifest's ui block is authoritative over it.

cssMeta collision guard. A module that exports its own cssMeta keeps it: the builder detects the export from the bundle chunk's export metadata rather than by regexing source text, so detection is reliable across minification, aliasing and const-to-var lowering. It then skips appending its default and warns the author to declare supports themselves. The decision is recomputed per build, so watch mode does not go stale.

Verification

  • packages/plugin-builder: 17 unit tests and the test typecheck pass; lint clean.
  • The cssMeta collision guard is exercised against real bundle shapes — const-to-var lowering, aliased exports, cssMeta-looking text inside string literals, and repeated watch-mode builds — by driving the generateBundle/writeBundle hooks directly.
  • End-to-end on a sample plugin: the runtime CSS string previously lost all of its selector escapes and every layer/property/keyframe block; with this change it is identical to the compiled stylesheet minus the preflight (external @import rules included) and re-parses with zero errors.
  • The export and cssMeta are emitted identically with inlineCss: true, inlineCss: false, and the option absent.

Relationship to the 1.4 line

The host-side compatibility mode ships on the release/1.4 maintenance line as a deploy-only hotfix (it rescues already-deployed plugins by rebuilding from their stylesheet — no rebuild required); its manifest flag (AppUIConfig.css_rebuild) lands there: #1664. This PR carries only the build-tool fix that makes future builds correct at the source, publishing on the main line. Host-side consumption of the cssMeta capability export is deliberately deferred: builds emitting it render correctly under the host's default path, so the marker is forward-looking metadata until a host chooses to read it.

Test Plan

  • Build a plugin and verify the runtime export preserves escapes, layers, property registrations, keyframes and @import rules while dropping the preflight
  • Verify the export and cssMeta capability list are emitted regardless of the deprecated inlineCss option
  • Verify a module that exports its own cssMeta is not clobbered (warned instead), across minified/lowered bundle shapes
  • Load a plugin built with this builder in a host under css isolation and confirm variant/fraction/arbitrary utilities, custom theme tokens, custom animations and @import webfonts apply — pending a host running the compatibility-mode consumer

🤖 Generated with Claude Code

https://claude.ai/code/session_01JpAEfftYG74jVAFhZ87EcD

@cmorman89
cmorman89 requested a review from a team as a code owner July 9, 2026 16:12
cmorman89 and others added 2 commits July 9, 2026 09:27
Emit the full stylesheet asset (linked under shadow isolation) and a
preflight-stripped inline css export (injected under css isolation)
together, plus an export const cssMeta = { supports: ['shadow', 'css'] }
capability marker, so the isolation mode is purely a host/manifest choice.
The inline css is embedded through JSON serialization so Tailwind's escaped
selectors survive module evaluation, and keeps the compiled stylesheet in
order minus the base layer (preflight) the host already provides. A module
that exports its own cssMeta keeps it: the collision guard reads the chunk's
export metadata, reliable across minification and const-to-var lowering.
inlineCss is accepted as a deprecated no-op.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JpAEfftYG74jVAFhZ87EcD
Drop the vite-side inlineCss wiring; the scaffold prompt writes a single
value, uiConfig.isolation in the tool server config, and the README states
that a registered manifest's ui block is authoritative over it. Point the
legacy remedy at rebuilding first, with the manifest flag as the fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JpAEfftYG74jVAFhZ87EcD
@cmorman89
cmorman89 force-pushed the feat-plugin-css-payloads branch from 9a90522 to 22c008b Compare July 9, 2026 16:35
@cmorman89
cmorman89 marked this pull request as draft July 23, 2026 19:22
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.

1 participant