feat: make Light DOM a global CSS opt-in - #429
Conversation
There was a problem hiding this comment.
Pull request overview
This PR makes Light DOM the default rendering mode across WebUI, introduces a required componentStyles catalog (resources + ordered closures) as the unified CSS delivery contract, and updates the full stack (parser → protocol → handler → router/framework → assets → docs/tests/examples) to match the new default and style-delivery model.
Changes:
- Switch default DOM strategy to Light, while preserving explicit
--dom=shadow/dom: "shadow"and per-component Shadow opt-in via a sole top-level<template shadowrootmode="open">. - Replace legacy
templateStyles/ inference with required, versionedcomponentStyles(resources + closures) and plumb it through SSR, streaming checkpoints, partial navigation, and component assets (v3). - Update tests, fixtures, examples, and documentation to align with the new default and new CSS/style installation behavior.
Reviewed changes
Copilot reviewed 111 out of 111 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/webui/test/integration.test.ts | Update integration expectations for Light default and componentStyles. |
| packages/webui/src/index.ts | Add dom build option and default it to "light" in Node API wrapper. |
| packages/webui/README.md | Document Light default + Shadow opt-in + componentStyles return shape. |
| packages/webui-test-support/src/fixture-render.ts | Add per-fixture dom override behavior (shadow vs default vs explicit light). |
| packages/webui-router/src/types.ts | Define ComponentStyles and expose registration bridge types. |
| packages/webui-router/src/templates.ts | Switch router registration flow to componentStyles and bridge integration. |
| packages/webui-router/src/streaming.ts | Remove legacy injected module-style set from streaming context usage. |
| packages/webui-router/src/router.ts | Remove router-level module-style tracking; keep SSR styles array for framework lazy dedupe. |
| packages/webui-router/src/router.test.ts | Update tests for componentStyles + bridge ordering and SSR styles retention. |
| packages/webui-router/src/index.ts | Re-export ComponentStyles and related public types. |
| packages/webui-router/src/cache.ts | Replace templateStyles with required componentStyles in partial response shape. |
| packages/webui-router/README.md | Update partial/streaming response contract docs to componentStyles. |
| packages/webui-framework/tests/fixtures/slot-shadow/webui.config.json | Add fixture-level global Shadow config for slot coverage. |
| packages/webui-framework/tests/fixtures/slot-shadow/src/test-slot-btn/test-slot-btn.html | Wrap slot component in Shadow opt-in template. |
| packages/webui-framework/tests/fixtures/slot-shadow/slot-shadow.spec.ts | Add regression asserting explicit global Shadow still wraps templates. |
| packages/webui-framework/tests/fixtures/README.md | Update fixture authoring guidance for new default + Shadow opt-in rules. |
| packages/webui-framework/tests/fixtures/light-dom/webui.config.json | Configure fixture to test product default without passing dom option. |
| packages/webui-framework/tests/fixtures/light-dom/state.json | Add fixture state for Light pipeline coverage. |
| packages/webui-framework/tests/fixtures/light-dom/src/test-shadow-opt-in/test-shadow-opt-in.html | Add component-level Shadow opt-in fixture component. |
| packages/webui-framework/tests/fixtures/light-dom/src/test-shadow-opt-in/test-shadow-opt-in.css | Add Shadow-scoped ::slotted styling in fixture. |
| packages/webui-framework/tests/fixtures/light-dom/src/test-shadow-light-child/test-shadow-light-child.html | Add nested Light child component markup for mixed-mode test. |
| packages/webui-framework/tests/fixtures/light-dom/src/test-shadow-light-child/test-shadow-light-child.css | Add nested Light child styling for mixed-mode test. |
| packages/webui-framework/tests/fixtures/light-dom/src/test-light-dom/test-light-dom.html | Add Light root fixture with client-side child spawning and Shadow opt-in. |
| packages/webui-framework/tests/fixtures/light-dom/src/test-light-dom/test-light-dom.css | Add scoped Light CSS validation in fixture. |
| packages/webui-framework/tests/fixtures/light-dom/src/test-light-child/test-light-child.html | Add Light child fixture component markup. |
| packages/webui-framework/tests/fixtures/light-dom/src/test-light-child/test-light-child.css | Add Light child fixture styling. |
| packages/webui-framework/tests/fixtures/light-dom/src/index.html | Add fixture entry document for real-pipeline Light rendering. |
| packages/webui-framework/tests/fixtures/light-dom/light-dom.spec.ts | Rewrite E2E to validate Light default, scoped CSS, closures order, Shadow opt-in, and slot projection. |
| packages/webui-framework/tests/fixtures/light-dom/element.ts | Switch fixture to real pipeline (define components + behaviors) instead of manual registration. |
| packages/webui-framework/tests/fixtures/css-module/css-module.spec.ts | Update comment to reflect new module install function naming/behavior. |
| packages/webui-framework/src/template.ts | Add componentStyles registration + bridge integration into template registry. |
| packages/webui-framework/src/template.test.ts | Update SSR bootstrap parsing test data to include componentStyles. |
| packages/webui-framework/src/template-types.ts | Remove sa from template metadata surface. |
| packages/webui-framework/src/template-events.ts | Include componentStyles in templates-registered event payload. |
| packages/webui-framework/src/template-element.ts | Install component style closures during hydration and add Light host marker for client-created Light elements. |
| packages/webui-framework/src/template-element.test.ts | Adjust HTMLElement/document mocks for root detection and ownership. |
| packages/webui-framework/src/streaming-protocol.ts | Make streaming bootstrap require componentStyles. |
| packages/webui-framework/src/streaming-pipeline.test.ts | Add coverage for halting stream when checkpoint omits componentStyles. |
| packages/webui-framework/src/streaming-bootstrap.ts | Register componentStyles during boundary bootstrap and avoid merging it into ephemeral state. |
| packages/webui-framework/src/index.ts | Export componentStyles APIs/types from framework package surface. |
| packages/webui-framework/src/element/styles.test.ts | Add unit test suite for componentStyles catalogs (Document/ShadowRoot install, module importmaps, nonce, dedupe). |
| packages/webui-framework/src/element/markers.ts | Ensure ordinal walker skips compiler-emitted style fallback markers. |
| packages/webui-framework/src/element/markers.test.ts | Add test ensuring data-webui-resource elements are skipped in ordinal counting. |
| packages/webui-framework/src/component-asset/resources.ts | Remove legacy component-asset templateStyles importmap injection implementation. |
| packages/webui-framework/src/component-asset/loader.ts | Switch component-asset loading to required componentStyles + graph validation. |
| packages/webui-framework/src/component-asset/asset.ts | Bump component assets to v3 and require componentStyles. |
| packages/webui-framework/src/component-asset.test.ts | Update component asset tests to validate v3 + componentStyles installation/validation. |
| packages/webui-framework/RENDERING.md | Update rendering spec docs for componentStyles/closures and Light default. |
| packages/webui-framework/README.md | Update authoring + DOM selection docs and remove sa references. |
| examples/app/service-worker/scripts/check-render.ts | Update render check to assert new data-webui-resource style markers. |
| examples/app/contact-book-manager/src/atoms/cb-icon-button/cb-icon-button.html | Wrap slot usage in Shadow opt-in template. |
| examples/app/contact-book-manager/src/atoms/cb-button/cb-button.html | Wrap slot usage in Shadow opt-in template. |
| examples/app/commerce/server/src/server.rs | Update server tests to validate new componentStyles resource shapes. |
| docs/guide/why.md | Refresh platform-primitive messaging for Light default + scoped CSS. |
| docs/guide/integrations/wasm.md | Update sample component to wrap <slot> in Shadow opt-in template. |
| docs/guide/integrations/rust.md | Document dom default Light and Shadow selection rules. |
| docs/guide/integrations/node.md | Update Node integration defaults + Shadow opt-in guidance. |
| docs/guide/index.md | Update guide intro to reflect Light default + optional Shadow. |
| docs/guide/concepts/routing.md | Update routing payload examples/field table to componentStyles. |
| docs/guide/concepts/react-comparison.md | Update styling comparison for scoped Light + Shadow opt-in. |
| docs/guide/concepts/plugins/index.md | Update plugin contract to include ComponentTemplateContext with effective DOM mode. |
| docs/guide/concepts/performance.md | Update DOM-mode guidance to reflect Light default and Shadow selection. |
| docs/guide/concepts/interactivity.md | Update template wrapper guidance and styling section for new model. |
| docs/guide/concepts/how-it-works.md | Update SSR/hydration description for Light default + component style installation. |
| docs/guide/concepts/components/index.md | Update component authoring docs for Light default and Shadow opt-in rules. |
| docs/guide/concepts/best-practices.md | Update best practices for Light default and slot-in-Shadow constraint. |
| docs/guide/cli/index.md | Update CLI docs for --dom default and new CSS resource delivery contract. |
| docs/ai/SKILL.md | Update AI authoring guidance for Light default + slot-in-Shadow rule. |
| docs/.webui-press/components/code-comparison/code-comparison.html | Wrap slots in Shadow opt-in template for docs component. |
| crates/webui/src/server.rs | Update Rust server tests to validate componentStyles resources across CSS strategies. |
| crates/webui/src/component_assets/serialize.rs | Emit v3 component assets with componentStyles resources + closures. |
| crates/webui/src/component_assets/render.rs | Thread protocol context into asset rendering options. |
| crates/webui/src/component_assets/payload.rs | Render style resources based on CssStrategy instead of legacy templateStyles importmap. |
| crates/webui/src/component_assets.rs | Prune/validate style closures when retaining entry protocol; add tests. |
| crates/webui/README.md | Update Rust crate README for Light default and doc table updates. |
| crates/webui-wasm/src/parser.rs | Snapshot/apply effective component DOM strategies and style closure metadata in WASM parser output. |
| crates/webui-wasm/src/lib.rs | Update WASM tests for slot-in-Shadow and new style marker output. |
| crates/webui-protocol/src/gen_webui.rs | Add effective_dom_strategy, style_closures, and flip DomStrategy enum values (Light=0). |
| crates/webui-protocol/proto/webui.proto | Update proto schema for effective_dom_strategy + style_closures; flip DomStrategy enum ordering. |
| crates/webui-press/README.md | Update docs generator narrative for Light default + Shadow opt-in. |
| crates/webui-press/components/webui-press-tabs/webui-press-tabs.html | Wrap slots in Shadow opt-in template. |
| crates/webui-press/components/webui-press-tab/webui-press-tab.html | Wrap slots in Shadow opt-in template. |
| crates/webui-press/components/webui-press-tab-panel/webui-press-tab-panel.html | Wrap slots in Shadow opt-in template. |
| crates/webui-press/components/webui-blockquote/webui-blockquote.html | Wrap slots in Shadow opt-in template. |
| crates/webui-press/components/code-block/code-block.html | Wrap slot component in Shadow opt-in template. |
| crates/webui-parser/src/plugin/webui.rs | Resolve/store per-component effective DOM strategy and remove sa emission. |
| crates/webui-parser/src/plugin/mod.rs | Extend plugin contract with ComponentTemplateContext and require effective_dom_strategy in artifacts. |
| crates/webui-parser/src/plugin/fast_v3.rs | Plumb effective DOM strategy through FAST v3 artifacts. |
| crates/webui-parser/src/plugin/fast_v2.rs | Plumb effective DOM strategy through FAST v2 artifacts. |
| crates/webui-parser/src/diagnostic.rs | Add diagnostic codes for Light DOM slot, invalid Shadow wrapper, unsupported light CSS, etc. |
| crates/webui-parser/src/component_registry.rs | Preserve authored CSS for diagnostics when processed CSS is replaced. |
| crates/webui-parser/benches/parser_bench.rs | Update benches for new parser options + add Light CSS boundary benchmark. |
| crates/webui-node/src/lib.rs | Update Node binding docs/tests for Light default and slot constraints. |
| crates/webui-handler/src/streaming/session.rs | Track CSS strategy + style closure roots during streaming; ensure style metadata present. |
| crates/webui-handler/src/streaming/checkpoint.rs | Emit componentStyles at checkpoints; gate module importmap emission by strategy. |
| crates/webui-handler/src/html_encode.rs | Add safe CSS style text writer to prevent premature </style> termination. |
| crates/webui-handler/README.md | Update handler README for componentStyles return shape. |
| crates/webui-handler/benches/bootstrap_state_bench.rs | Update benchmark payload shapes to include componentStyles. |
| crates/webui-cli/src/commands/serve.rs | Update comment for JSON partial contents. |
| crates/webui-cli/src/commands/common.rs | Default CLI --dom to Light and add parsing tests. |
| crates/webui-cli/src/commands/build.rs | Default build command to Light; update tests and asset version assertions. |
| .github/skills/webui-dev/SKILL.md | Update skill guidance to reflect Light default + Shadow opt-in wrapper rule. |
| .github/skills/testing/SKILL.md | Update fixture guidance for Light default and Shadow opt-in patterns. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 111 out of 111 changed files in this pull request and generated no new comments.
Suppressed comments (1)
crates/webui/src/component_assets.rs:210
- This new unit test asserts on a specific error string fragment ("requires missing ..."), which is brittle and will break on harmless wording changes. Prefer asserting on the error variant and (optionally) a stable substring describing the condition, rather than the full rendered error text.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 164 out of 164 changed files in this pull request and generated no new comments.
Suppressed comments (1)
packages/webui-framework/tests/fixtures/README.md:97
- The fixtures README says
webui.config.jsonsupports adomkey, but the fixture renderer only readscssandscript(see packages/webui-test-support/src/fixture-render.ts:62-83 and :113-120). Keepingdomhere is misleading and suggests a build option that no longer exists.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 166 out of 170 changed files in this pull request and generated no new comments.
Suppressed comments (1)
packages/webui-router/src/router.ts:67
- isRouteStyleMarker requires a
data-webui-strategyattribute, but the client runtime’s style installer (packages/webui-framework/src/element/styles.ts, appendResource) only setsdata-webui-resourceon inserted<link>/<style>markers (SSR may include the strategy attribute, but client-installed markers often won’t). As a result,mountedRouteComponent()/clearRouteContent()can treat real style markers as ordinary content and remove them when remounting routes, breaking route-scoped CSS on navigation.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 165 out of 170 changed files in this pull request and generated no new comments.
Suppressed comments (3)
crates/webui-cli/src/utils/output.rs:312
- After switching force_colors() to an RAII guard, these tests should stop manually toggling console::set_colors_enabled(prev) and just keep the guard alive for the duration of the assertion block.
let (_guard, prev) = force_colors();
let (_display, message) = build_error_renderings(&template_error());
console::set_colors_enabled(prev);
crates/webui-cli/src/utils/output.rs:328
- After switching force_colors() to an RAII guard, this test can just bind the guard for the scope and avoid manually restoring console color state.
let (_guard, prev) = force_colors();
let (display, _message) = build_error_renderings(&template_error());
console::set_colors_enabled(prev);
crates/webui-cli/src/utils/output.rs:288
- force_colors() relies on callers to restore console::set_colors_enabled(prev). If the test panics between enabling and restoring, the process-global color setting can leak into later tests (even though the mutex prevents concurrent runs). Prefer an RAII guard that restores the previous state in Drop so restoration happens even on panic.
This issue also appears in the following locations of the same file:
- line 310
- line 326
fn force_colors() -> (MutexGuard<'static, ()>, bool) {
let guard = COLOR_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let previous = console::colors_enabled();
An earlier note recorded that per-element marker stamping renders differently by raising selector specificity. Re-measuring with an identity-rebuild control shows that claim came from a harness bug: the harness gave each host its own marker, but inside @scope a bare selector is relative and never matches the scoping root. Correctly stamped descendants reproduce the current computed styles exactly. Record what the measurements actually support: the plain-descendant host prefix leaks into nested components and is strictly dominated, while stamping is exact and faster but cannot mark DOM created outside a compiled template, which @scope covers natively. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Replace the native `@scope (tag[data-wl]) to (:scope [data-wl] > *)` enclosure with per-element scope markers stamped at build time. Every element a Light component's template declares receives a hashed `data-wl-<id>` attribute, and every top-level compound in that component's CSS is qualified with a zero-specificity `:where([data-wl-<id>])`. Measured on the commerce example, this removes the 27-35% style recalculation cost that Blink's `@scope` activation bookkeeping added, in exchange for roughly +200 bytes of compressed markup per document. Computed styles are identical: ~12,500 declarations per route match byte-for-byte against an identity-rebuild control on two routes. Split the transform into four layers so a future minifier or dead-selector pass has the primitives it needs: `css_scan` produces byte-level tokens, `css_selector` walks top-level compounds with roles, `css_boundary` applies the transform, and `light_scope` derives markers and stamps HTML. Stamping is what makes dead-selector analysis sound, since a rule is now statically bounded to exactly one template. Also fix an SSR bug the new fixture test surfaced: a valueless `data-wl-*` marker on a nested component host was silently dropped, because component elements skip `data-`-prefixed attributes and only re-emit the ones carrying a value. Reserved markers are compiler-owned and must reach SSR bytes verbatim, so they are now excluded from the skip set. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Build-time stamping can only mark elements a compiled template declares. A
template with a raw binding (`{{{expr}}}`) interpolates author-supplied markup
at render time, so those elements carry no marker and every marker-qualified
selector silently stops matching them.
This was caught by a computed-style equivalence harness run against two live
builds: of 642,539 declarations compared across four commerce routes, 8
diverged, all on one element. `<div class="product-description">
{{{descriptionHtml}}}</div>` stamped the div but not the interpolated `<p>`,
so `.product-description p { margin: 0 0 1.5rem }` stopped applying and the
paragraph fell back to the UA default.
Make the boundary a per-component decision instead of a global one. `LightScope`
has two shapes and the compiler picks the strongest the component's DOM permits:
`Stamped` when the rendered DOM is fully build-time known, `Enclosed` (the
native `@scope` prelude, which resolves membership at match time) when it is
not. The two shapes differ in only three places -- whether compounds are
qualified, what `:host` lowers to, and whether the body is wrapped -- so the
selection routes through `Stamper` without a second transform.
Detection is core, not plugin-specific: `{{{` is `HandlebarsParser`'s raw-signal
syntax, and binding `innerHTML`/`outerHTML`/`srcdoc`/`content` is already
blocked, so it is the only sanctioned dynamic-HTML path. The test is a
conservative substring check, so a literal `{{{` in text costs the fast path but
never correctness.
The equivalence harness now reports 0 of 642,539 declarations different, and
the win survives: on the commerce example only `mp-page-product` takes the
enclosure. Style recalculation is 11-14% faster at load (88-90% paired win
rate) and 5-8% faster across a route change (76-88%), at +3.9-4.7% compressed
document bytes.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A nested at-rule inherits the enclosing style block's kind, so the scoping
pass treated `@media (min-width: 1px)` as a selector list and spliced the
component marker into it, emitting `@media:where([data-wl-x]) (min-width:
1px)`. Browsers drop the whole at-rule, silently losing every declaration
inside it. The same applied to nested `@supports`, `@container`, `@layer`,
`@starting-style`, and `@scope`.
CSS nesting with a nested at-rule is how responsive component CSS is
written, so this would have hit the first app that used it. It did not
surface earlier because no CSS in the repository nests an at-rule, which is
also why the computed-style equivalence run reported no divergence: the
corpus never reached the broken path.
Track the pending at-rule prelude alongside its block start and qualify a
prelude only when it is a real selector list. A nested `@scope` now also
routes to the prelude-aware path instead of being stamped as a compound.
Also exempt custom properties from the nested-rule heuristic: a custom
property value is an arbitrary token stream that may legally contain
braces, so `--x: { color: red }` was being rewritten as a selector.
Add a modern-CSS corpus pinning the exact output for both shapes, and two
output-side invariants: stamping may only insert qualifiers, and no
qualifier may land between an at-keyword and the brace it opens. Both
placement tests fail without this fix.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The stamper rewrites developer CSS, so the risk that matters is silent corruption of syntax it predates. The existing corpus pins exact output, which makes each new case expensive to add - and an expensive corpus stops being extended, which is how a gap hides. Add MODERN_CSS_INVARIANTS: inputs with no expected output. Three invariants derive their own oracle from each entry, so a new construct costs one line and still gets full coverage. Add the third invariant, closing the last uncovered regression class: a qualifier placed after a pseudo-element emits invalid CSS, and stripping the qualifier undoes the mistake, so the round-trip invariant cannot see it. Verified by mutation - forcing compound-end insertion fails the test with the offending selector named. Extend rejects_global_at_rules with @Property, @position-try, @scroll-timeline, and @font-feature-values to document that unrecognized at-rules fail closed rather than being rewritten. Test-only; no behavior change. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Fifteen findings from a correctness, performance, and maintainability pass.
Correctness, parser
- `:host` nested inside a functional pseudo-class (`:not(:host)`, `:is(:host,
.a)`) cannot be represented by stamping: the host carries `data-wl` while
its descendants carry `data-wl-<id>`, so no single zero-specificity token
bounds both branches. Such components now take the `@scope` enclosure,
chosen by `stamping_is_representable` and memoized per tag so a component
first reached through a CSS-less path cannot change shape mid-build. The
rewriter fails loudly if the two ever disagree.
- CSS escape sequences are consumed whole. `\{`, `\(`, and `\:host` were read
as real structure, which desynced the depth counters for the rest of the
stylesheet.
- A `//` line comment no longer swallows a `}` on the same line. `//` is a
WebUI dialect extension, so the brace still closes its block; swallowing it
left the block open and silently un-scoped everything after it.
- Statement-form `@layer a, b;` passes through verbatim instead of being
rejected. It declares only cascade order - no selector list, no block.
- The equivalence corpus now derives a host-inclusive oracle, closing the gap
that hid the nested-`:host` bug.
Correctness, handler and protocol
- A component already covered by a shared CSS chunk is no longer re-shipped
inline. Coverage is computed from the chunk index rather than from
traversal order, so the result no longer depends on which route is rendered
first. `WebUIProtocol::style_chunk_index()` is now the single definition of
"already covered".
Correctness, framework
- `loadWebUIDataBlock` publishes parsed state and templates before registering
component styles. A rejected `componentStyles` payload discarded a
successful parse and left the block to be re-parsed on every subsequent
read.
Performance
- `directResourceMarkers` caches its scan per style target, keyed on
`childElementCount`, instead of walking the scope's children on every
install.
- Component-asset styles are validated and deep-copied once and memoized by
payload identity, instead of once in `validateAsset` and again in
`prepareComponentPayload`.
Maintainability
- A DEV-only warning fires when two style closures disagree on resource
order, which is otherwise silent and position-dependent.
- A missing-template error lists every missing tag with remediation, rather
than the first one found.
- `installComponentStyles` documents why an unknown root returns quietly but
an unknown resource throws.
- The framework's `dispatchTemplatesRegistered` drops its unused
`componentStyles` parameter. The router's separate dispatcher still
attaches styles when the framework bridge is absent; that path is live and
stays.
Two findings were resolved as non-defects and are recorded rather than
changed: `closure.component_tags` is read by five call sites and cannot be
cleared, and `css_boundary.rs` is left intact - moving the riskiest file in
the change right before merge hides more than it reveals.
DESIGN.md and docs/ai/SKILL.md record the two behavior changes: statement-form
at-rule passthrough, and CSS shape as a second reason a component takes the
enclosed path.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The example server could select a CSS strategy but not enable bundling, so there was no way to A/B `--css link` against `--css link --css-bundle` on a realistic 26-component app. Thread `css_bundle` through `ApiArgs`, `AppState::load`, and `FrontendRuntime::load` into `BuildOptions`. `serve_asset` already serves anything in `build_result.css_files` by relative path, so emitted chunk files need no extra wiring. Measured on the home route at 40 ms emulated RTT, 20 paired iterations, bundling won every iteration: FCP -23.1%, LCP -17.2%, DOM interactive -24.3%, CSS requests 13 -> 9, and CSS bytes -14.3% because fewer larger files compress better than many small ones. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Bundling's value is a shared chunk that stays cached across routes and deploys, but the default asset filename template is [name].[ext], so a chunk keeps the same URL after its bytes change and cannot carry a long immutable Cache-Control. Point at --asset-file-name-template and record what bundling actually buys over HTTP/2, where request count is multiplexed rather than head-of-line blocked. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The merge with main brought PR microsoft#425's lazy-hydration fixtures onto a branch where Light DOM is the default, leaving four E2E regressions and one type error. Drop the dead `dom` passthrough in the shared fixture renderer and the matching config key. `BuildOptions.dom` no longer exists, so assigning it failed `typecheck:e2e` and aborted the whole webui-framework E2E job before Playwright ever started. Give `test-shadow-policy-parent` an authored `<template shadowrootmode="open">`. Its purpose is to prove render-policy CSS crosses a Shadow boundary, so it is exactly the case that must opt in now that wrappers are the only way to get a shadow root. Query the Light DOM in the three specs that reached through `shadowRoot` into `test-streamed-lazy-parent` and `test-lazy-item`. Neither component needs a shadow root, and the surrounding assertions already use Playwright locators that pierce either tree. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Reintroduce the build-time DOM fallback across Rust, CLI, Node, and WASM while preserving per-component mixed roots and hardening scoped CSS, style delivery, streaming, deferred assets, and router lifecycle behavior. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Share the style-closure walk, the CSS strategy wire name, and the FAST template builder instead of keeping per-call-site copies, and drop the exports and helpers the pivot left unreferenced. - Add `WebUIProtocol::style_closure_unit` and `style_closure_unit_count` so all three delivery walks resolve closure members identically. The two handler walks now consult `style_chunk_index`, which its own docs already named the single definition of "covered by a chunk"; chunk-root closures keep byte-identical output. - Carry `style_chunk_index` on `WebUIProcessContext` so it is built once per render. It cannot live on `RouteHandlerProtocol`, which owns the protocol it would borrow from. - Add `CssStrategy::wire_name`; prost's `as_str_name` yields the uppercase proto identifier, which the client runtime does not accept. - Delegate `render_style_resource` to `component_style_resource` and drop the now-redundant strategy parameters. - Move `build_f_template`, `f_template_style_injection`, and `minify_inter_tag_whitespace` into `fast_v3`, extending the existing `fast_v2` -> `fast_v3` import, and delete the duplicate tests. - Add `sameComponentStyleClosure` and a `queueModule` helper, fix the router's `sameComponentStyleResource` to compare members like the framework's, and de-export `readNonce`, `isRouteStyleMarker`, and `hasRegisteredComponentStyleClosure`. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Make explicit --dom=light use ordinary authored CSS in the owning Document or ShadowRoot instead of compiler-generated selector and template scoping. - Remove the Light boundary compiler, scope marker stamping, and marker runtime writes; delete the now-unused selector/scope modules. - Reject :host, :host-context, and ::slotted in effective Light CSS with the existing unsupported-light-css diagnostic instead of rewriting them. - Keep authored Shadow CSS native and preserve CSS-tree closures, ordered bundling, route head hoisting, ShadowRoot preloads, streaming, navigation, and component assets. - Generate lazy-render policy CSS as a normal tag selector for Light trees and retain the Shadow host form for Shadow trees. - Update mixed-mode tests, benchmark fixtures, DESIGN.md, and user docs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Review the authored Light CSS refactor for dead code, false positives, and stale contract documentation. - Parse CSS into selector preludes before checking Shadow-only pseudos, so declaration values such as `--state:host` and `url(:host)` remain valid. - Keep the removed marker/keyframe diagnostic codes as deprecated public constants for downstream compatibility; they are no longer emitted. - Avoid policy metadata lookups for direct parser helper calls without a registered component while preserving explicit missing-component errors in the registry. - Update the remaining AI, framework, interactivity, rationale, and how-it-works documentation that still described compiler-scoped Light CSS. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Treat a sole top-level bare template as an explicit Light-DOM wrapper even when --dom=shadow is the build fallback. Unwrap it before SSR and client hydration, while attributed templates and compiler policy wrappers keep their existing meaning and shadowrootmode="open" remains the explicit Shadow form. - Extend effective DOM analysis with an authored Light-root content range. - Preserve Shadow fallback behavior for attributed and policy templates. - Add parser coverage for mode selection, slot rejection, exact unwrapping, and the attributed-template fallback. - Add a real default-Shadow Playwright fixture proving a bare template renders direct Light DOM with global CSS. - Synchronize DESIGN.md, framework, CLI, AI, and authoring documentation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Correct deferred component asset URLs, refresh the generated Python fixture, and reuse SSR stylesheet preloads. Align contact-book CSS assertions and snapshots with the documented per-CSS-tree Shadow DOM behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update documentation search tests for nested ShadowRoots and restore Ubuntu visual baselines generated from the corrected component behavior.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Harden CSS bundling and hydration for independent component delivery. - Reject top-level @import rules when creating shared CSS chunks because concatenation cannot preserve import ordering; report an actionable build error instead of emitting invalid CSS. - Reuse compatibility scratch storage across bundle-root checks. - Resolve bundled chunks for non-root Light closures only when the complete ordered chunk membership is present; otherwise keep the component fallback, preventing unrelated global CSS from leaking into an independently loaded tree. - Exclude compiler-generated CSS import-map scripts from SSR element ordinals while keeping them distinct from retained Link/Style markers. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
32e945e to
af2f90f
Compare
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2efb60e
into
microsoft:main
## Summary - allow inert `#webui-data` startup blocks to omit `componentStyles` - preserve strict validation for malformed `componentStyles` values when the field is present - cover the templates/state-only bootstrap regression introduced by #429 ## Validation - regression-red proof produced `[WebUI] componentStyles is required.` with the pre-fix loader - `pnpm --dir packages/webui-framework test:unit` - `pnpm --dir packages/webui-framework typecheck:e2e` - `pnpm --dir packages/webui-framework exec playwright test lazy-hydration.spec.ts light-shadow-policy.spec.ts` (37 passed) - `WEBUI_LAZY_HYDRATION_RUNS=1 WEBUI_LAZY_HYDRATION_TRACE_RUNS=0 pnpm --dir examples/integration/streaming-browser-bench test:lazy-hydration` (1 passed) - `cargo xtask check` Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: febce809-72a9-4daf-bb94-6ee494bed686
## The failure on `main`
`cargo xtask bench all` mapped every Criterion target onto a single
`cargo bench --workspace`. Cargo forwards everything after `--` to
*every* benchable target in the workspace, including the libtest
unit-test harnesses of libraries and binaries. Those harnesses don't
understand Criterion's baseline flags, so the run aborted on the first
one it reached.
Reproduced on today's tip (`e971c13a`):
```console
$ cargo xtask bench all --save-baseline a9-red-final
Finished `bench` profile [optimized] target(s) in 1m 09s
Running unittests src\main.rs (target\release\deps\demo_shell-ffd2d20b0bea4e63.exe)
error: Unrecognized option: 'save-baseline'
error: bench failed, to rerun pass `-p demo-shell --bin demo-shell`
bench failed: exit code 101
error: process didn't exit successfully: `xtask.exe bench all --save-baseline a9-red-final` (exit code: 1)
```
**Baselines recorded: 0.** The before/after workflow documented in
`BENCHMARKS.md` could not work at all.
## The fix
`bench all` now walks an explicit `CRITERION_BENCHES` table and invokes
each target on its own, so baseline flags only ever reach a real
Criterion binary:
```
cargo bench -p <package> --bench <target> -- [extra args] --save-baseline NAME
```
Declared targets (11):
| package | bench target |
|---|---|
| microsoft-webui-parser | parser_bench |
| microsoft-webui-handler | handler_bench |
| microsoft-webui-handler | bootstrap_state_bench |
| microsoft-webui-handler | streaming_hydration_bench |
| microsoft-webui-protocol | protocol_bench |
| microsoft-webui-expressions | expressions_bench |
| microsoft-webui-state | state_bench |
| microsoft-webui-ffi | protocol_bench |
| microsoft-webui | contact_book_bench |
| microsoft-webui | streaming_bench |
| microsoft-webui | component_assets_bench |
Each target is announced and run separately:
```console
$ cargo xtask bench all --save-baseline a9-green-final
▸ microsoft-webui-parser / parser_bench
Running benches\parser_bench.rs (...)
▸ microsoft-webui-handler / handler_bench
Running benches\handler_bench.rs (...)
▸ microsoft-webui-handler / bootstrap_state_bench
Running benches\bootstrap_state_bench.rs (...)
▸ microsoft-webui-handler / streaming_hydration_bench
```
**Baseline directories recorded under `target/criterion/`: 0 on `main` →
77 with this change.** That is the claim this PR makes.
The run stays **fail-fast**: a declared benchmark that is broken stops
the run rather than being silently skipped.
## New: `cargo xtask bench lazy-hydration`
Exposes the existing Playwright/CDP offscreen hydration matrix in
`examples/integration/streaming-browser-bench` as a first-class bench
target, matching how the other integration benches are surfaced.
Baseline flags map onto `WEBUI_LAZY_HYDRATION_SAVE` /
`WEBUI_LAZY_HYDRATION_COMPARE`, which is exactly what
`tests/lazy_hydration_matrix.spec.ts` already reads and what the bench
README already documents.
The shared driver/fixtures also now assert the Toggle/Delete bindings
are wired to the *right* handlers via `__benchToggleCount` /
`__benchRemoveCount`, instead of only counting interactions in
aggregate, and `TODO_TEMPLATE` is typed as `TemplateMeta` so fixture
drift is caught at typecheck time.
## Two pre-existing `main` failures this surfaces — neither introduced
here
This PR touches no file under `crates/` or `packages/`. Both failures
below reproduce on pristine `main` with every change in this PR stashed.
**1. `microsoft-webui-handler / streaming_hydration_bench`**
```
thread 'main' panicked at crates\webui-handler\benches\streaming_hydration_bench.rs:175:9:
legacy render failed: Rendering invariant error: Shadow style hook `bench-island` does not match the active component root
```
The old `--workspace` path died at the `demo-shell` harness before ever
reaching this bench, so fixing the routing *un-masks* it. Left declared
and fail-fast deliberately; skipping it would hide a real failure.
The other 7 declared targets were each run individually to confirm they
route correctly and accept their baseline arguments — all exit 0.
**2. `cargo xtask bench lazy-hydration` → `[WebUI] componentStyles is
required.`**
Verified identical on pristine `main` via `pnpm test:lazy-hydration`
(same assertion, `lazy_hydration_matrix.spec.ts:312`). Root cause:
`packages/webui-framework/src/template.ts:274` calls
`registerComponentStyles(parsed.componentStyles)` unconditionally in
`loadWebUIDataBlock()`, and `requireComponentStyles` throws when the
value is `undefined`. The bench fixture's `#webui-data` block
legitimately carries only `{templates, state}` — it has no component
CSS. `git log -L` blames that line to #429.
Not patched here: the fix belongs in the runtime, and emitting dummy
`componentStyles` from the fixture would paper over the regression. Both
failures are being routed to separate correctness PRs.
The new command itself is verified correct — it resolves the bench
directory, spawns `playwright test -c
playwright.lazy-hydration.config.ts`, and faithfully propagates the
child's exit code; `cargo xtask bench lazy-hydration` and a direct `pnpm
test:lazy-hydration` produce identical output.
## Validation
`cargo xtask check`:
```
✔ license-headers ✔ fmt ✔ clippy ✔ proto (drift check)
✔ deny ✔ test ✔ build ✔ build (wasm)
✔ build (examples) ✔ bench (validate)
✘ docs
```
The `docs` failure is `PROJ-C013: Adapter module graph is incomplete or
inconsistent`, an esbuild projection error. Confirmed
**pristine-`main`-equivalent** by stashing this PR and running `pnpm
build` in `docs/` — byte-identical failure. Documented, not fixed; out
of scope here.
Includes 7 new `xtask` unit tests covering per-target routing,
`--`-separator placement, extra args ordering ahead of baseline flags,
and `CRITERION_BENCHES` integrity (non-empty, no duplicate pairs, every
declared harness exists on disk).
## Scope
Four files: `xtask/src/main.rs`, `BENCHMARKS.md`, and the two
lazy-hydration bench lib files.
Deliberately excluded: `benchmark_report` / `bench report`,
`ssr-frameworks` and `examples/integration/ssr-framework-bench`, docs
snapshot generation and published benchmark JSON,
`xtask/src/build_examples.rs`, workspace member changes, and any
lockfile, dependency, or product runtime change.
This is a tooling-correctness change. It makes **no product performance
claim** and reports no framework speedup.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2605df0b-c217-4047-9413-49be787b1786
Current behavior
WebUI uses Shadow DOM for unwrapped components by default. This PR makes explicit
--dom=lighta first-class mode for authored/global Light CSS:--dom=lightrender directly into the host and use ordinary CSS in their owningDocumentorShadowRootCSS tree.<template>is an explicit Light-mode wrapper even when the fallback is--dom=shadow; WebUI removes the wrapper before rendering and hydration.<template shadowrootmode="open">explicitly selects native Shadow DOM in either build mode.w-render/w-hydratepolicy wrappers, and nested templates do not select a mode; they retain their ordinary/policy meaning.@scope, or namespaced for keyframes/layers. Normal cascade and inheritance apply across Light components in the same CSS tree.:host,:host(...),:host-context(...), and::slotted(...)in effective Light CSS fail at build time withunsupported-light-cssand actionable help to use a normal selector or opt into Shadow.Benchmark: representative 432-instance fixture
Eight browser cycles and eight release-server cycles were run on the current head with 48 component types, nested Light/Shadow variants, shared selectors, container queries, and bundled/unbundled Link delivery. Computed-style parity was identical across all 48 component types.
Global Light improves server rendering, output size, stylesheet consolidation, and document readiness. Native Shadow remains the stronger CSS-engine boundary for CSS-heavy or frequently restyled trees; this PR does not claim Light is universally faster.
Validation
cargo xtask checkCloses #410
Closes #433