@tenphi/tasty 3.3.1 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +147 -129
- package/dist/astro-ib7E7V4Y.js +307 -0
- package/dist/astro-ib7E7V4Y.js.map +1 -0
- package/dist/{babel-BUQGeOXA.d.ts → babel-DWCsafYw.d.ts} +2 -2
- package/dist/{collector-DUaHCcTS.d.ts → collector-BTVXj2hS.d.ts} +21 -3
- package/dist/{collector-BKqNBmzA.js → collector-C6TtL8HJ.js} +36 -13
- package/dist/collector-C6TtL8HJ.js.map +1 -0
- package/dist/{config-BCdCTIED.js → config-B3gPdCqd.js} +344 -209
- package/dist/config-B3gPdCqd.js.map +1 -0
- package/dist/{config-YsxGv4tq.d.ts → config-LfIDmVHx.d.ts} +132 -32
- package/dist/core/index.d.ts +5 -5
- package/dist/core/index.js +6 -6
- package/dist/{core-wxP3GHQu.js → core-Bq7w2kti.js} +219 -98
- package/dist/core-Bq7w2kti.js.map +1 -0
- package/dist/{css-writer-D64NY9AX.js → css-writer-Bgowobbx.js} +3 -3
- package/dist/{css-writer-D64NY9AX.js.map → css-writer-Bgowobbx.js.map} +1 -1
- package/dist/{format-rules-Bo_e2u7r.js → format-rules-rCZ37rqY.js} +2 -2
- package/dist/format-rules-rCZ37rqY.js.map +1 -0
- package/dist/{hydrate-CMKOuKAx.js → hydrate-CNOmZprz.js} +3 -3
- package/dist/{hydrate-CMKOuKAx.js.map → hydrate-CNOmZprz.js.map} +1 -1
- package/dist/{index-DhhUI0yi.d.ts → index-Bm9ltvdH.d.ts} +45 -16
- package/dist/{index-Cd45t5NM.d.ts → index-BmogEzTo.d.ts} +69 -15
- package/dist/index.d.ts +5 -5
- package/dist/index.js +9 -8
- package/dist/index.js.map +1 -1
- package/dist/{keyframes-D737PShJ.js → keyframes-DE-OE76F.js} +132 -21
- package/dist/keyframes-DE-OE76F.js.map +1 -0
- package/dist/{merge-styles-CU7JbEwg.d.ts → merge-styles-BcLl1u_d.d.ts} +2 -2
- package/dist/{merge-styles-CUIQcs5v.js → merge-styles-DuoZEsm9.js} +2 -2
- package/dist/{merge-styles-CUIQcs5v.js.map → merge-styles-DuoZEsm9.js.map} +1 -1
- package/dist/{resolve-recipes-Df1Ta-Q0.js → resolve-recipes-H9NqOQuP.js} +3 -3
- package/dist/{resolve-recipes-Df1Ta-Q0.js.map → resolve-recipes-H9NqOQuP.js.map} +1 -1
- package/dist/ssr/astro-client.js +1 -1
- package/dist/ssr/astro-middleware-extract-static.d.ts +11 -0
- package/dist/ssr/astro-middleware-extract-static.js +9 -0
- package/dist/ssr/astro-middleware-extract-static.js.map +1 -0
- package/dist/ssr/astro-middleware-extract.d.ts +11 -0
- package/dist/ssr/astro-middleware-extract.js +9 -0
- package/dist/ssr/astro-middleware-extract.js.map +1 -0
- package/dist/ssr/astro-middleware-static.d.ts +3 -1
- package/dist/ssr/astro-middleware-static.js +1 -1
- package/dist/ssr/astro-middleware.d.ts +3 -1
- package/dist/ssr/astro-middleware.js +1 -1
- package/dist/ssr/astro.d.ts +37 -3
- package/dist/ssr/astro.js +1 -156
- package/dist/ssr/index.d.ts +2 -2
- package/dist/ssr/index.js +3 -3
- package/dist/ssr/index.js.map +1 -1
- package/dist/ssr/next.d.ts +1 -1
- package/dist/ssr/next.js +4 -4
- package/dist/static/index.d.ts +2 -2
- package/dist/static/index.js +1 -1
- package/dist/zero/babel.d.ts +1 -1
- package/dist/zero/babel.js +4 -4
- package/dist/zero/babel.js.map +1 -1
- package/dist/zero/index.d.ts +1 -1
- package/dist/zero/index.js +1 -1
- package/dist/zero/next.d.ts +1 -1
- package/docs/README.md +13 -8
- package/docs/adoption.md +56 -31
- package/docs/ai-agents.md +78 -77
- package/docs/comparison.md +69 -40
- package/docs/configuration.md +97 -87
- package/docs/debug.md +4 -2
- package/docs/design-system.md +41 -22
- package/docs/dsl.md +225 -195
- package/docs/getting-started.md +60 -39
- package/docs/injector.md +63 -26
- package/docs/methodology.md +42 -36
- package/docs/migration-v3.md +49 -49
- package/docs/plugins.md +37 -33
- package/docs/react-api.md +65 -44
- package/docs/runtime-benchmarks.md +178 -0
- package/docs/ssr.md +99 -61
- package/docs/styles.md +1 -1
- package/docs/tasty-static.md +137 -104
- package/package.json +26 -8
- package/dist/collector-BKqNBmzA.js.map +0 -1
- package/dist/config-BCdCTIED.js.map +0 -1
- package/dist/core-wxP3GHQu.js.map +0 -1
- package/dist/format-rules-Bo_e2u7r.js.map +0 -1
- package/dist/keyframes-D737PShJ.js.map +0 -1
- package/dist/ssr/astro.js.map +0 -1
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Runtime Benchmarks
|
|
2
|
+
|
|
3
|
+
Tasty keeps its performance claims in reproducible benchmarks rather than
|
|
4
|
+
combining unlike measurements into one score. The repository measures three
|
|
5
|
+
different costs:
|
|
6
|
+
|
|
7
|
+
1. Style parsing and generation in Node.
|
|
8
|
+
2. The React overhead of an empty `tasty({})` wrapper.
|
|
9
|
+
3. Cold browser generation and injection compared with equivalent CSS that is
|
|
10
|
+
already on the page.
|
|
11
|
+
|
|
12
|
+
These are focused microbenchmarks, not page-load or interaction scores. Run
|
|
13
|
+
them several times on an otherwise idle machine and use a production profile
|
|
14
|
+
to decide whether any cost matters in an application.
|
|
15
|
+
|
|
16
|
+
## Reproducing the Results
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm bench
|
|
20
|
+
pnpm bench:overhead
|
|
21
|
+
pnpm bench:injection
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`pnpm bench` runs the core pipeline benchmarks in Node. The other two commands
|
|
25
|
+
use production code paths in headless Chromium. The first checkout may require
|
|
26
|
+
`pnpm test:setup` to download Chromium.
|
|
27
|
+
|
|
28
|
+
Run the Node and browser suites separately so they do not compete for CPU. The
|
|
29
|
+
browser timer has 0.1 ms resolution, so both browser benchmarks perform many
|
|
30
|
+
matched operations per sample and divide the absolute difference by the number
|
|
31
|
+
of elements or rules. Machine load, browser versions, and CPU power will move
|
|
32
|
+
the results.
|
|
33
|
+
|
|
34
|
+
## Core Style Pipeline
|
|
35
|
+
|
|
36
|
+
The following numbers are single-call throughput measured on an Apple M1 Max
|
|
37
|
+
with Node 22:
|
|
38
|
+
|
|
39
|
+
| Operation | ops/sec | Latency (mean) |
|
|
40
|
+
| ----------------------------------------------------------- | -------------------: | -------------: |
|
|
41
|
+
| `renderStyles` — 5 flat properties (cold) | ~60,000 | ~17 us |
|
|
42
|
+
| `renderStyles` — state map with media/hover/modifier (cold) | ~18,500 | ~54 us |
|
|
43
|
+
| `renderStyles` — same styles (cached) | ~5,800,000 | ~0.17 us |
|
|
44
|
+
| `parseStateKey` — simple key like `:hover` (cold) | ~790,000 | ~1.3 us |
|
|
45
|
+
| `parseStateKey` — complex OR/AND/NOT key (cold) | ~140,000 | ~7 us |
|
|
46
|
+
| `parseStateKey` — any key (cached) | ~3,400,000–8,300,000 | ~0.1–0.3 us |
|
|
47
|
+
| `parseStyle` — value tokens like `2x 4x` (cold) | ~344,000 | ~2.9 us |
|
|
48
|
+
| `parseStyle` — color tokens (cold) | ~567,000 | ~1.8 us |
|
|
49
|
+
| `parseStyle` — any value (cached) | ~15,250,000 | ~0.07 us |
|
|
50
|
+
|
|
51
|
+
“Cold” cases use unique inputs to bypass the relevant caches. Cached cases
|
|
52
|
+
reuse one input and measure the LRU hot path. Expect roughly ±10% between runs.
|
|
53
|
+
These benchmarks do not include React, DOM work, stylesheet injection, style
|
|
54
|
+
resolution, layout, or paint.
|
|
55
|
+
|
|
56
|
+
The benchmark sources are colocated with the code they exercise:
|
|
57
|
+
[`pipeline.bench.ts`](../src/pipeline/pipeline.bench.ts),
|
|
58
|
+
[`parseStateKey.bench.ts`](../src/pipeline/parseStateKey.bench.ts), and the
|
|
59
|
+
parser benchmark files under [`src/parser`](../src/parser).
|
|
60
|
+
|
|
61
|
+
## Empty Wrapper Overhead
|
|
62
|
+
|
|
63
|
+
Skipping the style pipeline does not make a `tasty()` component free. Even
|
|
64
|
+
`tasty({})` is a React component between its parent and the host element. React
|
|
65
|
+
tracks another fiber, and Tasty still processes and forwards the element's
|
|
66
|
+
props.
|
|
67
|
+
|
|
68
|
+
[`tasty-overhead.bench.tsx`](../src/tasty-overhead.bench.tsx) compares 10,000
|
|
69
|
+
raw `<div className>` siblings with 10,000 instances of one module-scoped
|
|
70
|
+
`tasty({})` component. Both receive the same props, and the benchmark fails if
|
|
71
|
+
they do not produce equivalent DOM.
|
|
72
|
+
|
|
73
|
+
The benchmark uses production React in headless Chromium. The factory is
|
|
74
|
+
created and its empty class-name cache is warmed before timing, so factory
|
|
75
|
+
creation, style generation, and injection are excluded. A detached container
|
|
76
|
+
excludes layout, paint, and stylesheet matching. Every commit is wrapped in
|
|
77
|
+
`flushSync`, keeping its synchronous reconciliation and commit inside the
|
|
78
|
+
sample. This does not estimate React's concurrent scheduling latency.
|
|
79
|
+
|
|
80
|
+
On an Apple M3 Pro with React 19.2.4 and Chromium 151, three consecutive runs
|
|
81
|
+
produced these ranges:
|
|
82
|
+
|
|
83
|
+
| Work on 10,000 siblings | Raw elements | `tasty({})` | Extra per wrapped element |
|
|
84
|
+
| ----------------------------------- | -----------: | -----------: | ------------------------: |
|
|
85
|
+
| Mount + remove | 4.5–4.9 ms | 14.0–14.9 ms | 0.95–1.00 us |
|
|
86
|
+
| Rerender, same host props | 1.3–1.4 ms | 10.9–11.4 ms | 0.96–1.00 us |
|
|
87
|
+
| Rerender, change one host attribute | 2.8–3.4 ms | 15.0–15.7 ms | 1.21–1.27 us |
|
|
88
|
+
|
|
89
|
+
The useful result is the raw/Tasty time difference divided by 10,000, not the
|
|
90
|
+
ratio between the two times. The ratio becomes large because the raw baseline
|
|
91
|
+
is tiny. In this synthetic workload, an empty wrapper adds roughly 1 us per
|
|
92
|
+
participating element, or 1.2–1.3 us when React also changes a DOM attribute.
|
|
93
|
+
|
|
94
|
+
This is the floor Tasty consumes when it has no styling job. It is not a
|
|
95
|
+
page-level score. Real trees include application components, effects, layout,
|
|
96
|
+
paint, and usually far fewer simultaneous styled-element updates. The benchmark
|
|
97
|
+
also does not measure retained memory; that requires a matched-tree heap
|
|
98
|
+
snapshot experiment with controlled garbage collection.
|
|
99
|
+
|
|
100
|
+
## Cold Generation and Injection
|
|
101
|
+
|
|
102
|
+
[`tasty-injection.bench.ts`](../src/tasty-injection.bench.ts) measures the extra
|
|
103
|
+
work when Tasty must generate and inject CSS that an otherwise equivalent page
|
|
104
|
+
already has. It does not compare different stylesheet insertion techniques.
|
|
105
|
+
|
|
106
|
+
The benchmark covers two useful workloads:
|
|
107
|
+
|
|
108
|
+
- **One new rule:** add one rule to an existing stylesheet, append its one
|
|
109
|
+
element, and immediately read its computed style. Each timed sample performs
|
|
110
|
+
50 independent one-rule transactions and reports their total; dividing the
|
|
111
|
+
raw/Tasty difference by 50 produces a stable per-rule result despite
|
|
112
|
+
Chromium's 0.1 ms timer resolution.
|
|
113
|
+
- **1,000 new rules together:** generate and insert all 1,000 rules into one
|
|
114
|
+
stylesheet, append all 1,000 elements, then read every computed color without
|
|
115
|
+
another write in between. This gives the browser one style-resolution
|
|
116
|
+
boundary for the group.
|
|
117
|
+
|
|
118
|
+
For every transaction, the existing-CSS control has the equivalent stylesheet
|
|
119
|
+
parsed, adopted, and attached before timing. The runtime root has a Tasty
|
|
120
|
+
stylesheet pre-created with an unrelated sentinel rule, but not the measured
|
|
121
|
+
rules. Both paths perform the same class assignment, DOM commit, and
|
|
122
|
+
computed-style reads. Only the runtime path calls `computeStyles()` and inserts
|
|
123
|
+
the new rules.
|
|
124
|
+
|
|
125
|
+
Preparation and cleanup happen outside the sample timer. Every runtime style
|
|
126
|
+
value is unique within a cycle, the relevant caches are cleared between cycles,
|
|
127
|
+
and a guard verifies that both paths resolve to the same color. React and the
|
|
128
|
+
`tasty()` wrapper are absent so their independently measured costs do not enter
|
|
129
|
+
the result. Pre-creating both stylesheets also excludes one-time sheet creation
|
|
130
|
+
and adoption from the subtraction.
|
|
131
|
+
|
|
132
|
+
On an Apple M3 Pro with Chromium 151, three consecutive runs produced these
|
|
133
|
+
ranges:
|
|
134
|
+
|
|
135
|
+
| Workload | CSS already present | Tasty runtime | Incremental Tasty cost |
|
|
136
|
+
| ---------------------------------------------------- | ------------------: | -------------: | ---------------------: |
|
|
137
|
+
| One new rule + immediate resolution, per transaction | 2.8–3.3 us | 110.3–113.8 us | 107.3–111.0 us |
|
|
138
|
+
| 1,000 new rules + one resolution | 1.86–2.06 ms | 9.01–9.98 ms | 7.13–7.92 ms |
|
|
139
|
+
| 1,000-rule workload, incremental cost per rule | — | — | 7.1–7.9 us |
|
|
140
|
+
|
|
141
|
+
Directly compared, injecting 1,000 rules before one resolution boundary cost
|
|
142
|
+
about 66–71 times as much in total as injecting one rule and resolving it—not
|
|
143
|
+
1,000 times as much. Its average incremental cost per rule was about 14–16
|
|
144
|
+
times lower. This is the same Tasty generation and injection path in both cases;
|
|
145
|
+
the group amortizes fixed transaction work and lets the browser resolve all the
|
|
146
|
+
stylesheet writes together.
|
|
147
|
+
|
|
148
|
+
The subtraction is the meaningful result. It includes Tasty's cold style
|
|
149
|
+
generation, cache and injector bookkeeping, rule insertion, and any additional
|
|
150
|
+
style invalidation exposed by that workload's resolution boundary. It does not
|
|
151
|
+
pretend to isolate `insertRule()` from the system that calls it.
|
|
152
|
+
|
|
153
|
+
This is a deliberately cold workload. Reused styles resolve from cache and do
|
|
154
|
+
not inject another rule. Different rule complexity, DOM shape, stylesheet size,
|
|
155
|
+
browser, and hardware will change the number. The single-rule and 1,000-rule
|
|
156
|
+
results are not interchangeable: the first crosses the injection-to-resolution
|
|
157
|
+
boundary once per rule, while the second lets the browser resolve 1,000 writes
|
|
158
|
+
together. Because the same resolution pattern is present in each workload's
|
|
159
|
+
control, the difference answers the narrower delivery question: how much extra
|
|
160
|
+
work did Tasty perform when the same CSS was not already there?
|
|
161
|
+
|
|
162
|
+
## Reading the Results Together
|
|
163
|
+
|
|
164
|
+
Do not add all three benchmark numbers to estimate an application blindly.
|
|
165
|
+
They describe different paths:
|
|
166
|
+
|
|
167
|
+
- A stable `tasty()` factory can skip the style pipeline on later renders, but
|
|
168
|
+
its React wrapper still participates in reconciliation.
|
|
169
|
+
- A cached style avoids cold parsing and generation and does not insert a new
|
|
170
|
+
rule.
|
|
171
|
+
- A genuinely new style pays generation and injection once, then becomes
|
|
172
|
+
reusable.
|
|
173
|
+
- Browser style resolution, layout, and paint depend on the actual document and
|
|
174
|
+
need application-level profiling.
|
|
175
|
+
|
|
176
|
+
The practical optimization target is therefore repeated work: keep style input
|
|
177
|
+
stable when possible, reuse generated chunks, and generate CSS at build or
|
|
178
|
+
server time when runtime flexibility is unnecessary.
|
package/docs/ssr.md
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
# Server-Side Rendering (SSR)
|
|
2
2
|
|
|
3
|
-
Tasty supports server-side rendering with zero-cost client hydration. This does **not** introduce a separate styling engine:
|
|
3
|
+
Tasty supports server-side rendering with zero-cost client hydration. This does **not** introduce a separate styling engine: `tasty()` uses the same rendering pipeline on the server and in the browser, while the SSR integrations add server-side CSS collection and client-side cache hydration. Your existing `tasty()` components work unchanged, and SSR remains opt-in with no per-component modifications. For the broader docs map, see the [Docs Hub](README.md).
|
|
4
|
+
|
|
5
|
+
## Zero-runtime terminology
|
|
6
|
+
|
|
7
|
+
Zero-runtime delivery is an outcome, not an alias for `tastyStatic()`. When `tasty()` components render only on the server, their CSS is delivered with the HTML and no Tasty styling runtime is shipped to the browser. Astro's `tastyIntegration({ islands: false })` is the explicit integration for this setup. Server-only Next.js React Server Components follow the same architecture, although you should verify the generated output for your deployment.
|
|
8
|
+
|
|
9
|
+
`tastyStatic()` reaches the same client-side outcome by extracting CSS during the build instead of during React rendering. Use it when extraction must happen before rendering or when the consumer is not React; see [Build-Time Extraction](tasty-static.md).
|
|
4
10
|
|
|
5
11
|
---
|
|
6
12
|
|
|
7
13
|
## Requirements
|
|
8
14
|
|
|
9
|
-
| Dependency | Version | Required for
|
|
10
|
-
|
|
11
|
-
| `react`
|
|
12
|
-
| `next`
|
|
13
|
-
| Node.js
|
|
15
|
+
| Dependency | Version | Required for |
|
|
16
|
+
| ---------- | ------- | ----------------------------------------------------------------------------------------------- |
|
|
17
|
+
| `react` | >= 18 | All SSR entry points (matches the current peer dependency of `@tenphi/tasty`) |
|
|
18
|
+
| `next` | >= 13 | Next.js integration (`@tenphi/tasty/ssr/next`) — App Router with `useServerInsertedHTML` |
|
|
19
|
+
| Node.js | >= 20 | Generic / streaming SSR (`@tenphi/tasty/ssr`) — uses `node:async_hooks` for `AsyncLocalStorage` |
|
|
14
20
|
|
|
15
21
|
The Astro integration (`@tenphi/tasty/ssr/astro`) has no additional dependencies beyond `react`.
|
|
16
22
|
|
|
@@ -122,13 +128,15 @@ The nonce is automatically applied to all `<style>` and `<script>` tags injected
|
|
|
122
128
|
|
|
123
129
|
## Astro
|
|
124
130
|
|
|
125
|
-
Tasty offers
|
|
131
|
+
Tasty offers several levels of Astro integration. Choose the one that matches your needs:
|
|
126
132
|
|
|
127
|
-
| Setup
|
|
128
|
-
|
|
129
|
-
| Zero setup
|
|
130
|
-
| `tastyIntegration({ islands: false })`
|
|
131
|
-
| `tastyIntegration()`
|
|
133
|
+
| Setup | Config needed | Deduplication | Hooks work | Client JS |
|
|
134
|
+
| --------------------------------------------------------- | ------------- | ----------------------------- | ---------------------- | -------------- |
|
|
135
|
+
| Zero setup | None | Per render tree | Yes (within each tree) | None |
|
|
136
|
+
| `tastyIntegration({ islands: false })` | One line | Cross-tree | Yes | None |
|
|
137
|
+
| `tastyIntegration()` | One line | Cross-tree | Yes | Auto-hydration |
|
|
138
|
+
| `tastyIntegration({ css: { mode: 'extract' } })` | One line | Cross-tree and cross-page | Yes | Auto-hydration |
|
|
139
|
+
| `tastyIntegration({ islands: false, css: { mode: 'extract' } })` | One line | Cross-tree and cross-page | Yes | None |
|
|
132
140
|
|
|
133
141
|
### Zero setup (static pages)
|
|
134
142
|
|
|
@@ -225,6 +233,39 @@ export default defineConfig({
|
|
|
225
233
|
|
|
226
234
|
This gives the same middleware deduplication and hook support, but ships zero client-side JavaScript. No class-list `<script>` is emitted.
|
|
227
235
|
|
|
236
|
+
#### Build-wide CSS extraction
|
|
237
|
+
|
|
238
|
+
Static Astro builds can move repeated Tasty CSS into a content-hashed,
|
|
239
|
+
browser-cacheable asset:
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
export default defineConfig({
|
|
243
|
+
integrations: [
|
|
244
|
+
react(),
|
|
245
|
+
tastyIntegration({
|
|
246
|
+
islands: false,
|
|
247
|
+
css: {
|
|
248
|
+
mode: 'extract',
|
|
249
|
+
},
|
|
250
|
+
}),
|
|
251
|
+
],
|
|
252
|
+
});
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
`css.mode` defaults to `'inline'`, so existing projects keep their current
|
|
256
|
+
output. Extraction requires Astro 5 or newer and only applies to prerendered
|
|
257
|
+
production pages. Development, preview-time SSR, and on-demand routes continue
|
|
258
|
+
to receive the normal inline `<style data-tasty-ssr>` output.
|
|
259
|
+
|
|
260
|
+
Extraction moves the largest common block whose position can be preserved on
|
|
261
|
+
every generated page into one stylesheet. Page-only component, global, and raw
|
|
262
|
+
CSS stays inline, so unvisited routes do not receive unrelated styles.
|
|
263
|
+
|
|
264
|
+
The asset is written under Astro's configured `build.assets` directory (for
|
|
265
|
+
example, `/_astro/tasty.a1b2c3.css`). Links include the configured Astro
|
|
266
|
+
`base`, so nested routes do not need relative-path handling. The content hash
|
|
267
|
+
and output are deterministic for identical builds.
|
|
268
|
+
|
|
228
269
|
### Manual middleware (advanced)
|
|
229
270
|
|
|
230
271
|
If you need to compose Tasty's middleware with other middleware (e.g., via `sequence()`), use `tastyMiddleware()` directly:
|
|
@@ -234,10 +275,7 @@ If you need to compose Tasty's middleware with other middleware (e.g., via `sequ
|
|
|
234
275
|
import { sequence } from 'astro:middleware';
|
|
235
276
|
import { tastyMiddleware } from '@tenphi/tasty/ssr/astro';
|
|
236
277
|
|
|
237
|
-
export const onRequest = sequence(
|
|
238
|
-
tastyMiddleware(),
|
|
239
|
-
myOtherMiddleware,
|
|
240
|
-
);
|
|
278
|
+
export const onRequest = sequence(tastyMiddleware(), myOtherMiddleware);
|
|
241
279
|
```
|
|
242
280
|
|
|
243
281
|
For island hydration with manual middleware, import the client module in a shared entry point or in each island:
|
|
@@ -260,10 +298,14 @@ Astro's `@astrojs/react` renderer calls `renderToString()` for each React compon
|
|
|
260
298
|
- **Static components** (no `client:*`): Styles are collected during `renderToString` and injected into `</head>` as a single `<style>` tag. No JavaScript is shipped.
|
|
261
299
|
- **Islands** (`client:load`, `client:visible`, etc.): Styles are collected during SSR the same way. On the client, the hydration script (auto-injected by `tastyIntegration()` or manually via `@tenphi/tasty/ssr/astro-client`) reads the class list from `window.__TASTY__` and pre-populates the injector's rules map. The island's `computeStyles()` calls see the class names as already registered and skip the pipeline during hydration.
|
|
262
300
|
- The middleware reads the full response body, then injects the collected CSS into `</head>` before sending the final HTML.
|
|
301
|
+
- In extraction mode, prerendered responses also carry temporary structured
|
|
302
|
+
artifact metadata. `astro:build:done` uses those collector-provided
|
|
303
|
+
boundaries to write the shared asset and rewrite generated HTML, then removes
|
|
304
|
+
the metadata. CSS is never split on newlines or parsed heuristically.
|
|
263
305
|
|
|
264
306
|
### CSP nonce
|
|
265
307
|
|
|
266
|
-
Call `configure({ nonce: '...' })` before any rendering happens. The middleware reads the nonce and applies it to injected `<style>` and `<script>` tags.
|
|
308
|
+
Call `configure({ nonce: '...' })` before any rendering happens. The middleware reads the nonce and applies it to injected `<style>` and `<script>` tags. In extraction mode, page-local inline style tags and the external stylesheet link retain the nonce.
|
|
267
309
|
|
|
268
310
|
---
|
|
269
311
|
|
|
@@ -285,9 +327,7 @@ import { hydrateRoot } from 'react-dom/client';
|
|
|
285
327
|
|
|
286
328
|
const collector = createServerStyleCollector();
|
|
287
329
|
|
|
288
|
-
const html = await runWithCollector(collector, () =>
|
|
289
|
-
renderToString(<App />)
|
|
290
|
-
);
|
|
330
|
+
const html = await runWithCollector(collector, () => renderToString(<App />));
|
|
291
331
|
|
|
292
332
|
const css = collector.getCSS();
|
|
293
333
|
const classNames = collector.getRenderedClassNames();
|
|
@@ -338,7 +378,7 @@ const stream = await runWithCollector(collector, () =>
|
|
|
338
378
|
`<script>(window.__TASTY__=window.__TASTY__||[]).push(${JSON.stringify(classNames)})</script>`,
|
|
339
379
|
);
|
|
340
380
|
},
|
|
341
|
-
})
|
|
381
|
+
}),
|
|
342
382
|
);
|
|
343
383
|
```
|
|
344
384
|
|
|
@@ -348,13 +388,13 @@ const stream = await runWithCollector(collector, () =>
|
|
|
348
388
|
|
|
349
389
|
### Entry points
|
|
350
390
|
|
|
351
|
-
| Import path
|
|
352
|
-
|
|
353
|
-
| `@tenphi/tasty/ssr`
|
|
354
|
-
| `@tenphi/tasty/ssr/next`
|
|
355
|
-
| `@tenphi/tasty/ssr/astro`
|
|
356
|
-
| `@tenphi/tasty/ssr/astro-client`
|
|
357
|
-
| `@tenphi/tasty/ssr/astro-middleware`<br>`@tenphi/tasty/ssr/astro-middleware-static` | Astro: the middleware entrypoints `tastyIntegration()` registers via `addMiddleware()`. Exported so Astro can resolve them by specifier; you should not import them. For manual setups use `tastyMiddleware()`. |
|
|
391
|
+
| Import path | Description |
|
|
392
|
+
| ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
393
|
+
| `@tenphi/tasty/ssr` | Core SSR API: `ServerStyleCollector`, `createServerStyleCollector`, `runWithCollector`, `hydrateTastyClasses` |
|
|
394
|
+
| `@tenphi/tasty/ssr/next` | Next.js App Router: `TastyRegistry` component |
|
|
395
|
+
| `@tenphi/tasty/ssr/astro` | Astro: `tastyIntegration`, `tastyMiddleware` |
|
|
396
|
+
| `@tenphi/tasty/ssr/astro-client` | Astro: client-side cache hydration (auto-injected by integration, or import manually) |
|
|
397
|
+
| `@tenphi/tasty/ssr/astro-middleware`<br>`@tenphi/tasty/ssr/astro-middleware-static`<br>`@tenphi/tasty/ssr/astro-middleware-extract`<br>`@tenphi/tasty/ssr/astro-middleware-extract-static` | Astro: the middleware entrypoints `tastyIntegration()` registers via `addMiddleware()`. Exported so Astro can resolve them by specifier; you should not import them. For manual setups use `tastyMiddleware()`. |
|
|
358
398
|
|
|
359
399
|
### `ServerStyleCollector`
|
|
360
400
|
|
|
@@ -362,47 +402,47 @@ Server-safe style collector. One instance per request.
|
|
|
362
402
|
|
|
363
403
|
Constructor: `new ServerStyleCollector(namePrefix?)`, or use the `createServerStyleCollector(namePrefix?)` factory. The optional `namePrefix` overrides the value from `configure({ namePrefix })`; in normal usage you pass nothing and let the global config drive it. See [Configuration: Name prefix](configuration.md#name-prefix).
|
|
364
404
|
|
|
365
|
-
| Method
|
|
366
|
-
|
|
367
|
-
| `allocateClassName(cacheKey)`
|
|
368
|
-
| `collectChunk(cacheKey, className, rules)` | Record CSS rules for a chunk. Deduplicated by `cacheKey`.
|
|
369
|
-
| `collectKeyframes(name, css)`
|
|
370
|
-
| `allocateKeyframeName(providedName?)`
|
|
371
|
-
| `collectProperty(name, css)`
|
|
372
|
-
| `collectFontFace(key, css)`
|
|
373
|
-
| `collectCounterStyle(name, css)`
|
|
374
|
-
| `allocateCounterStyleName(providedName?)`
|
|
375
|
-
| `collectGlobalStyles(key, css)`
|
|
376
|
-
| `collectRawCSS(key, css)`
|
|
377
|
-
| `collectInternals()`
|
|
378
|
-
| `getCSS()`
|
|
379
|
-
| `flushCSS()`
|
|
380
|
-
| `getRenderedClassNames()`
|
|
405
|
+
| Method | Description |
|
|
406
|
+
| ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
407
|
+
| `allocateClassName(cacheKey)` | Allocate a deterministic, content-hashed class name for a cache key (e.g. `t1a2b3` with the default prefix). The same `cacheKey` always produces the same class name on server and client when both share the same `namePrefix`. Returns `{ className, isNewAllocation }`. |
|
|
408
|
+
| `collectChunk(cacheKey, className, rules)` | Record CSS rules for a chunk. Deduplicated by `cacheKey`. |
|
|
409
|
+
| `collectKeyframes(name, css)` | Record a `@keyframes` rule. Deduplicated by name. |
|
|
410
|
+
| `allocateKeyframeName(providedName?)` | Allocate a keyframe name. Returns `providedName` if given, otherwise generates one using `${namePrefix}k${counter}` (e.g. `tk0`, `tk1`, ...). |
|
|
411
|
+
| `collectProperty(name, css)` | Record a `@property` rule. Deduplicated by name. |
|
|
412
|
+
| `collectFontFace(key, css)` | Record a `@font-face` rule. Deduplicated by content hash. |
|
|
413
|
+
| `collectCounterStyle(name, css)` | Record a `@counter-style` rule. Deduplicated by name. |
|
|
414
|
+
| `allocateCounterStyleName(providedName?)` | Allocate a counter-style name. Returns `providedName` if given, otherwise generates one using `${namePrefix}c${counter}` (e.g. `tc0`, `tc1`, ...). |
|
|
415
|
+
| `collectGlobalStyles(key, css)` | Record global styles (from `useGlobalStyles`). Deduplicated by key. |
|
|
416
|
+
| `collectRawCSS(key, css)` | Record raw CSS text (from `useRawCSS`). Deduplicated by key. |
|
|
417
|
+
| `collectInternals()` | Collect internal `@property` rules, `:root` token defaults, `@font-face`, and `@counter-style` rules from the global config. Called automatically on first chunk collection; idempotent. |
|
|
418
|
+
| `getCSS()` | Get all collected CSS as a single string. For non-streaming SSR. |
|
|
419
|
+
| `flushCSS()` | Get only CSS collected since the last flush. For streaming SSR. |
|
|
420
|
+
| `getRenderedClassNames()` | Get the list of class names rendered so far. Serialized to `window.__TASTY__` for client hydration via `hydrateTastyClasses()`. |
|
|
381
421
|
|
|
382
422
|
### `TastyRegistry`
|
|
383
423
|
|
|
384
424
|
Next.js App Router component. Props:
|
|
385
425
|
|
|
386
|
-
| Prop
|
|
387
|
-
|
|
388
|
-
| `children`
|
|
389
|
-
| `transferCache` | `boolean`
|
|
426
|
+
| Prop | Type | Default | Description |
|
|
427
|
+
| --------------- | ----------- | -------- | ------------------------------------------------ |
|
|
428
|
+
| `children` | `ReactNode` | required | Application tree |
|
|
429
|
+
| `transferCache` | `boolean` | `true` | Embed cache state script for zero-cost hydration |
|
|
390
430
|
|
|
391
431
|
### `tastyIntegration(options?)`
|
|
392
432
|
|
|
393
433
|
Astro integration factory. Registers middleware and optionally injects client hydration.
|
|
394
434
|
|
|
395
|
-
| Option
|
|
396
|
-
|
|
397
|
-
| `islands` | `boolean` | `true`
|
|
435
|
+
| Option | Type | Default | Description |
|
|
436
|
+
| --------- | --------- | ------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
437
|
+
| `islands` | `boolean` | `true` | When `true`, injects client hydration script and enables `transferCache`. When `false`, no client JS is shipped. |
|
|
398
438
|
|
|
399
439
|
### `tastyMiddleware(options?)`
|
|
400
440
|
|
|
401
441
|
Astro middleware factory. Use for manual middleware composition.
|
|
402
442
|
|
|
403
|
-
| Option
|
|
404
|
-
|
|
405
|
-
| `transferCache` | `boolean` | `true`
|
|
443
|
+
| Option | Type | Default | Description |
|
|
444
|
+
| --------------- | --------- | ------- | --------------------------------------------- |
|
|
445
|
+
| `transferCache` | `boolean` | `true` | Embed cache state script for island hydration |
|
|
406
446
|
|
|
407
447
|
### `hydrateTastyClasses(classes?)`
|
|
408
448
|
|
|
@@ -424,11 +464,11 @@ The `TastyRegistry` or `tastyIntegration` is missing. Ensure your layout wraps t
|
|
|
424
464
|
|
|
425
465
|
Class names are deterministic for the same render order. If you see mismatches, ensure `hydrateTastyClasses()` runs before React hydration. For Next.js, this is automatic. For Astro with `tastyIntegration()`, this is also automatic. For manual Astro middleware setups, import `@tenphi/tasty/ssr/astro-client` in your island components. For custom setups, call `hydrateTastyClasses()` before `hydrateRoot()`.
|
|
426
466
|
|
|
427
|
-
Class names are also derived from the
|
|
467
|
+
Class names are also derived from the _resolved_ styles, so the server and the client must configure Tasty identically. Anything that changes what a component's styles resolve to will produce a mismatch if it is registered on only one side — `namePrefix`, `recipes`, `handlers`, and the `propHandlers` / `baseStyleProps` extension points described in [Plugins](plugins.md). Call the same `configure()` on both; global CSS is deduplicated automatically, so no `typeof window` guard is needed.
|
|
428
468
|
|
|
429
469
|
### Styles duplicated after hydration
|
|
430
470
|
|
|
431
|
-
**Global CSS** (`:root` tokens, `@property`, `globalStyles`, `@font-face`, `@counter-style`) configured via `configure()` is automatically deduplicated. When Tasty detects
|
|
471
|
+
**Global CSS** (`:root` tokens, `@property`, `globalStyles`, `@font-face`, `@counter-style`, `@function`) configured via `configure()` is automatically deduplicated. When Tasty detects an inline or extracted `[data-tasty-ssr]` stylesheet in the document, it skips client-side injection of globals that were already rendered by the SSR collector. This means `configure()` can be called with the full config on both server and client — no `typeof window === 'undefined'` guard is needed.
|
|
432
472
|
|
|
433
473
|
**Component CSS**: SSR `<style data-tasty-ssr>` tags remain in the DOM. The client injector creates separate `<style>` elements for any new styles. SSR styles are never modified or removed by the client. If this is a concern for very large apps, you can remove the SSR style tags and hydration scripts manually after hydration:
|
|
434
474
|
|
|
@@ -440,11 +480,9 @@ hydrateRoot(root, <App />);
|
|
|
440
480
|
|
|
441
481
|
// Optional: remove SSR style tags and class-list scripts after hydration
|
|
442
482
|
document.querySelectorAll('style[data-tasty-ssr]').forEach((el) => el.remove());
|
|
443
|
-
document
|
|
444
|
-
.
|
|
445
|
-
|
|
446
|
-
if (el.textContent?.includes('__TASTY__')) el.remove();
|
|
447
|
-
});
|
|
483
|
+
document.querySelectorAll('script').forEach((el) => {
|
|
484
|
+
if (el.textContent?.includes('__TASTY__')) el.remove();
|
|
485
|
+
});
|
|
448
486
|
```
|
|
449
487
|
|
|
450
488
|
### `AsyncLocalStorage` not available
|