Skip to content

feat(compiler): export the scoped-CSS engine from the Rust core, independent of TSRX - #4289

Merged
ryansolid merged 3 commits into
nextfrom
compiler-export-compile-style
Oct 26, 2026
Merged

ryansolid merged 3 commits into
nextfrom
compiler-export-compile-style

Conversation

@ryansolid

@ryansolid ryansolid commented Oct 26, 2026 •

Copy link
Copy Markdown
Member

Requested by Ryan · project thread

What

This PR moves TSRX's scoped-CSS engine from src/tsrx/style/ to src/style/ and exports it as a public solidjs_compiler::style module in every build, without needing the tsrx feature:

  • compile_style and compile_style_with_class_map_selectors
  • StyleInput, StyleLocation, StyleKind, Element, ElementKind, ElementChild, Attribute, AttributeValue, StyleOutput, ClassMapEntry and StyleError

The engine already "deliberately has no dependency on the TSRX/Oxc frontend". A frontend describes the elements a <style> block governs as Element values. It gets back the scoped stylesheet, its hash, the class map and the ids of the elements to annotate, which are the same bytes a .tsrx file gets. TSRX becomes one caller of the engine among others.

Why

ReactiveScript's Rust port (spec/11 CP-1d in ryansolid/reactivescript) reuses these style semantics for .rx views instead of reimplementing them. The port shouldn't depend on TSRX being present, so the engine moves out of the frontend that happened to host it.

Changes

  • git mv src/tsrx/style src/style. lib.rs gains pub mod style;. tsrx/style_projection.rs and tsrx/lower.rs now import from crate::style. The module doc is updated.
  • StyleLocation now documents the line and column bases it expects: a 1-based line and a 0-based UTF-16 column of the <style tag. Both feed the hash, so an external frontend has to match them.
  • New tests/style_engine.rs, which covers:
    • with tsrx enabled, parity: driving the engine directly gives exactly the css and css_hash that compile() returns for the equivalent .tsrx module
    • hashing that changes with location
    • the expression class map
    • preserve_class_map_selectors
    • StyleError
  • The engine's existing unit tests now run in the --no-default-features build too.
  • A README section and a patch changeset.

There is no behaviour change. The TSRX route and the Node transform() produce the same output.

Notes for review

  • Element::native and with_static_attr are still #[cfg(test)], so callers build an Element with a struct literal. All of its fields are public.

Testing

All of these are green locally:

  • cargo test in the three feature sets the test:rust script runs (default, --no-default-features, and --no-default-features --features tsrx)
  • clippy with -D warnings for each of those feature sets
  • cargo fmt --check

The red Size check is pre-existing on next and shows 0 B against base (see the comment below).

🤖 Generated with Claude Code

https://claude.ai/code/session_01ViozQ3kVSofMUQ3fwpyq5o

Expose `solidjs_compiler::style` (behind the default `tsrx` feature):
`compile_style`, `compile_style_with_class_map_selectors` and their input
and output types. The engine already has no dependency on the TSRX
frontend, so another frontend can describe a `<style>` block's elements as
`Element` values and get the same stylesheet and hash a `.tsrx` file gets.

Documents `StyleLocation`'s line/column bases, since they feed the hash.
Additive: the TSRX route and the Node `transform()` are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01ViozQ3kVSofMUQ3fwpyq5o
@changeset-bot

changeset-bot Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4d271cf

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 15 packages
Name Type
@solidjs/compiler Patch
@solidjs/babel-plugin Patch
@solidjs/diagnostics Patch
@solidjs/element Patch
@solidjs/h Patch
@solidjs/html Patch
@solidjs/signals Patch
solid-js Patch
@solidjs/universal Patch
@solidjs/web Patch
test-integration Patch
board-example Patch
canvas-example Patch
server-dbmon-example Patch
todos-server-example Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@ryansolid ryansolid self-assigned this Oct 26, 2026
@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Size (brotli, eager entry chunk)

scenario head vs base minified vs base minified vs recorded cap lazy chunks (not counted)
signals: core floor (createSignal/Memo/Effect/Root/flush) 7.44 KB 0 B 0 B −43 B 7.44 KB ✅
signals: + createStore 15.06 KB 0 B 0 B −842 B 15.04 KB ⚠️ over by 19 B, 862 B minified headroom
signals: + isPending/latest 10.32 KB 0 B 0 B +9 B 10.30 KB ⚠️ over by 21 B, 11 B minified headroom
signals: + until/refresh (truth readers, no optimism) 8.07 KB 0 B 0 B −49 B 8.05 KB ⚠️ over by 15.99999999999909 B, 69 B minified headroom
app: render + one signal (the simple-app floor) 9.80 KB 0 B 0 B −85 B 9.75 KB ⚠️ over by 46 B, 105 B minified headroom
app: hydrating (no stores) with Show/For/Loading/Errored/lazy 18.47 KB 0 B 0 B −2 B 18.38 KB ⚠️ over by 86 B, 22 B minified headroom lazy-page.js 0.04 KB
app: hydrating + every store primitive family 30.85 KB 0 B 0 B −748 B 30.66 KB ⚠️ over by 192 B, 768 B minified headroom lazy-page.js 0.04 KB
app: CSR with Show/For/Loading/Errored/lazy 13.40 KB 0 B 0 B −69 B 13.33 KB ⚠️ over by 66 B, 89 B minified headroom lazy-page.js 0.04 KB
app: CSR, observe tier (same app on the observe artifacts) 15.13 KB 0 B 0 B −70 B 15.08 KB ⚠️ over by 53 B, 90 B minified headroom lazy-page.js 0.04 KB
app: CSR, observe tier + attribution engine enabled 29.69 KB 0 B 0 B −50 B 29.64 KB ⚠️ over by 51 B, 70 B minified headroom lazy-page.js 0.04 KB
app: compiled floor (one template, one text hole, one delegated click) 9.99 KB 0 B 0 B −85 B 9.93 KB ⚠️ over by 65 B, 105 B minified headroom
app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store) 26.30 KB 0 B 0 B −849 B 26.23 KB ⚠️ over by 69 B, 869 B minified headroom stats.js 0.18 KB
app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable) 32.25 KB 0 B 0 B −872 B 32.23 KB ⚠️ over by 23.000000000003638 B, 892 B minified headroom stats.js 0.20 KB
frames: eager client consumer (frames client + transport, lazy codec) 11.75 KB 0 B 0 B 0 B 11.76 KB ✅
page: base server components (hydrating + dynamic + frames + sf reference) 33.34 KB 0 B 0 B 0 B 33.36 KB ✅ assets.js 0.78 KB, decode.js 7.09 KB, lazy-page.js 0.04 KB, regions.js 1.06 KB, trace.js 9.06 KB, wire.js 3.47 KB
page: live server components (base + live/GET + action + isPending/latest) 37.71 KB 0 B 0 B 0 B 37.72 KB ✅ assets.js 0.78 KB, decode.js 7.09 KB, lazy-page.js 0.04 KB, regions.js 1.06 KB, trace.js 9.04 KB, wire.js 3.47 KB
page: compiled base server components (the base page as JSX: templates with class/style/attributes/events, For/Show; no spread) 35.32 KB 0 B 0 B 0 B 35.34 KB ✅ assets.js 0.78 KB, decode.js 7.09 KB, regions.js 1.06 KB, sc-comments.js 0.20 KB, trace.js 9.11 KB, wire.js 3.47 KB
page: compiled live server components (the compiled base page + live/GET + action + isPending/latest) 41.55 KB 0 B 0 B 0 B 41.56 KB ✅ eager (counted): web.js 22.16 KB; assets.js 0.78 KB, decode.js 7.09 KB, regions.js 1.06 KB, sc-comments.js 0.19 KB, trace.js 9.12 KB, wire.js 3.47 KB
page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate) 40.83 KB 0 B 0 B −1 B 40.78 KB ⚠️ over by 54 B, 21 B minified headroom assets.js 0.78 KB, decode.js 7.09 KB, lazy-page.js 0.04 KB, regions.js 1.06 KB, server.js 1.03 KB, serverForms.js 3.58 KB, trace.js 9.12 KB, wire.js 3.48 KB
page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate) 47.10 KB 0 B 0 B 0 B 47.11 KB ✅ eager (counted): client.js 27.23 KB; assets.js 0.78 KB, decode.js 7.09 KB, lazy-page.js 0.04 KB, regions.js 1.07 KB, server.js 1.03 KB, serverForms.js 3.32 KB, trace.js 9.11 KB, wire.js 3.49 KB
server: floor (getRequestEvent + isServer) 1.27 KB 0 B 0 B −69 B 1.34 KB ✅
server: renderToString (the server-render floor) 22.79 KB 0 B 0 B 0 B 22.80 KB ✅

⚠️ Over the brotli cap within the minified allowance (passes)

  • signals: + createStore: over brotli cap by 19 B; minified 44,488 B vs 45,330 B recorded with the cap (−842 B) — 862 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • signals: + isPending/latest: over brotli cap by 21 B; minified 28,744 B vs 28,735 B recorded with the cap (+9 B) — 11 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • signals: + until/refresh (truth readers, no optimism): over brotli cap by 15.99999999999909 B; minified 21,897 B vs 21,946 B recorded with the cap (−49 B) — 69 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • app: render + one signal (the simple-app floor): over brotli cap by 46 B; minified 26,944 B vs 27,029 B recorded with the cap (−85 B) — 105 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • app: hydrating (no stores) with Show/For/Loading/Errored/lazy: over brotli cap by 86 B; minified 53,596 B vs 53,598 B recorded with the cap (−2 B) — 22 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • app: hydrating + every store primitive family: over brotli cap by 192 B; minified 95,089 B vs 95,837 B recorded with the cap (−748 B) — 768 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • app: CSR with Show/For/Loading/Errored/lazy: over brotli cap by 66 B; minified 37,183 B vs 37,252 B recorded with the cap (−69 B) — 89 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • app: CSR, observe tier (same app on the observe artifacts): over brotli cap by 53 B; minified 42,139 B vs 42,209 B recorded with the cap (−70 B) — 90 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • app: CSR, observe tier + attribution engine enabled: over brotli cap by 51 B; minified 88,477 B vs 88,527 B recorded with the cap (−50 B) — 70 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • app: compiled floor (one template, one text hole, one delegated click): over brotli cap by 65 B; minified 27,475 B vs 27,560 B recorded with the cap (−85 B) — 105 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store): over brotli cap by 69 B; minified 80,478 B vs 81,327 B recorded with the cap (−849 B) — 869 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable): over brotli cap by 23.000000000003638 B; minified 101,020 B vs 101,892 B recorded with the cap (−872 B) — 892 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate): over brotli cap by 54 B; minified 125,363 B vs 125,364 B recorded with the cap (−1 B) — 21 B of the 20 B minified allowance left; +0 B minified over this PR's base

Bundled with Rolldown (what Vite ships), brotli q11, decimal KB. A scenario fails only when it is over its brotli cap and its minified size is more than 20 B over the minified recorded with the cap; over the cap within that allowance is brotli layout noise and passes with a warning. Caps and their recorded minified in scripts/size/scenarios.js; the floor and page caps in floor-caps.json are frozen (lower only, or Size-Exception: in the PR body). npm run ratchet lowers caps per RC; it never raises one (scripts/size/README.md).

Copy link
Copy Markdown
Member Author

The Size / check job is red, but the failure isn't from this PR. The failing scenarios are "page: live server components" and "page: live + router". The log reports "+0 B minified over this PR's base" for every one of them, and this diff touches only the Rust compiler crate, so no bundle changes. Size is also failing on next's own push runs (0856fee and 95dd480). I know of no fix to port yet, and re-running wouldn't help because the result is deterministic. It should go green once next's caps are brought back under or raised.


Generated by Claude Code

`src/tsrx/style/` becomes `src/style/`, exported as `solidjs_compiler::style`
in every build: the engine never depended on TSRX, and a frontend that does
not use TSRX should not need the `tsrx` feature to reach it. The TSRX
projection and lowering now import it from `crate::style`. No behaviour
change; the engine's unit tests now also run without the `tsrx` feature.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01ViozQ3kVSofMUQ3fwpyq5o
@coveralls

coveralls commented Oct 26, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 40292136222

Warning

No base build found for commit fbd375d on next.
Coverage changes can't be calculated without a base build.
If a base build is processing, this comment will update automatically when it completes.

Coverage: 77.855%

Details

  • Patch coverage: No coverable lines changed in this PR.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

Requires a base build to compare against. How to fix this →


Coverage Stats

Coverage Status
Relevant Lines: 1366
Covered Lines: 1122
Line Coverage: 82.14%
Relevant Branches: 1068
Covered Branches: 773
Branch Coverage: 72.38%
Branches in Coverage %: Yes
Coverage Strength: 27.68 hits per line

💛 - Coveralls

@ryansolid ryansolid changed the title feat(compiler): export the TSRX scoped-CSS engine from the Rust core feat(compiler): export the scoped-CSS engine from the Rust core, independent of TSRX Oct 26, 2026
@codspeed

codspeed Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

1 warning

⚠️ 4 benchmarks are dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

✅ 196 untouched benchmarks
⏩ 5 skipped benchmarks1


Comparing compiler-export-compile-style (4d271cf) with next (fbd375d)

Open in CodSpeed

Footnotes

  1. 5 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

Copy link
Copy Markdown
Member Author

The CodSpeed Performance Analysis failure doesn't come from this PR. The only regression it reports is packages/signals mount-rows (−12.6%), and #4290 shows the same −12.9% even though it shares no code with this PR. Both PRs change only the Rust compiler crate and leave its output unchanged. CodSpeed also warns that base and head ran on different runner types. I'm not changing anything here; if a re-run still flags it, it can be acknowledged on CodSpeed.


Generated by Claude Code

@ryansolid
ryansolid merged commit 838b74b into next Oct 26, 2026
15 checks passed
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.

3 participants