Skip to content

Emit style modules via importmap + dataURI instead of <style type="module">#325

Merged
KurtCattiSchmidt merged 2 commits into
mainfrom
users/kschmi/css-modules-via-importmap
Jun 6, 2026
Merged

Emit style modules via importmap + dataURI instead of <style type="module">#325
KurtCattiSchmidt merged 2 commits into
mainfrom
users/kschmi/css-modules-via-importmap

Conversation

@KurtCattiSchmidt

Copy link
Copy Markdown
Contributor

<style type="module"> is going to need some spec updates and will probably have some syntax changes before we can ship it. In the meantime, we can simulate it via an Import Map and a dataURI. Performance characteristics are very similar between the two, with small stylesheets and duplicated stylesheets actually being slightly faster than <style type="module">.

This change updates the Rust BTR code with the "module" strategy to output an equivalent Import Map instead of <style type="module">. The serde_json Rust library is used for JSON handling, which is used elsewhere in WebUI, so this is not a new dependency.

e.g. this:

<style type="module" specifier="foo">
  span {color: blue;}
</style>

...would be replaced by:

<script type="importmap">
{
  "imports": {
    "foo": "data:text/css,span {color: blue;}"
  }
}
</script>

...where the CSS content is URL-encoded in the Import Map version. This depends on Multiple Import Map support, which was enabled in Chromium 133. Mozilla and WebKit have positive support for Multiple Import Maps but haven't shipped it yet. Since the "module" strategy also depends on shadowrootadoptedstylesheets support (which hasn't shipped yet), it should be fine to add this new dependency.

…URIs

Replace the per-component `<style type="module" specifier="X">CSS</style>`
emission with `<script type="importmap">{"imports":{"X":"data:text/css,…"}}</script>`.

Why
---
The `<style type="module">` shape is a non-standard CSS-module-style-tag
prototype that has not shipped in all host browsers Edge serves built-in
WebUIs in. Import Maps + `data:text/css,…` URIs achieve the same end —
the browser resolves a CSS module by specifier — using only standards-
track features available in Chrome 133+.

The shape change is transparent to downstream consumers: the framework's
client-side adoption code resolves CSS modules by specifier the same way
in both forms, and `adoptedStyleSheets` continues to work unchanged.

What changed
------------
* New private method `emit_css_module_importmap(specifier, css, ctx)`
  in `crates/webui-handler/src/lib.rs`. Percent-encodes only the bytes
  that would actually mis-parse in a data URI (`%` `#` `"` whitespace
  control chars, non-ASCII) — keeps human-readable CSS readable in
  DevTools' importmap view.
* Builds the JSON body with `serde_json::json!` so the specifier and
  data URI are always correctly JSON-escaped.
* Both emission call sites — `emit_css_module` (per-component first
  render) and the `body_end` reachable-but-unrendered loop — route
  through the new helper.
* CSP nonce is applied identically to the legacy `<style type="module">`
  path and to the bootstrap `<script>` emit, so the existing nonce
  tests stay green (assertions updated to the new emission shape).
* Tests updated: positive-shape asserts now check for
  `<script type="importmap"…>` and the embedded data URI; negative-
  shape asserts in non-module strategies additionally guard against
  accidental importmap emission.

Tests
-----
`cargo test -p microsoft-webui-handler --lib` — 285/285 pass.
`cargo test --workspace --lib` — all crates green.
@KurtCattiSchmidt KurtCattiSchmidt force-pushed the users/kschmi/css-modules-via-importmap branch from e4f9826 to b40946e Compare June 5, 2026 23:13
@KurtCattiSchmidt KurtCattiSchmidt merged commit f5cedc3 into main Jun 6, 2026
30 checks passed
@KurtCattiSchmidt KurtCattiSchmidt deleted the users/kschmi/css-modules-via-importmap branch June 6, 2026 01:06
mohamedmansour added a commit that referenced this pull request Jun 8, 2026
Clusters:
- feat: parser comment policy strips template/style comments while preserving legal comments, with CLI, Node, docs, and benchmark coverage (#326, b513efb).
- feat: CSS module delivery now emits import-map data URI modules and the commerce demo defaults to module styles (#325, #327, f5cedc3, 88fc307).
- fix: repeat-scope event arguments hydrate correctly for framework bindings, including strict argument handling (#317, #322, 11b6a6d, c0bd525).
- fix: client binding lifecycle ordering preserves child updates across conditional and repeated DOM paths (#329, 0f40666).
- fix: compiled templates preserve raw style text instead of altering author-provided CSS content (#330, 4db730a).
- docs: issue forms, contribution/support policy, framework rendering docs, CLI docs, and integration guides were refreshed (#321, #328, plus docs in #322/#325/#326/#329; 6853b4c, f97bdc6).

Release bump:
- Update Rust workspace crates, internal crate dependency constraints, Cargo.lock package versions, npm packages, platform packages, router/framework/test-support packages, and .NET Directory.Build.props from 0.0.14 to 0.0.15.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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