fix: CssStrategy::Module partial rendering for SPA navigation#212
Merged
mohamedmansour merged 2 commits intomainfrom Apr 10, 2026
Merged
fix: CssStrategy::Module partial rendering for SPA navigation#212mohamedmansour merged 2 commits intomainfrom
mohamedmansour merged 2 commits intomainfrom
Conversation
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>
53bc9c9 to
59bb773
Compare
KurtCattiSchmidt
approved these changes
Apr 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
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 viaemit_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_endhook, which now emits definitions for all inventoried components in<head>, mirroring how template IIFEs are already emitted atbody_endfor 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