Skip to content

fix: CssStrategy::Module partial rendering for SPA navigation#212

Merged
mohamedmansour merged 2 commits intomainfrom
mmansour/css-module-partial-rendering
Apr 10, 2026
Merged

fix: CssStrategy::Module partial rendering for SPA navigation#212
mohamedmansour merged 2 commits intomainfrom
mmansour/css-module-partial-rendering

Conversation

@mohamedmansour
Copy link
Copy Markdown
Contributor

@mohamedmansour mohamedmansour commented Apr 10, 2026

After commit d690975 removed <script> wrappers from compiled templates, CssStrategy::Module broke during SPA navigation. This commit fixes the full rendering pipeline and hardens the inventory system.

What changed

Partial response contract (handler + router)

render_partial() now emits two separate arrays in the JSON response:

  • templateStyles[]: module CSS definition tags for inventory-new components
  • templates[]: clean JS IIFE payloads (no hybrid <style>...</style>IIFE strings)

The router appends all templateStyles to first (deduped by specifier), then executes all template IIFEs in one nonce-friendly <script> tag. Link/Style modes are unaffected (templateStyles is empty).

SSR module style emission (handler)

Previously, <style type="module"> tags were emitted inline during component rendering via emit_css_module(), which only fired for components actually rendered in the SSR pass. But the inventory covered ALL route-reachable components, creating a mismatch: the inventory said "you have mp-cart-panel" while the CSS definition didn't exist in the document because that component wasn't rendered on the current route.

Moved module style emission to the head_end hook, which now emits definitions for all inventoried components in <head>, mirroring how template IIFEs are already emitted at body_end for all reachable components. This ensures the inventory contract holds symmetrically for both templates and CSS definitions.

The inline emit_css_module() function was removed entirely, which also eliminates the duplicate "import map rule removed" browser warning that occurred when both emission paths produced the same <style type="module"> tag.

Duplicate template override (server.rs + CLI serve)

Both serve_request() and the CLI dev server were calling render_partial() then overwriting templates/inventory with their own derivation, which discarded templateStyles entirely. Both now return render_partial() output directly.

Framework adoptedStyleSheets dedup (styles.ts)

injectModuleStyle() had a page-level Set that prevented the same specifier from being processed more than once. After the first shadow root adopted a stylesheet, all subsequent roots were skipped. Replaced with a Map<string, CSSStyleSheet> cache: the sheet is parsed once per specifier but adopted onto every new shadow root that needs it.

Inventory hash collision (route_handler.rs)

filter_needed_components() was checking the accumulating inventory (built during iteration) instead of the client's original inventory. When two component names hash to the same FNV-1a mod 256 bit position (e.g. mp-app and mp-cart-panel both map to bit 218), the second component was silently dropped. Fixed to check against the immutable client inventory only.

Dead code removal

Removed get_route_templates(), get_route_templates_for_request(), and prepend_css_module() which produced the broken hybrid format and were only called by their own tests. Removed their re-exports from the webui crate.

Docker dual-process deployment (commerce)

Added docker-entrypoint.sh and updated Dockerfile to run two server processes in one container: port 3004 (--css=link) and port 3003 (--css=module).

Test coverage

  • Handler: Style/Link/Module SSR emission, hash collision resilience, non-route sibling inclusion, render_partial style/template separation
  • Parser: shadow DOM shell fragment graph includes child components
  • Server helper: Link/Style/Module partial response shape
  • Router: module style ordering + nonce, empty templateStyles for Link/Style
  • Framework: 5 new unit tests for injectModuleStyle sheetCache/adoptedStyleSheets
  • Commerce: module-mode about page includes cart-panel style in , module-mode partial splits styles from templates

After commit d690975 removed <script> wrappers from compiled templates,
CssStrategy::Module broke during SPA navigation. This commit fixes the
full rendering pipeline and hardens the inventory system.

## What changed

### Partial response contract (handler + router)

render_partial() now emits two separate arrays in the JSON response:
- templateStyles[]: module CSS definition tags for inventory-new components
- templates[]: clean JS IIFE payloads (no hybrid <style>...</style>IIFE strings)

The router appends all templateStyles to <head> first (deduped by specifier),
then executes all template IIFEs in one nonce-friendly <script> tag.
Link/Style modes are unaffected (templateStyles is empty).

### SSR module style emission (handler)

Moved <style type="module"> emission from inline during component rendering
(emit_css_module) to the head_end hook, which now emits definitions for ALL
inventoried components. This ensures the inventory contract holds: when the
inventory says "you have mp-cart-panel", the CSS definition actually exists
in the document for the framework to create CSSStyleSheets from.

The inline emit_css_module() function was removed entirely. This also
eliminates the duplicate "import map rule removed" browser warning that
occurred when both inline and head_end emission produced the same tag.

### Duplicate template override (server.rs + CLI serve)

Both serve_request() and the CLI dev server were calling render_partial()
then overwriting templates/inventory with their own derivation, which
discarded templateStyles entirely. Both now return render_partial() output
directly.

### Framework adoptedStyleSheets dedup (styles.ts)

injectModuleStyle() had a page-level Set that prevented the same specifier
from being processed more than once. After the first shadow root adopted a
stylesheet, all subsequent roots were skipped. Replaced with a Map<string,
CSSStyleSheet> cache: the sheet is parsed once per specifier but adopted
onto every new shadow root that needs it.

### Inventory hash collision (route_handler.rs)

filter_needed_components() was checking the accumulating inventory (built
during iteration) instead of the client's original inventory. When two
component names hash to the same FNV-1a mod 256 bit position (e.g. mp-app
and mp-cart-panel both map to bit 218), the second component was silently
dropped. Fixed to check against the immutable client inventory only.

### Dead code removal

Removed get_route_templates(), get_route_templates_for_request(), and
prepend_css_module() which produced the broken hybrid format and were only
called by their own tests. Removed their re-exports from the webui crate.

### Docker dual-process deployment (commerce)

Added docker-entrypoint.sh and updated Dockerfile to run two server
processes in one container: port 3004 (--css=link) and port 3003
(--css=module).

## Test coverage

- Handler: Style/Link/Module SSR emission, hash collision resilience,
  non-route sibling inclusion, render_partial style/template separation
- Parser: shadow DOM shell fragment graph includes child components
- Server helper: Link/Style/Module partial response shape
- Router: module style ordering + nonce, empty templateStyles for Link/Style
- Framework: 5 new unit tests for injectModuleStyle sheetCache/adoptedStyleSheets
- Commerce: module-mode about page includes cart-panel style in <head>,
  module-mode partial splits styles from templates

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mohamedmansour mohamedmansour changed the title feat: fix CssStrategy::Module partial rendering for SPA navigation fix: CssStrategy::Module partial rendering for SPA navigation Apr 10, 2026
@mohamedmansour mohamedmansour requested a review from a team April 10, 2026 06:29
@mohamedmansour mohamedmansour force-pushed the mmansour/css-module-partial-rendering branch from 53bc9c9 to 59bb773 Compare April 10, 2026 06:29
@mohamedmansour mohamedmansour merged commit cb50e67 into main Apr 10, 2026
21 checks passed
@mohamedmansour mohamedmansour deleted the mmansour/css-module-partial-rendering branch April 10, 2026 16:39
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