@tanstack/redact 0.0.7 → 0.0.8

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/README.md +0 -481
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/redact",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "React, redacted. A minimal React-API-compatible drop-in replacement.",
5
5
  "type": "module",
6
6
  "main": "./dist/react/index.js",
package/README.md DELETED
@@ -1,481 +0,0 @@
1
- # redact
2
-
3
- **React, redacted.** A minimal React-19-API-compatible drop-in replacement, **~4× smaller** than canonical React. Shipped as a single `@tanstack/redact` package with subpath exports for the `react` / `react-dom` / `react-dom/server` / `scheduler` / `react/jsx-runtime` shapes. User code keeps its canonical `import { useState } from 'react'` — the swap happens at the bundler level.
4
-
5
- - **9.07 KB** gzip at full drop-in parity (vs ~45 KB for React 19)
6
- - **6.75 KB** gzip with every opt-in feature stubbed (`nano` preset)
7
- - **707/707** unit + integration tests passing, SSR + streaming Suspense + hydration included
8
- - Running in production on [tanstack.com](https://tanstack.com) as of 2026-04-20
9
-
10
- ---
11
-
12
- ## Quick start
13
-
14
- ```bash
15
- pnpm add @tanstack/redact@next
16
- ```
17
-
18
- ```ts
19
- // vite.config.ts
20
- import { defineConfig } from 'vite'
21
- import { redact } from '@tanstack/redact/vite'
22
-
23
- export default defineConfig({
24
- plugins: [redact()],
25
- })
26
- ```
27
-
28
- That's it. The plugin aliases `react` / `react-dom` / `scheduler` across Vite's client + ssr environments. The RSC environment is skipped so `@vitejs/plugin-rsc` keeps using real React for Flight serialization. User-facing imports are unchanged:
29
-
30
- ```ts
31
- import { useState, Suspense } from 'react'
32
- import { createRoot, hydrateRoot } from 'react-dom/client'
33
- ```
34
-
35
- ### Shrink further with feature flags
36
-
37
- Two presets — pick a starting point, flip flags from there:
38
-
39
- ```ts
40
- redact({ preset: 'full' }) // 9.07 KB — everything on, opt OUT individual features
41
- redact({ preset: 'nano' }) // 6.75 KB — everything off, opt IN what you need
42
- ```
43
-
44
- Opt out from `full`:
45
-
46
- ```ts
47
- redact({
48
- preset: 'full',
49
- features: {
50
- hydration: false, // SPA only — no SSR
51
- classComponents: false, // function components only
52
- },
53
- })
54
- ```
55
-
56
- Opt in from `nano`:
57
-
58
- ```ts
59
- redact({
60
- preset: 'nano',
61
- features: {
62
- context: true, // bring back just what you need
63
- suspense: true,
64
- },
65
- })
66
- ```
67
-
68
- Full feature matrix and alternative configuration paths below.
69
-
70
- ---
71
-
72
- ## How it works in 30 seconds
73
-
74
- `@tanstack/redact/dom` is built as an irreducible core (fiber reconciler, host DOM, core hooks, elements) plus **8 opt-in features** layered on top. Each feature has a `full.ts` (real implementation) and a `stub.ts` (graceful degradation). Features self-register with the reconciler at module load — renderers, type matchers, capability hooks.
75
-
76
- Feature selection is a bundler-level concern. The `@tanstack/redact/vite` plugin's `resolveId` hook swaps `features/<name>/index.js` → `features/<name>/stub.js` for features you've flagged off. Stubbed features' full code never enters the module graph, so tree-shaking strips it. No user-code changes. No runtime branching.
77
-
78
- ---
79
-
80
- ## Feature flags
81
-
82
- ### Feature matrix
83
-
84
- | Flag | Full behavior | Stub behavior (when `false`) | Savings (gzip) |
85
- |---|---|---|---:|
86
- | `portal` | `createPortal` into alt container | Children render in place, `container` ignored | ~30 B |
87
- | `context` | Provider push/pop + consumer walk | Provider → Fragment; `useContext` returns default | ~80 B |
88
- | `suspense` | Boundary + fallback + streaming hydration | Suspense → Fragment; thenables retry on settle | **~640 B** |
89
- | `memo` | `shallowEqual` prop-equality gate | Passes through every parent render | ~80 B |
90
- | `forwardRef` | Ref forwarded to inner fn | Ref dropped (React 19 "refs as props" still works) | ~70 B |
91
- | `lazy` | Full hydration coordination | Sync-resolvable payloads work; async retries on settle | ~20 B |
92
- | `classComponents` | Full lifecycle + `contextType` + error boundaries | `constructor` + `render` + `setState` only | ~200 B |
93
- | `hydration` | SSR DOM adoption, streaming boundaries, scroll guard, event replay | `hydrateRoot` throws; use `createRoot` for SPA | **~1270 B** |
94
-
95
- **Always on** (irreducible core, ~6.7 KB gzip): fiber reconciler with keyed child diffing, host DOM mount/update, `useState` / `useReducer` / `useEffect` / `useLayoutEffect` / `useInsertionEffect` / `useRef` / `useMemo` / `useCallback` / `useId` / `useSyncExternalStore` / `use` (for thenables), native event binding, Fragments, StrictMode/Profiler (aliased to Fragment), element creation + JSX runtime.
96
-
97
- ### Presets
98
-
99
- | Preset | What's on | `react-dom/client` gzip | Intent |
100
- |---|---|---:|---|
101
- | `full` (default) | all 8 features | **9.07 KB** | Drop-in React — opt OUT individual features you don't need |
102
- | **`nano`** | none | **6.75 KB** | Start minimal — opt IN individual features you need |
103
-
104
- Two presets, not a spectrum: every app either wants most of React (start from `full`, opt out) or a tight bundle (start from `nano`, opt in). Per-feature overrides merge on top of preset defaults either way.
105
-
106
- ---
107
-
108
- ## Configuration
109
-
110
- Four ways to configure, depending on your bundler and ergonomics preference.
111
-
112
- ### 1. Vite plugin (recommended)
113
-
114
- `@tanstack/redact/vite`'s `redact()` plugin. Covered in [Quick start](#quick-start) above. Full options:
115
-
116
- ```ts
117
- interface RedactOptions {
118
- preset?: 'nano' | 'full' // default: 'full'
119
- features?: {
120
- portal?: boolean
121
- context?: boolean
122
- suspense?: boolean
123
- memo?: boolean
124
- forwardRef?: boolean
125
- lazy?: boolean
126
- classComponents?: boolean
127
- hydration?: boolean
128
- }
129
- skip?: ReadonlyArray<string> // don't alias these specifiers
130
- resolveFrom?: string // override package resolution root
131
- packageRoots?: Record<string, string> // explicit package paths
132
- }
133
- ```
134
-
135
- The plugin also handles Vite-specific wiring: `optimizeDeps.exclude` for the shim packages, `ssr.noExternal` so SSR bundles inline them, and an `enforce: 'pre'` hook ordering so the alias wins over other resolvers.
136
-
137
- ### 2. Bundler aliases (Webpack / Rollup / esbuild / …)
138
-
139
- The package exposes every feature module as a `./features/*` subpath export. Any bundler with a path-alias feature can redirect a feature's `index` to its `stub` to opt the feature out of the bundle.
140
-
141
- **Subpath layout:**
142
-
143
- ```
144
- @tanstack/redact/features/
145
- portal/ context/ suspense/ memo/ forward-ref/ lazy/ class/ hydration/
146
- index ← re-exports from ./full by default
147
- full ← real implementation
148
- stub ← graceful degradation
149
- ```
150
-
151
- **Webpack example (stubs hydration + suspense):**
152
-
153
- ```js
154
- // webpack.config.js
155
- module.exports = {
156
- resolve: {
157
- alias: {
158
- '@tanstack/redact/features/hydration/index':
159
- '@tanstack/redact/features/hydration/stub',
160
- '@tanstack/redact/features/suspense/index':
161
- '@tanstack/redact/features/suspense/stub',
162
- },
163
- },
164
- }
165
- ```
166
-
167
- **Rollup:**
168
-
169
- ```js
170
- import alias from '@rollup/plugin-alias'
171
-
172
- export default {
173
- plugins: [
174
- alias({
175
- entries: [
176
- {
177
- find: '@tanstack/redact/features/hydration/index',
178
- replacement: '@tanstack/redact/features/hydration/stub',
179
- },
180
- ],
181
- }),
182
- ],
183
- }
184
- ```
185
-
186
- **esbuild:**
187
-
188
- ```js
189
- import { build } from 'esbuild'
190
-
191
- await build({
192
- entryPoints: ['src/app.tsx'],
193
- bundle: true,
194
- alias: {
195
- '@tanstack/redact/features/hydration/index':
196
- '@tanstack/redact/features/hydration/stub',
197
- },
198
- })
199
- ```
200
-
201
- **Gotchas:**
202
-
203
- - **On-disk folder names vs. config keys**: `forward-ref/` ↔ `forwardRef`, `class/` ↔ `classComponents`. When configuring aliases manually, match the on-disk folder.
204
- - **Single-instance requirement**: `@tanstack/redact` (and any subpath of it) must resolve to **one** installed copy in your app. Mixing source + dist, or two different tarballs, duplicates `ReactSharedInternals` and breaks hooks. The package's `ReactSharedInternals` is stashed on `globalThis` under a registered symbol as a defense-in-depth, but you should still aim for a single copy.
205
- - **Feature interdependencies**: Suspense's full implementation imports hydration helpers. If hydration is stubbed but Suspense is full, the Suspense feature uses hydration's no-op stubs (fine — you're not hydrating). Suspense stubbed + hydration full is also fine (streaming boundaries just won't render fallback UI because `Suspense` maps to Fragment).
206
-
207
- ### 3. Prebuilt bundle presets (planned)
208
-
209
- Not yet shipped. The planned shape:
210
-
211
- ```ts
212
- import { createRoot } from '@tanstack/redact/dom/nano/client'
213
- ```
214
-
215
- Zero bundler configuration; useful for script-tag usage, non-bundler Node tools, or users who just want the smallest install without thinking about it.
216
-
217
- **Why not yet:** the preset bundle would need its own self-contained `_all.js` built with the right stubs compiled in — stubs can't reliably overlay a module that registers full variants first (registration order matters, last-write-wins). We want to gather real Vite-plugin usage data before deciding which prebuilt configurations are worth publishing. Open an issue with your use case if this unblocks you.
218
-
219
- ### 4. npm aliases (limited)
220
-
221
- `npm:` package aliases in `package.json` work for the top-level `react` mapping but **not** for subpaths — there's no spec-level way to point `react-dom` at a subpath like `@tanstack/redact/dom` purely via `package.json`. So this path only gets you partway:
222
-
223
- ```jsonc
224
- // package.json — works, but only swaps `react` itself
225
- {
226
- "dependencies": {
227
- "react": "npm:@tanstack/redact@next"
228
- }
229
- }
230
- ```
231
-
232
- Anything that imports `react-dom`, `react-dom/client`, `react-dom/server`, or `scheduler` will still resolve to the real React in `node_modules` unless your bundler can rewrite those specifiers — at which point you may as well use Path 1 (Vite plugin) or Path 2 (bundler aliases). This is a real trade-off of the single-package layout: the install side is simpler but the no-bundler workflow loses some flexibility versus a multi-package shim. If you need a no-bundler full swap, open an issue with your toolchain and we can publish individual `@tanstack/redact-dom`, `@tanstack/redact-server`, etc. compatibility re-export packages.
233
-
234
- ---
235
-
236
- ## Advanced: authoring custom features & bundler plugins
237
-
238
- If you're extending the system, writing a bundler plugin for a tool without one, or just curious how the swap works — the internal API surface is exported from `@tanstack/redact/_all`.
239
-
240
- ### Registration primitives
241
-
242
- Feature modules self-register by calling these at module load:
243
-
244
- ```ts
245
- import {
246
- registerRenderer,
247
- registerTypeMatcher,
248
- registerElementMarker,
249
- type RenderFn,
250
- type TypeMatcher,
251
- } from '@tanstack/redact/_all'
252
-
253
- // Install a renderer for a FiberTag. Later calls overwrite earlier ones —
254
- // stubs exploit this order-dependence.
255
- function registerRenderer(tag: FiberTag, fn: RenderFn): void
256
-
257
- // Add a type matcher. Iterated in registration order during fiber creation,
258
- // after core checks (string → Host, REACT_FRAGMENT_TYPE → Fragment) and
259
- // before the function-vs-class fallback.
260
- type TypeMatcher = (type: any, marker: any) => FiberTag | null
261
- function registerTypeMatcher(m: TypeMatcher): void
262
-
263
- // Extend the accepted $$typeof set for child normalization. Default:
264
- // REACT_ELEMENT_TYPE, REACT_LEGACY_ELEMENT_TYPE. Portal adds REACT_PORTAL_TYPE.
265
- function registerElementMarker(sym: symbol): void
266
- ```
267
-
268
- ### Capability hooks
269
-
270
- Cross-cutting concerns (thrown-thenable handling, context reads) install via `installCapability`:
271
-
272
- ```ts
273
- import { installCapability, type Capabilities } from '@tanstack/redact/_all'
274
-
275
- interface Capabilities {
276
- handleSuspended: (fiber: Fiber, thenable: Promise<any>) => void
277
- readContext: (fiber: Fiber, ctx: any) => any
278
- }
279
-
280
- function installCapability<K extends keyof Capabilities>(
281
- name: K,
282
- fn: Capabilities[K],
283
- ): void
284
- ```
285
-
286
- Defaults when no feature installs an override:
287
- - `handleSuspended`: retry-on-settle (no boundary stack, no fallback)
288
- - `readContext`: returns `ctx._currentValue` with no provider-tree walk
289
-
290
- The full Suspense feature installs a boundary-stack-based `handleSuspended`. The full Context feature installs a walking `readContext`.
291
-
292
- ### Authoring a custom feature
293
-
294
- ```ts
295
- // my-feature/full.ts
296
- import {
297
- FiberTag,
298
- registerRenderer,
299
- registerTypeMatcher,
300
- reconcileChildren,
301
- childrenToArray,
302
- type Fiber,
303
- } from '@tanstack/redact/_all'
304
- import { SOME_SYMBOL } from '@tanstack/redact'
305
-
306
- function renderMyThing(fiber: Fiber, domParent: Node, anchor: Node | null): void {
307
- // your render logic
308
- }
309
-
310
- registerTypeMatcher((_type, marker) =>
311
- marker === SOME_SYMBOL ? FiberTag.SomeTag : null,
312
- )
313
- registerRenderer(FiberTag.SomeTag, renderMyThing)
314
- ```
315
-
316
- ```ts
317
- // my-feature/stub.ts
318
- import { FiberTag, registerTypeMatcher } from '@tanstack/redact/_all'
319
- import { SOME_SYMBOL } from '@tanstack/redact'
320
-
321
- // Stub: treat my-thing elements as Fragments (children render normally).
322
- registerTypeMatcher((_type, marker) =>
323
- marker === SOME_SYMBOL ? FiberTag.Fragment : null,
324
- )
325
- ```
326
-
327
- Pair with an `index.ts` (`export * from './full'`) and let your bundler pick which to import.
328
-
329
- ### Authoring a bundler plugin
330
-
331
- The Vite plugin's core is two `resolveId` cases. Port this pattern to any bundler's resolve hook:
332
-
333
- ```ts
334
- // Case 1: short specifier from features/index.ts
335
- // Matches `./portal`, `./context`, etc.
336
- if (importer matches /features[/\\]index\.(ts|js)$/) {
337
- const name = id.match(/^\.\/([a-z-]+)$/)?.[1]
338
- if (name && flags[name] === false) {
339
- return resolveFrom(`./${name}/stub`, importer)
340
- }
341
- }
342
-
343
- // Case 2: resolved-path match for hydration
344
- // (imported from reconcile, root, suspense/full, lazy/full)
345
- if (flags.hydration === false && /\/hydration$/.test(id)) {
346
- const resolved = await resolve(id, importer)
347
- if (/features[/\\]hydration[/\\]index\.(ts|js)$/.test(resolved)) {
348
- return resolved.replace(/index\.(ts|js)$/, 'stub.$1')
349
- }
350
- }
351
- ```
352
-
353
- Real implementation: [packages/redact/src/vite/index.ts](packages/redact/src/vite/index.ts).
354
-
355
- ### Verifying your setup
356
-
357
- Whichever path you choose, check that stubbed features' full code isn't in your output. Use your bundler's analyzer (rollup-plugin-visualizer, Webpack's bundle-analyzer, etc.) and search for `features/<name>/full.js`. With `hydration: false`, you should NOT see `features/hydration/full.js` or its imports (cursor machinery, event-replay, scroll-guard).
358
-
359
- ---
360
-
361
- ## Scope
362
-
363
- ### Supported
364
-
365
- - React 19 element model, JSX (classic + automatic), Fragment, Suspense, Portal, Error boundaries, forwardRef, memo, lazy
366
- - Full hook surface: `useState`, `useReducer`, `useEffect`, `useLayoutEffect`, `useInsertionEffect`, `useMemo`, `useCallback`, `useRef`, `useContext`, `useSyncExternalStore`, `useId`, `useDeferredValue`, `useTransition`, `use` (Context + Promise), `useEffectEvent`
367
- - Class components with full lifecycle (`componentDidMount`/`componentDidUpdate`/`componentWillUnmount`, `contextType`, `shouldComponentUpdate`, `getDerivedStateFromError`, `componentDidCatch`, legacy lifecycles as no-ops)
368
- - SSR via `renderToString` / `renderToReadableStream` / `renderToPipeableStream` — including Suspense boundary streaming with `$RC` reveal + event replay
369
- - Hydration: SSR DOM adoption, deferred hydration for `use(promise)` / lazy, cursor preservation across the synchronous `endHydration`
370
- - Cohabitation with `@vitejs/plugin-rsc`: the Vite plugin deliberately skips the RSC environment so Flight serialization stays on real `react-server-dom`
371
-
372
- ### Best-effort / subset behavior
373
-
374
- - `useTransition` / `useDeferredValue` run synchronously — no priority scheduling
375
- - Scheduler shim is a no-op wrapper around microtasks
376
- - No time slicing, no lane-based work interruption
377
-
378
- ### Out of scope
379
-
380
- - `react-server-dom-*/client` Flight deserializer (TanStack Start uses its own seroval-based codec + `@vitejs/plugin-rsc`)
381
- - React DevTools protocol
382
- - Behavioral 1:1 parity with React under concurrent-mode stress
383
-
384
- See [docs/SURFACE.md](./docs/SURFACE.md) for the full React-19 export-by-export audit.
385
-
386
- ---
387
-
388
- ## Performance
389
-
390
- Measured against TanStack Router + TanStack Start benchmarks (`pnpm nx run @benchmarks/client-nav:test:perf:react`, `@benchmarks/ssr:test:perf:react`):
391
-
392
- | Bench | Real React | This shim | Ratio |
393
- |---|---:|---:|---:|
394
- | `client-nav` (router-driven navigation loop) | 34.9 hz | **78.1 hz** | **2.24× faster** |
395
- | `ssr` (request loop) | ~48 hz | **168 hz** | **~3× faster**[^1] |
396
-
397
- [^1]: SSR speedup requires a latent `stringifyValue` bug in `@tanstack/router-core` to be patched (exception-throwing in a hot loop was eating 34% of request time regardless of renderer — see `scripts/repro-router-hang.mjs`).
398
-
399
- On tanstack.com (full site, not just renderer): Lighthouse perf scores at parity with stock React, consistent FCP wins across desktop/mobile, mild LCP regression on RSC-heavy pages (tied to the shim's Flight-deserialize suspend/resume), CLS/TBT ≈ 0. Full 30-run median breakdown: [tanstack.com/docs/perf/lighthouse-shim-vs-react-2026-04-20.md](https://github.com/TanStack/tanstack.com/blob/main/docs/perf/lighthouse-shim-vs-react-2026-04-20.md).
400
-
401
- ---
402
-
403
- ## Development
404
-
405
- ### Layout
406
-
407
- One package, one tree, internal subdirectories per concern:
408
-
409
- ```
410
- packages/redact/src/
411
- core/ VDOM types + symbols (FiberTag, Hook, ReactNode, …)
412
- react/ 'react' entry: createElement, hooks, context, class,
413
- memo, suspense, jsx-runtime, ReactSharedInternals
414
- dom/ 'react-dom' entry: reconciler, host DOM, root,
415
- createPortal, flushSync
416
- features/ opt-in features (each is an index/full/stub triple)
417
- portal/ context/ suspense/ memo/
418
- forward-ref/ lazy/ class/ hydration/
419
- server/ 'react-dom/server' entry: renderToString,
420
- renderToReadableStream, renderToPipeableStream
421
- scheduler/ 'scheduler' shim (no-op microtask wrapper)
422
- vite/ redact() Vite plugin: aliases + feature-flag swaps
423
- tests/ vitest suite — 707 tests
424
- examples/
425
- ssr-demo/ full SSR + Suspense streaming smoke app
426
- docs/
427
- SURFACE.md React 19 export audit
428
- SAVINGS_ANALYSIS.md per-export size savings vs React 19
429
- scripts/
430
- build.mjs per-entry esbuild build (every TS module emitted)
431
- size.mjs per-preset / per-flag gzip report
432
- size-check.mjs CI size-budget assertions
433
- size-analyze.mjs per-module byte breakdown for a given preset
434
- ```
435
-
436
- Cross-subdir imports inside `packages/redact/src/` use relative paths
437
- (`../core`, `../react`, etc.). The build emits each TS module as its own
438
- dist file with all relative imports kept literal — that's what preserves the
439
- import-graph boundaries the Vite plugin needs to swap features at consumer
440
- build time.
441
-
442
- ### Commands
443
-
444
- ```bash
445
- pnpm install
446
- pnpm build # esbuild dist/ + tsc declaration emit
447
- pnpm test # vitest suite (707 tests)
448
- pnpm test:types # tsc --noEmit
449
- pnpm size # gzip/brotli per entry + per feature-stub
450
- pnpm size:check # CI budget assertions (fails on regression)
451
- pnpm --filter ssr-demo dev # serve http://localhost:5173
452
- ```
453
-
454
- ### Current sizes
455
-
456
- Subpath sizes from `pnpm size`. The `react` / `react-dom/client` / `react-dom/server` column names are the user-facing aliases the Vite plugin sets up; under the hood they all resolve into `@tanstack/redact/*`.
457
-
458
- | Entry | min | gzip | brotli |
459
- |---|---:|---:|---:|
460
- | `react` (= `@tanstack/redact`) | 6.59 KB | 2.65 KB | 2.41 KB |
461
- | `react/jsx-runtime` (= `@tanstack/redact/jsx-runtime`) | 247 B | 189 B | 178 B |
462
- | `react-dom/client` (= `@tanstack/redact/dom-client`, `full`) | 26.56 KB | **9.07 KB** | 8.21 KB |
463
- | `react-dom/client` (= `@tanstack/redact/dom-client`, `nano`) | 18.75 KB | **6.75 KB** | 6.10 KB |
464
- | `react-dom/server` (= `@tanstack/redact/server`) | 11.48 KB | 4.59 KB | 4.16 KB |
465
- | **Client total** (`full`: react + react-dom/client + jsx-runtime) | 32.63 KB | **11.18 KB** | 10.14 KB |
466
-
467
- Regenerate with `pnpm size`.
468
-
469
- ---
470
-
471
- ## Changelog
472
-
473
- The project's first 9 alpha versions shipped as separate `@tanstack/react`, `@tanstack/react-dom`, `@tanstack/react-dom-server`, `@tanstack/dom-core`, `@tanstack/scheduler`, and `@tanstack/dom-vite` packages (`0.1.0-alpha.0` … `0.1.0-alpha.9`). Those packages are now deprecated. The project starts fresh as a single `@tanstack/redact` (`0.0.1`+) with subpath exports — the fixes below predate the rename and the package names refer to the previous multi-package layout.
474
-
475
- - `@tanstack/redact@0.0.1` — **first release of `@tanstack/redact`**. Consolidates the 6 previously-separate alpha packages into a single package with subpath exports (`./jsx-runtime`, `./dom`, `./dom-client`, `./dom-test-utils`, `./server`, `./scheduler`, `./vite`, `./features/*`, `./_all`). Vite plugin renamed `tanstackDom()` → `redact()`, types `TanStackDom*` → `Redact*`. `ReactSharedInternals` made a `globalThis`-stashed singleton via `Symbol.for` to defend against duplicate package copies under bundlers like Cloudflare's `vite-plugin` that mix `noExternal: true` worker bundling with separate pre-bundled dep copies. New `tests/public-exports.test.ts` snapshot guards every subpath's named-export set against silent link-time drift.
476
- - `react@0.1.0-alpha.8` — added `useEffectEvent` hook (stable callback over a `useInsertionEffect`-refreshed ref). Fixes missing-export errors in consumers using React 19 event handlers.
477
- - `react-dom@0.1.0-alpha.8` — **feature-flag system landed**: 8 opt-in features with stub/full pairs, typed Vite plugin config, `pnpm size:check` CI budget enforcement. `nano` preset ships **6.75 KB gzip** — a 26% reduction from `full`.
478
- - `react-dom@0.1.0-alpha.5` — `useEffect` / `useLayoutEffect` cleanup now runs at effect-run time (in the passive drain) instead of dispatch time. Coalesced renders landing back-to-back before the drain (common with router/store state updates triggered by one user action) no longer leak side-effects into the DOM.
479
- - `react-dom@0.1.0-alpha.4` — `renderFunction`'s deferred-hydration branch now matches `renderLazy`'s ancestor-Suspense guard (`_awaitingLazyHydration`). Fixes duplicate markup on RSC-hydrated subtrees.
480
- - `react-dom-server@0.1.0-alpha.4` — shell + bootstrap emits are buffered into one `TextEncoder.encode` + `ReadableStream.enqueue` instead of per-chunk, cutting Node stream overhead in the SSR CPU profile.
481
- </content>