[Feature]: Support `--css-bundle` with `--css module` by registering importmaps per chunk
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 64/100
- Issue type
- Feature
- Clarity
- Mostly clear
- Activity status
- Quiet
- Tech stack
- css, rust
- Domain
- build-system, frontend
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.
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_hrefa plugin injects into the shadow template it builds roots from, and the module specifier recorded onshadowrootadoptedstylesheets. 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 <style data-webui-strategy="module"> 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 offcomponent.fragment_id, readsprotocol.components[tag].csscrates/webui-handler/src/lib.rsreachable-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)
CSSStyleSheetconstructions 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.
- Dominant language
- Rust
- Stars
- 90
- Forks
- 23
- Avg merge
- 13h 3m
- Merged PRs (30d)
- 64
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from microsoft/webui
-
Difficulty 4/5 3-5 days Newbie friendliness 58/100
-
enhancement
Difficulty 5/5 Over a week Newbie friendliness 35/100
-
enhancement
-
Difficulty 5/5 Over a week Newbie friendliness 35/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 55/100
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
Eynzof/Hermes-CN-Desktop#610 ·
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
gitbutlerapp/gitbutler#15998 · 1 comment ·
-
bug triage:deciding
Difficulty 1/5 Under an hour Newbie friendliness 88/100
open-telemetry/otel-arrow#4132 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100