[Feature]: Support `--css-bundle` with `--css module` by registering importmaps per chunk
- Dominant language
- Rust
- Stars
- 90
- Forks
- 23
- Avg merge
- 13h 14m
- Merged PRs (30d)
- 68
Description
## Summary
`--css-bundle` is rejected under `--css module` at `crates/webui/src/lib.rs:521`. The rejection is currently correct, but the stated reason undersells the real blocker, and the underlying design already supports what bundling would need.
## What already works
`crates/webui/src/style_bundle.rs` is strategy-agnostic and already implements the right grouping. Its module doc states the rule explicitly:
> Authored-Shadow stylesheets are never merged. […] the `css_href` a plugin injects into the shadow template it builds roots from, and the module specifier recorded on `shadowrootadoptedstylesheets`. Keeping those stylesheets in single-component chunks keeps both references correct […]. **Light components carry no such parse-time identity and merge freely, which under a Light-first default is nearly all of them.**
This is sound. Light CSS is Document-owned (`webui-parser/src/lib.rs:3576-3578`), so every Light sheet adopts into the same target and merging is semantically identical — build-time stamping already scopes the selectors. Only authored-`shadowrootmode` components need discrete sheets, because each shadow root adopts its own named sheet via `shadowrootadoptedstylesheets`.
Verified empirically: temporarily gating the guard behind an env var and building `examples/app/commerce` with `--css module --css-bundle` produced **the same 20 chunks** as the `--css link --css-bundle` build.
## The actual blocker
Delivery is incoherent, in a way the current error message does not describe. On the commerce home route:
- importmap keys registered (13, one per component): `mp-app`, `mp-navbar`, `mp-search-bar`, `mp-mobile-menu`, `mp-page-home`, `mp-hero-grid`, …
- `componentStyles.closures["mp-app"]`: `["mp-app", "_chunk-mp-navbar-4", "mp-product-image", "mp-price", "mp-footer"]`
`_chunk-mp-navbar-4` and `_chunk-mp-page-home-2` are referenced by closures but registered by **no** importmap, so the client cannot resolve those specifiers. The chunk CSS additionally arrives a second time as inline `` blocks, on top of the per-component data URIs.
### Root cause
Two emission sites resolve tag → CSS directly and never consult `protocol.style_chunks`:
- `crates/webui-handler/src/lib.rs::emit_css_module` (~line 1581) — keys off `component.fragment_id`, reads `protocol.components[tag].css`
- `crates/webui-handler/src/lib.rs` reachable-but-unrendered loop (~line 2059) — same pattern
Both the routed-delivery path (`lib.rs` ~line 1380) and the `componentStyles` payload builder (`route_handler.rs` ~line 470) are **already** chunk-aware. Only the inline importmap registration is not.
## Proposed fix
Resolve tag → chunk in those two sites and emit one importmap per chunk rather than per component.
## Expected value
Small. Measured on commerce home:
- importmap boilerplate 1,907 B → ~440 B (147 B/tag × 13 → × 3)
- `CSSStyleSheet` constructions 13 → 3
- importmap script parses 13 → 3
That is real but minor next to Module's dominant cost: it inlines 26.9 KB of raw CSS as 34.0 KB of percent-encoded `data:` URIs (+26.5%), none of it cacheable across navigations. Compressed home document:
| strategy | gzip | br |
|---|---:|---:|
| link | 13,262 | 9,756 |
| link + bundle | 13,167 | 9,641 |
| module | 35,181 | 23,886 |
Module is ~2.5x the compressed document of link+bundle. Chunking recovers ~1.5 KB of that gap; it does not change Module's shape.
## Recommendation
Low priority. Either implement the tag → chunk resolution above, or keep the guard and correct its message — the current text says "there are no requests to merge", when the load-bearing reason is that closures would reference chunk specifiers the importmap never registers.
## Reproduction
`--css-bundle` was added to the commerce example server in 8aafacaf. Full harness, raw samples, and build outputs are attached to the originating session.
Contributor guide
Research direction
Start in crates/webui-handler/src/lib.rs at emit_css_module around line 1581 and the reachable-but-unrendered loop around line 2059; compare them with the chunk-aware paths near line 1380 and route_handler.rs around line 470. Reproduce with the examples/app/commerce build using --css module --css-bundle. Done means chunk specifiers referenced by closures have importmap registrations without duplicate inline chunk styles.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- css, rust
- Domain
- build-system, frontend
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 64/100