@fluojs/react 0.1.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.
Files changed (106) hide show
  1. package/LICENSE +21 -0
  2. package/README.ko.md +715 -0
  3. package/README.md +729 -0
  4. package/dist/client/errors.d.ts +12 -0
  5. package/dist/client/errors.d.ts.map +1 -0
  6. package/dist/client/errors.js +19 -0
  7. package/dist/client/hooks.d.ts +38 -0
  8. package/dist/client/hooks.d.ts.map +1 -0
  9. package/dist/client/hooks.js +56 -0
  10. package/dist/client/link.d.ts +13 -0
  11. package/dist/client/link.d.ts.map +1 -0
  12. package/dist/client/link.js +39 -0
  13. package/dist/client/provider.d.ts +12 -0
  14. package/dist/client/provider.d.ts.map +1 -0
  15. package/dist/client/provider.js +54 -0
  16. package/dist/client/search-params.d.ts +17 -0
  17. package/dist/client/search-params.d.ts.map +1 -0
  18. package/dist/client/search-params.js +39 -0
  19. package/dist/client/snapshot.d.ts +15 -0
  20. package/dist/client/snapshot.d.ts.map +1 -0
  21. package/dist/client/snapshot.js +57 -0
  22. package/dist/client/store.d.ts +26 -0
  23. package/dist/client/store.d.ts.map +1 -0
  24. package/dist/client/store.js +138 -0
  25. package/dist/client/types.d.ts +63 -0
  26. package/dist/client/types.d.ts.map +1 -0
  27. package/dist/client/types.js +1 -0
  28. package/dist/client.d.ts +9 -0
  29. package/dist/client.d.ts.map +1 -0
  30. package/dist/client.js +5 -0
  31. package/dist/decorators.d.ts +80 -0
  32. package/dist/decorators.d.ts.map +1 -0
  33. package/dist/decorators.js +194 -0
  34. package/dist/experimental/rsc-diagnostics.d.ts +13 -0
  35. package/dist/experimental/rsc-diagnostics.d.ts.map +1 -0
  36. package/dist/experimental/rsc-diagnostics.js +58 -0
  37. package/dist/experimental/rsc-flight-response.d.ts +15 -0
  38. package/dist/experimental/rsc-flight-response.d.ts.map +1 -0
  39. package/dist/experimental/rsc-flight-response.js +76 -0
  40. package/dist/experimental/rsc-manifest.d.ts +13 -0
  41. package/dist/experimental/rsc-manifest.d.ts.map +1 -0
  42. package/dist/experimental/rsc-manifest.js +88 -0
  43. package/dist/experimental/rsc-types.d.ts +118 -0
  44. package/dist/experimental/rsc-types.d.ts.map +1 -0
  45. package/dist/experimental/rsc-types.js +50 -0
  46. package/dist/experimental/rsc.d.ts +11 -0
  47. package/dist/experimental/rsc.d.ts.map +1 -0
  48. package/dist/experimental/rsc.js +8 -0
  49. package/dist/experimental/server-functions-client.d.ts +14 -0
  50. package/dist/experimental/server-functions-client.d.ts.map +1 -0
  51. package/dist/experimental/server-functions-client.js +120 -0
  52. package/dist/experimental/server-functions-configuration.d.ts +17 -0
  53. package/dist/experimental/server-functions-configuration.d.ts.map +1 -0
  54. package/dist/experimental/server-functions-configuration.js +68 -0
  55. package/dist/experimental/server-functions-errors.d.ts +19 -0
  56. package/dist/experimental/server-functions-errors.d.ts.map +1 -0
  57. package/dist/experimental/server-functions-errors.js +22 -0
  58. package/dist/experimental/server-functions-reference.d.ts +22 -0
  59. package/dist/experimental/server-functions-reference.d.ts.map +1 -0
  60. package/dist/experimental/server-functions-reference.js +76 -0
  61. package/dist/experimental/server-functions-serialization.d.ts +37 -0
  62. package/dist/experimental/server-functions-serialization.d.ts.map +1 -0
  63. package/dist/experimental/server-functions-serialization.js +158 -0
  64. package/dist/experimental/server-functions-server.d.ts +14 -0
  65. package/dist/experimental/server-functions-server.d.ts.map +1 -0
  66. package/dist/experimental/server-functions-server.js +176 -0
  67. package/dist/experimental/server-functions-types.d.ts +86 -0
  68. package/dist/experimental/server-functions-types.d.ts.map +1 -0
  69. package/dist/experimental/server-functions-types.js +35 -0
  70. package/dist/index.d.ts +10 -0
  71. package/dist/index.d.ts.map +1 -0
  72. package/dist/index.js +4 -0
  73. package/dist/module.d.ts +40 -0
  74. package/dist/module.d.ts.map +1 -0
  75. package/dist/module.js +58 -0
  76. package/dist/render-stream.d.ts +27 -0
  77. package/dist/render-stream.d.ts.map +1 -0
  78. package/dist/render-stream.js +135 -0
  79. package/dist/render.d.ts +50 -0
  80. package/dist/render.d.ts.map +1 -0
  81. package/dist/render.js +201 -0
  82. package/dist/server-entry.d.ts +87 -0
  83. package/dist/server-entry.d.ts.map +1 -0
  84. package/dist/server-entry.js +126 -0
  85. package/dist/types.d.ts +11 -0
  86. package/dist/types.d.ts.map +1 -0
  87. package/dist/types.js +1 -0
  88. package/dist/vite/asset-utils.d.ts +57 -0
  89. package/dist/vite/asset-utils.d.ts.map +1 -0
  90. package/dist/vite/asset-utils.js +184 -0
  91. package/dist/vite/create-asset-manifest.d.ts +9 -0
  92. package/dist/vite/create-asset-manifest.d.ts.map +1 -0
  93. package/dist/vite/create-asset-manifest.js +127 -0
  94. package/dist/vite/diagnostics.d.ts +27 -0
  95. package/dist/vite/diagnostics.d.ts.map +1 -0
  96. package/dist/vite/diagnostics.js +47 -0
  97. package/dist/vite/parse-manifest.d.ts +18 -0
  98. package/dist/vite/parse-manifest.d.ts.map +1 -0
  99. package/dist/vite/parse-manifest.js +168 -0
  100. package/dist/vite/types.d.ts +187 -0
  101. package/dist/vite/types.d.ts.map +1 -0
  102. package/dist/vite/types.js +59 -0
  103. package/dist/vite.d.ts +3 -0
  104. package/dist/vite.d.ts.map +1 -0
  105. package/dist/vite.js +1 -0
  106. package/package.json +68 -0
package/README.md ADDED
@@ -0,0 +1,729 @@
1
+ # @fluojs/react
2
+
3
+ <p><strong><kbd>English</kbd></strong> <a href="./README.ko.md"><kbd>한국어</kbd></a></p>
4
+
5
+ Runtime-neutral React integration for fluo applications.
6
+
7
+ ## Table of Contents
8
+
9
+ - [Installation](#installation)
10
+ - [When to Use](#when-to-use)
11
+ - [Stable SSR Mental Model](#stable-ssr-mental-model)
12
+ - [Runtime and Peer Contract](#runtime-and-peer-contract)
13
+ - [Phase Boundaries](#phase-boundaries)
14
+ - [ReactModule Registration](#reactmodule-registration)
15
+ - [Router and Path Decorators](#router-and-path-decorators)
16
+ - [Web Streams SSR](#web-streams-ssr)
17
+ - [Hydration Asset Contract](#hydration-asset-contract)
18
+ - [Vite Asset Manifest Integration](#vite-asset-manifest-integration)
19
+ - [Client Navigation Runtime](#client-navigation-runtime)
20
+ - [Experimental RSC Prototype](#experimental-rsc-prototype)
21
+ - [Experimental Server Functions](#experimental-server-functions)
22
+ - [RSC Graduation Policy](#rsc-graduation-policy)
23
+ - [Current Limitations](#current-limitations)
24
+ - [Public API](#public-api)
25
+ - [Related Packages](#related-packages)
26
+ - [Example Sources](#example-sources)
27
+
28
+ ## Installation
29
+
30
+ The first public release target for this package is `0.1.0`. The manifest starts at
31
+ `0.0.0` so Changesets can publish the initial `0.1.0` version through the canonical
32
+ release workflow.
33
+
34
+ When the package is published, install it with React and React DOM as peers:
35
+
36
+ ```bash
37
+ npm install @fluojs/react react react-dom
38
+ ```
39
+
40
+ ## When to Use
41
+
42
+ Use this package when React page handlers need a lexical distinction from API controllers while
43
+ still participating in fluo's module graph and HTTP metadata pipeline. `ReactModule.forRoot(...)`
44
+ places React routers into ordinary module controller metadata, and `@Router(...)` plus
45
+ `@Path(...)` are React facades over `@fluojs/http` controller and `GET` route metadata, so request
46
+ DTO binding, versioning, guards, interceptors, headers, route validation, matching, and dispatch
47
+ continue to use the HTTP runtime contracts.
48
+
49
+ ## Stable SSR Mental Model
50
+
51
+ The stable `0.1.0` model is HTTP-first React SSR. `@Router(...)` and `@Path(...)` are lexical
52
+ React facades over `@fluojs/http` metadata: they mark classes and methods as React page surfaces for
53
+ readability and diagnostics, then write the same controller and `GET` route metadata that the HTTP
54
+ runtime already understands. URL matching remains owned by `@fluojs/http`, not React, so React page
55
+ paths inherit the HTTP route grammar, conflict detection, versioning, DTO materialization,
56
+ validation, guards, interceptors, headers, module middleware, request scopes, and request lifecycle.
57
+
58
+ This package is therefore **not** a Next.js App Router clone, React Server Components framework,
59
+ TanStack route tree, Angular `Routes[]` table, file-route scanner, or primary React-owned
60
+ `routes: []` configuration model. Treat React routers as page-shaped HTTP handlers. Route discovery
61
+ and dispatch stay in the existing fluo module/controller pipeline, and page handlers return either
62
+ ordinary values or `createReactServerEntry(...)` when they want streamed HTML.
63
+
64
+ ## Runtime and Peer Contract
65
+
66
+ The root `@fluojs/react` import is runtime-neutral. Importing it must not eagerly load Node.js
67
+ built-ins, Vite, `react-dom/server`, React Server Components packages, or Server Functions code.
68
+
69
+ `react` and `react-dom` are declared as peer dependencies so applications own the React runtime
70
+ version. The package root exposes SSR helpers but resolves `react-dom/server` lazily only when a
71
+ React server entry is rendered.
72
+
73
+ ## Phase Boundaries
74
+
75
+ `@fluojs/react` uses explicit subpath boundaries so the stable root stays small and runtime-neutral:
76
+
77
+ - **root `@fluojs/react`** — stable `0.1.0` SSR MVP contracts: `ReactModule.forRoot(...)`,
78
+ `@Router(...)`, `@Path(...)`, metadata readers, `createReactServerEntry(...)`,
79
+ `renderReactResponse(...)`, Web Streams SSR, and explicit hydration asset options.
80
+ - **`@fluojs/react/vite`** — Vite build manifest parsing, React server/client entry selection,
81
+ deterministic stylesheet and JavaScript ordering, manifest diagnostics, and hydration option
82
+ creation. The root package still accepts explicit asset options without discovering or scanning
83
+ manifests.
84
+ - **`@fluojs/react/client`** — progressive anchors, HTTP-first full-document navigation, and
85
+ hydration-safe URL, path-param, and navigation lifecycle hooks. Browser APIs remain isolated from
86
+ the runtime-neutral root.
87
+ - **`@fluojs/react/experimental/rsc`** — an explicitly unstable React Server Components prototype
88
+ for exact-version compatibility diagnostics, client-reference/server-module manifest seams, and
89
+ Flight payload responses plus signed Server Function transport through ordinary fluo HTTP
90
+ handlers. It does not export from the stable root or `@fluojs/react/client`.
91
+
92
+ ## ReactModule Registration
93
+
94
+ Use `ReactModule.forRoot({ controllers: [...] })` inside an ordinary fluo module import to register
95
+ React routers. The returned module is a normal fluo module definition: it may include `imports`,
96
+ `providers`, `exports`, and `middleware`, and those fields keep the same visibility and lifecycle
97
+ rules as `@Module(...)` metadata.
98
+
99
+ ```tsx
100
+ import { Module, Inject } from '@fluojs/core';
101
+ import { ReactModule, Router, Path } from '@fluojs/react';
102
+
103
+ class DashboardPresenter {
104
+ render() {
105
+ return { page: 'dashboard' };
106
+ }
107
+ }
108
+
109
+ @Inject(DashboardPresenter)
110
+ @Router('/dashboard')
111
+ class DashboardRouter {
112
+ constructor(private readonly presenter: DashboardPresenter) {}
113
+
114
+ @Path('/')
115
+ index() {
116
+ return this.presenter.render();
117
+ }
118
+ }
119
+
120
+ @Module({
121
+ imports: [
122
+ ReactModule.forRoot({
123
+ controllers: [DashboardRouter],
124
+ providers: [DashboardPresenter],
125
+ }),
126
+ ],
127
+ })
128
+ class AppModule {}
129
+ ```
130
+
131
+ `ReactModule` does not own URL matching. Routers registered through it become ordinary HTTP handler
132
+ sources, so `createHandlerMapping(...)` and `Dispatcher` still detect duplicate routes, apply
133
+ module-level middleware, create request scopes, run guards and interceptors, and resolve route
134
+ versioning.
135
+
136
+ ## Router and Path Decorators
137
+
138
+ `@Router(basePath)` marks a class as a React router and writes HTTP controller metadata equivalent
139
+ to `@Controller(basePath)`. It also stores React router marker metadata readable through
140
+ `getReactRouterMetadata(...)` for diagnostics and future rendering integration.
141
+
142
+ `@Path(path, options?)` marks a method as a React page route and writes HTTP `GET` route metadata
143
+ equivalent to `@Get(path)`. It also stores React render metadata readable through
144
+ `getReactPathMetadata(...)`. The optional `options` object is metadata only in this phase; it does
145
+ not change HTTP matching or dispatch.
146
+
147
+ ```tsx
148
+ import { Router, Path } from '@fluojs/react';
149
+ import { FromPath, Optional, RequestDto, FromQuery } from '@fluojs/http';
150
+
151
+ class DashboardEditRequest {
152
+ @FromPath('id')
153
+ id = '';
154
+
155
+ @Optional()
156
+ @FromQuery('tab')
157
+ tab?: string;
158
+ }
159
+
160
+ @Router('/dashboard')
161
+ class DashboardRouter {
162
+ @Path('/:id/edit')
163
+ @RequestDto(DashboardEditRequest)
164
+ edit(input: DashboardEditRequest) {
165
+ return { page: 'dashboard-edit', input };
166
+ }
167
+ }
168
+ ```
169
+
170
+ React page paths use the exact `@fluojs/http` route grammar: literal segments and full-segment
171
+ `:param` placeholders only. Wildcards, catch-all routes, optional segments, regex-like tokens,
172
+ mixed literal/parameter segments such as `user-:id`, and suffix params such as `:id.json` are not
173
+ supported.
174
+
175
+ The [HTTP catch-all route grammar decision](../../docs/architecture/http-catch-all-route-grammar.md)
176
+ defers wildcard adoption. React does not add its own syntax: page handlers should keep explicit
177
+ server routes, and any future catch-all must first become an approved `@fluojs/http` contract.
178
+
179
+ ## Web Streams SSR
180
+
181
+ Return `createReactServerEntry(...)` from a React page handler to stream HTML through the existing
182
+ fluo HTTP dispatcher. Guards, interceptors, module middleware, route headers, `@HttpCode(...)`, DTO
183
+ binding, request scopes, and duplicate route detection all run before `renderReactResponse(...)`
184
+ finalizes the HTML response.
185
+
186
+ ```tsx
187
+ import { HttpCode, RequestDto, FromPath } from '@fluojs/http';
188
+ import { Router, Path, createReactServerEntry } from '@fluojs/react';
189
+
190
+ class DashboardRequest {
191
+ @FromPath('id')
192
+ id = '';
193
+ }
194
+
195
+ @Router('/dashboard')
196
+ class DashboardRouter {
197
+ @HttpCode(206)
198
+ @Path('/:id')
199
+ @RequestDto(DashboardRequest)
200
+ show(input: DashboardRequest) {
201
+ return createReactServerEntry(<main>Dashboard {input.id}</main>, {
202
+ headers: { 'x-react-page': 'dashboard' },
203
+ onRecoverableError(error, context) {
204
+ // Report through your application logger; the response status is already committed.
205
+ void error;
206
+ void context;
207
+ },
208
+ });
209
+ }
210
+ }
211
+ ```
212
+
213
+ The renderer uses `react-dom/server` `renderToReadableStream(...)` by default, preserves Suspense
214
+ streaming, writes `Content-Type: text/html; charset=utf-8`, forwards `RequestContext.request.signal`
215
+ when an adapter provides one, and throws shell render failures before response bytes are committed.
216
+ Recoverable Suspense errors are reported through `onRecoverableError` and do not rewrite an already
217
+ committed status. Call `renderReactResponse(entry, requestContext)` directly only when a custom
218
+ handler needs to finalize the response itself instead of returning the entry to the dispatcher.
219
+
220
+ ## Hydration Asset Contract
221
+
222
+ `createReactServerEntry(...)` accepts explicit React DOM hydration asset options and forwards them to
223
+ the Web Streams renderer:
224
+
225
+ - `bootstrapScripts` emits classic bootstrap `<script>` tags. Duplicate entries with the same `src`
226
+ are removed before React DOM receives the option.
227
+ - `bootstrapModules` emits module bootstrap `<script type="module">` tags with the same duplicate
228
+ removal rule.
229
+ - `bootstrapScriptContent` emits trusted inline script content exactly as provided. fluo does not
230
+ serialize arbitrary values into this string; only use it for trusted build-time data or content that
231
+ your application has escaped through an approved safety contract.
232
+ - `nonce` applies the CSP nonce that React DOM attaches to emitted bootstrap scripts.
233
+ - `identifierPrefix` is forwarded to React DOM so server-rendered `useId()` output and client
234
+ hydration share the same prefix.
235
+ - `assetMap` is a defensive snapshot of trusted build-produced logical asset names to public URLs.
236
+ fluo passes the snapshot to custom renderers and expects applications to pass the same data to the
237
+ server root component and client `hydrateRoot(...)` call when it affects markup.
238
+
239
+ ```tsx
240
+ const assetMap = {
241
+ 'main.js': '/assets/main.123.js',
242
+ 'styles.css': '/assets/styles.123.css',
243
+ } as const;
244
+
245
+ return createReactServerEntry(<App assetMap={assetMap} />, {
246
+ assetMap,
247
+ bootstrapModules: [assetMap['main.js']],
248
+ bootstrapScriptContent: 'window.__FLUO_ASSET_MAP__ = {"main.js":"/assets/main.123.js"};',
249
+ identifierPrefix: 'fluo-',
250
+ nonce: cspNonce,
251
+ });
252
+ ```
253
+
254
+ CSS/JS ordering remains caller-owned when applications pass hydration options manually.
255
+ `@fluojs/react/vite` preserves manifest stylesheet order for the app-rendered document head before
256
+ hydration scripts, then preserves manifest JavaScript order plus the caller order of
257
+ `bootstrapScripts` and `bootstrapModules` after duplicate removal. This package does not discover
258
+ Vite manifests from the filesystem, generate client bundles, or serialize untrusted user data into
259
+ inline scripts.
260
+
261
+ ## Vite Asset Manifest Integration
262
+
263
+ Use `@fluojs/react/vite` when a Vite-built React application needs to turn a loaded Vite manifest
264
+ into the stable hydration options accepted by `createReactServerEntry(...)`.
265
+
266
+ ```tsx
267
+ import { createReactViteAssetManifest } from '@fluojs/react/vite';
268
+ import { createReactServerEntry } from '@fluojs/react';
269
+
270
+ const assets = createReactViteAssetManifest({
271
+ base: '/assets/',
272
+ entries: {
273
+ client: 'src/entry-client.tsx',
274
+ server: 'src/entry-server.tsx',
275
+ },
276
+ manifest: viteManifest,
277
+ nonce: cspNonce,
278
+ });
279
+
280
+ if (!assets.ok) {
281
+ throw new Error(assets.diagnostics.map((diagnostic) => diagnostic.message).join('\n'));
282
+ }
283
+
284
+ return createReactServerEntry(<App assetMap={assets.manifest.assetMap} />, {
285
+ ...assets.manifest.hydrationOptions,
286
+ });
287
+ ```
288
+
289
+ `createReactViteAssetManifest(...)` accepts an already-loaded manifest value. It does not read the
290
+ filesystem, run Vite, create client bundles, or add a root dependency on Vite. The parsed manifest
291
+ schema is compatible with Vite's client manifest shape:
292
+
293
+ | Field | Required | Meaning |
294
+ | --- | --- | --- |
295
+ | `file` | yes | JavaScript output file for a chunk. Selected server/client entries must resolve to `.js`, `.mjs`, or `.cjs`. |
296
+ | `src` / `name` | no | Secondary lookup keys for the explicit `entries.server` and `entries.client` selectors. |
297
+ | `isEntry` / `isDynamicEntry` | no | Vite entry markers retained for diagnostics and future compatibility. |
298
+ | `imports` | no | Static imported chunk ids. Imported chunks are ordered before the client entry. |
299
+ | `css` | no | Stylesheet files emitted for a chunk. The returned `manifest.css` keeps dependency order and removes duplicates. |
300
+ | `assets` | no | Static asset files copied into the returned `assetMap`. |
301
+
302
+ The successful result contains:
303
+
304
+ - `manifest.hydrationOptions` — `assetMap`, `bootstrapModules`, `bootstrapScripts`, trusted
305
+ `bootstrapScriptContent`, `identifierPrefix`, and `nonce` shaped for `createReactServerEntry(...)`.
306
+ - `manifest.css` — stylesheet URLs for the application-rendered document head before hydration
307
+ scripts.
308
+ - `manifest.js.modules` and `manifest.js.scripts` — module scripts derived from the Vite client
309
+ import graph and caller-provided classic scripts.
310
+ - `manifest.assetMap` — a defensive snapshot of manifest keys, source names, emitted files, CSS, and
311
+ static assets mapped to public URLs.
312
+ - `manifest.serverEntry` and `manifest.clientEntry` — resolved React server/client entries.
313
+
314
+ Expected manifest failures return diagnostics instead of throwing. Stable diagnostic codes are:
315
+
316
+ - `react-vite-manifest-missing-server-entry`
317
+ - `react-vite-manifest-missing-client-entry`
318
+ - `react-vite-manifest-malformed`
319
+ - `react-vite-manifest-unsupported-output-shape`
320
+
321
+ `@fluojs/react/vite` is separate from `@fluojs/vite`. Use `@fluojs/vite` in `vite.config.ts` for the
322
+ TC39 decorator transform used by fluo applications. Use `@fluojs/react/vite` in React SSR code to
323
+ parse React build assets and feed the existing hydration contract. Neither package owns file routes,
324
+ React-only route grammar, Next.js route segment conventions, RSC bundler behavior, or URL matching.
325
+ The runnable `examples/react-vite-ssr/` application shows this boundary with generated assets,
326
+ streamed Suspense content, direct React DOM hydration, and the client navigation subpath.
327
+
328
+ ## Client Navigation Runtime
329
+
330
+ Use `@fluojs/react/client` in hydrated React pages that need navigation controls and URL state without
331
+ introducing a client-owned route table. Create the initial snapshot from the active fluo HTTP request,
332
+ then pass that same snapshot to `ReactClientRouterProvider` during server rendering and hydration.
333
+ This request-scoped snapshot prevents server/client URL state drift and carries path params produced by
334
+ the existing HTTP route match; the client runtime never derives or validates route params itself.
335
+
336
+ ```tsx
337
+ import {
338
+ Link,
339
+ ReactClientRouterProvider,
340
+ createReactRouteSnapshot,
341
+ useNavigation,
342
+ usePathname,
343
+ useRouter,
344
+ useSearchParams,
345
+ } from '@fluojs/react/client';
346
+
347
+ function DashboardNav() {
348
+ const navigation = useNavigation();
349
+ const pathname = usePathname();
350
+ const router = useRouter();
351
+ const searchParams = useSearchParams();
352
+
353
+ return (
354
+ <nav aria-label="Dashboard">
355
+ <Link href="/dashboard/42/edit?tab=profile">Edit</Link>
356
+ <button type="button" onClick={() => router.push('/dashboard/42/edit?tab=profile')}>
357
+ Open editor
358
+ </button>
359
+ <output>{pathname} · {searchParams.get('tab')} · {navigation.status}</output>
360
+ </nav>
361
+ );
362
+ }
363
+
364
+ const initialSnapshot = createReactRouteSnapshot({
365
+ url: requestContext.request.url,
366
+ params: requestContext.request.params,
367
+ });
368
+
369
+ const app = (
370
+ <ReactClientRouterProvider initialSnapshot={initialSnapshot}>
371
+ <DashboardNav />
372
+ </ReactClientRouterProvider>
373
+ );
374
+ ```
375
+
376
+ The navigation contract is deliberately HTTP-first:
377
+
378
+ - `Link` always renders a real `<a href>` so pre-hydration clicks, disabled JavaScript, modified
379
+ clicks, downloads, explicit targets, and cross-origin destinations keep native browser behavior.
380
+ After hydration, an unmodified primary click to a same-origin HTTP(S) URL delegates to
381
+ `router.push(...)`.
382
+ - `router.push(href)` uses `window.location.assign(...)`; `router.replace(href)` uses
383
+ `window.location.replace(...)`. A same-origin destination that changes the pathname or search
384
+ (query string) performs full-document navigation, so fluo HTTP route matching, `@RequestDto`
385
+ binding and validation, guards, interceptors, redirects, not-found responses, non-HTML responses,
386
+ and server failures remain authoritative.
387
+ - A fragment-only destination that keeps the current pathname and search is a same-document
388
+ exception. The browser does not issue a new HTTP request; it emits `hashchange`, and a matching
389
+ event completes the requested `push` or `replace` lifecycle while updating the route snapshot URL
390
+ and hash. Because no server request occurs, fluo HTTP route matching, `@RequestDto` binding and
391
+ validation, guards, and interceptors do not run for that fragment change.
392
+ - If the normalized destination is an identical URL to the current route snapshot, the router does
393
+ not call `window.location.assign(...)` or `window.location.replace(...)`. It exposes `skipped`
394
+ with the requested navigation type and destination instead.
395
+ - `router.back()` delegates to `window.history.back()`. `router.refresh()` uses
396
+ `window.location.reload()` as the documented revalidation mechanism. It does not imply an RSC,
397
+ loader, or client-data cache.
398
+ - `usePathname()`, `useSearchParams()`, `useParams()`, and `useRouterState()` read the provider's
399
+ immutable route snapshot. `popstate` and `hashchange` update URL-derived fields. If a history event
400
+ changes the pathname without a new server document, stale path params are cleared rather than
401
+ guessed from a client route grammar.
402
+ - `useNavigation()` exposes `idle`, `navigating`, `refreshing`, `complete`, `error`, and `skipped`.
403
+ Full-document path/search transitions normally leave the current document while `navigating` or
404
+ `refreshing`, and the destination document starts from a new server-owned `idle` snapshot.
405
+ Fragment-only transitions complete in the current document after the matching `hashchange`.
406
+ - Router methods reject cross-origin or non-HTTP(S) destinations with
407
+ `ReactClientNavigationError`. Use a normal anchor for those destinations.
408
+
409
+ This phase intentionally omits `prefetch`: without an owned client data or render cache, prefetching
410
+ would promise behavior the package cannot yet revalidate or consume consistently.
411
+
412
+ Client navigation does not require a catch-all route. `Link` remains a real anchor, so hydration
413
+ gaps and disabled JavaScript fall back to ordinary full-document browser navigation. The server then
414
+ matches an explicit `@Path(...)`/HTTP route or returns its normal not-found response. An intentional
415
+ deployment-level document rewrite may be configured separately, but it does not create a React route
416
+ grammar or change server DTO validation.
417
+
418
+ ## Experimental RSC Prototype
419
+
420
+ > **Experimental contract:** `@fluojs/react/experimental/rsc` can change before an explicit
421
+ > graduation issue makes any RSC API stable. Do not import these APIs from the root package or treat
422
+ > them as semver-stable React framework internals.
423
+
424
+ The prototype supports **exactly** `react@19.2.6`, `react-dom@19.2.6`, and a matching Flight renderer
425
+ version. Version ranges and canary versions are not supported. If an application selects
426
+ `react-server-dom-webpack`, pin it to the same exact version; this package does not install, import,
427
+ or wrap that renderer. The stable root peer range remains broader for applications that do not use
428
+ the experimental RSC subpath.
429
+
430
+ Call `inspectReactRscEnvironment(...)` before enabling an RSC endpoint. It reports stable diagnostics
431
+ when any React version differs, the runtime does not provide Web `ReadableStream`, or the
432
+ application-owned build adapter does not provide both the client-reference manifest and explicit
433
+ server-to-client module map.
434
+
435
+ ```ts
436
+ import {
437
+ REACT_RSC_SUPPORTED_VERSION,
438
+ inspectReactRscEnvironment,
439
+ } from '@fluojs/react/experimental/rsc';
440
+
441
+ const support = inspectReactRscEnvironment({
442
+ reactVersion: '19.2.6',
443
+ reactDomVersion: '19.2.6',
444
+ flightRendererVersion: REACT_RSC_SUPPORTED_VERSION,
445
+ runtime: { name: 'node', webStreams: typeof ReadableStream !== 'undefined' },
446
+ build: {
447
+ name: 'application-rsc-build',
448
+ clientReferenceManifest: true,
449
+ serverClientModuleMap: true,
450
+ },
451
+ });
452
+ ```
453
+
454
+ `createReactRscManifest(...)` defines the initial bundler-neutral module graph seam. Client-reference
455
+ keys resolve to `{ id, chunks, name, async? }` metadata. Each server module id maps export names back
456
+ to those client-reference keys. The helper returns a defensive snapshot, rejects unknown mapping
457
+ targets with diagnostics, and never scans files or generates bundles. Translating this seam into a
458
+ specific React Flight renderer manifest remains the application build adapter's responsibility.
459
+
460
+ ```ts
461
+ import { createReactRscManifest } from '@fluojs/react/experimental/rsc';
462
+
463
+ const manifest = createReactRscManifest({
464
+ clientReferences: {
465
+ Counter: {
466
+ id: 'client:counter',
467
+ chunks: ['assets/counter.js'],
468
+ name: 'Counter',
469
+ },
470
+ },
471
+ serverClientModuleMap: {
472
+ 'server:dashboard': {
473
+ Counter: 'Counter',
474
+ },
475
+ },
476
+ });
477
+ ```
478
+
479
+ The application also owns Flight encoding. Return `createReactFlightResponse(...)` from an ordinary
480
+ fluo HTTP controller or React `@Path(...)` handler after the selected renderer has produced encoded
481
+ text, bytes, or a Web `ReadableStream<Uint8Array>`. The existing dispatcher still owns route
482
+ metadata, middleware, guards, interceptors, request scopes, errors, and adapter response writing;
483
+ the helper adds the fixed `text/x-component; charset=utf-8` content type and does not create a
484
+ parallel router.
485
+
486
+ ```ts
487
+ import { Controller, Get } from '@fluojs/http';
488
+ import { createReactFlightResponse } from '@fluojs/react/experimental/rsc';
489
+
490
+ @Controller('/rsc')
491
+ class RscController {
492
+ @Get('/dashboard')
493
+ dashboard() {
494
+ const payload = applicationFlightRenderer.render({ page: 'dashboard' });
495
+ return createReactFlightResponse(payload);
496
+ }
497
+ }
498
+ ```
499
+
500
+ The RSC manifest and Flight response helpers do not provide a Flight encoder/decoder, a Webpack or
501
+ Vite RSC plugin, automatic module graph discovery, client bundle generation, file routes, route
502
+ segments, or a React-owned URL matcher. The Server Function transport below is a separate opt-in
503
+ prototype on the same unstable subpath.
504
+
505
+ ## Experimental Server Functions
506
+
507
+ `createReactServerFunctionRegistry(...)` defines server-only actions, issues HMAC-SHA-256 references,
508
+ and validates calls. It deliberately does not create a router. Mount `registry.invoke(context)` on an
509
+ explicit ordinary fluo `@Post(...)` route so module middleware, route/controller guards,
510
+ interceptors, request-scoped providers, request observers, error envelopes, and adapter response
511
+ writing remain in the existing HTTP lifecycle.
512
+
513
+ ```ts
514
+ import {
515
+ BadRequestException,
516
+ Controller,
517
+ Post,
518
+ type RequestContext,
519
+ UnauthorizedException,
520
+ } from '@fluojs/http';
521
+ import { createReactServerFunctionRegistry } from '@fluojs/react/experimental/rsc';
522
+
523
+ const actions = createReactServerFunctionRegistry({
524
+ actions: {
525
+ updateProfile(args, context) {
526
+ const subject = context.principal?.subject;
527
+ if (subject === undefined) {
528
+ throw new UnauthorizedException();
529
+ }
530
+ const name = args[0];
531
+ if (typeof name !== 'string') {
532
+ throw new BadRequestException('Profile name must be a string.');
533
+ }
534
+ return { name, subject, updated: true };
535
+ },
536
+ },
537
+ allowedOrigins: ['https://app.example.com'],
538
+ crypto: globalThis.crypto,
539
+ secret: serverFunctionSecret,
540
+ maxBodyBytes: 64 * 1024,
541
+ });
542
+
543
+ export const updateProfileReference = await actions.createReference('updateProfile');
544
+
545
+ @Controller('/_fluo')
546
+ class ReactActionController {
547
+ @Post('/actions')
548
+ invoke(_input: undefined, context: RequestContext) {
549
+ return actions.invoke(context);
550
+ }
551
+ }
552
+ ```
553
+
554
+ Transfer the issued reference to trusted client bootstrap data instead of importing the server
555
+ action module into a client bundle. Then create the callable with an explicit endpoint and
556
+ application-owned fetch implementation:
557
+
558
+ ```ts
559
+ import { createReactServerFunctionClient } from '@fluojs/react/experimental/rsc';
560
+
561
+ const updateProfile = createReactServerFunctionClient({
562
+ endpoint: '/_fluo/actions',
563
+ fetch: globalThis.fetch,
564
+ reference: bootstrap.serverFunctions.updateProfile,
565
+ });
566
+
567
+ await updateProfile('Ada');
568
+ ```
569
+
570
+ Treat every argument as untrusted. Authorization belongs inside each action or in guards surrounding
571
+ the explicit endpoint; a valid reference is not authorization. Action handlers receive the active
572
+ `RequestContext`, so they may resolve request-scoped providers through `context.container` and keep
573
+ request-local state isolated. Server Functions are mutation-oriented integration points, not query
574
+ loaders, a client data cache, or a bypass around DTO validation, guards, middleware, interceptors,
575
+ request scopes, or observability.
576
+
577
+ The transport security contract is intentionally explicit:
578
+
579
+ - Action ids match `[A-Za-z0-9_-]{1,128}`. References expose the id but protect its integrity with
580
+ HMAC-SHA-256 and an application-owned secret of at least 32 bytes. Rotating the secret invalidates
581
+ previously issued references; references do not encrypt action names.
582
+ - Requests must use `application/json`, send `x-fluo-react-action: 1`, and include an exact HTTP(S)
583
+ `Origin` from `allowedOrigins`. The custom header makes browser cross-origin calls non-simple, while
584
+ the origin allowlist supplies the CSRF policy.
585
+ - `maxBodyBytes` defaults to 64 KiB. The registry uses exact `rawBody` bytes when the adapter exposes
586
+ them and otherwise measures the normalized body. Configure the platform adapter's pre-parse body
587
+ limit to the same or lower value so oversized network bodies are rejected before JSON parsing.
588
+ - Arguments and results allow only `null`, booleans, finite numbers, strings, dense arrays, and plain
589
+ objects. Circular values, sparse arrays, symbol/non-enumerable properties, prototype-sensitive
590
+ keys, class instances, `undefined`, `bigint`, and non-finite numbers are rejected. Nesting defaults
591
+ to 32 levels and cannot be configured above 128.
592
+ - Results default to a 1 MiB serialized limit. Client responses also default to 1 MiB.
593
+
594
+ Expected failures use the existing fluo HTTP error envelope with stable codes:
595
+
596
+ | Condition | Status | Code |
597
+ | --- | ---: | --- |
598
+ | malformed request shape | 400 | `REACT_SERVER_FUNCTION_INVALID_REQUEST` |
599
+ | unsafe arguments | 400 | `REACT_SERVER_FUNCTION_ARGUMENT_SERIALIZATION_FAILED` |
600
+ | missing/invalid request marker | 403 | `REACT_SERVER_FUNCTION_CSRF_REJECTED` |
601
+ | missing/unapproved origin | 403 | `REACT_SERVER_FUNCTION_ORIGIN_REJECTED` |
602
+ | invalid, tampered, or retired action reference | 404 | `REACT_SERVER_FUNCTION_ACTION_NOT_FOUND` |
603
+ | oversized request | 413 | `REACT_SERVER_FUNCTION_PAYLOAD_TOO_LARGE` |
604
+ | non-JSON content type | 415 | `REACT_SERVER_FUNCTION_UNSUPPORTED_MEDIA_TYPE` |
605
+ | unsafe action result | 500 | `REACT_SERVER_FUNCTION_RESULT_SERIALIZATION_FAILED` |
606
+ | oversized action result | 500 | `REACT_SERVER_FUNCTION_RESULT_TOO_LARGE` |
607
+
608
+ This prototype does not scan for `"use server"`, transform modules, discover exports, generate
609
+ references automatically, encrypt action ids, or provide database mutation conventions. The stable
610
+ root and `@fluojs/react/client` remain isolated from all Server Function code.
611
+
612
+ ## RSC Graduation Policy
613
+
614
+ `@fluojs/react/rsc` is not currently exported. The canonical
615
+ [React RSC graduation policy](../../docs/contracts/react-rsc-graduation.md) records the evidence that
616
+ must exist before the experimental APIs can become stable: exact React/renderer compatibility,
617
+ versioned manifest and Server Function transport contracts, browser/server bundle separation,
618
+ SSR/CSR/prerendering behavior, hydration mismatch and error recovery, safe transfer rules for
619
+ private/auth/cookie-bearing data, HTTP route ownership, #2506 navigation isolation, executable
620
+ runtime/bundler coverage, bilingual docs, and Changesets release evidence.
621
+
622
+ When every gate is approved, `@fluojs/react/rsc` becomes the canonical implementation while
623
+ `@fluojs/react/experimental/rsc` remains a tested re-export for the documented deprecation window.
624
+ The stable package root remains RSC-free. The policy currently marks graduation as blocked, so this
625
+ documentation change neither adds the stable subpath nor starts the deprecation window.
626
+
627
+ ## Current Limitations
628
+
629
+ This package currently does **not** provide:
630
+
631
+ - a stable RSC root or `@fluojs/react/rsc` subpath; RSC is available only from the explicitly unstable
632
+ `@fluojs/react/experimental/rsc` prototype
633
+ - automatic `"use server"` transforms/export discovery or a built-in Flight renderer/build plugin
634
+ - SPA document swapping, a client data/loader cache, and navigation prefetching
635
+ - a Next.js App Router, TanStack route tree, Angular `Routes[]`, file-route scanner, or React-owned
636
+ `routes: []` table
637
+ - automatic client bundle generation
638
+ - filesystem scanning or automatic manifest file discovery; pass an already-loaded manifest value to
639
+ `@fluojs/react/vite`
640
+ - automatic serialization of arbitrary data into `bootstrapScriptContent`
641
+ - Node-only `react-dom/server` pipeable stream root APIs such as `renderToPipeableStream(...)`
642
+
643
+ ## Public API
644
+
645
+ - `Router` — class decorator that writes HTTP controller metadata plus React router marker metadata.
646
+ - `Path` — method decorator that writes HTTP `GET` route metadata plus React render metadata.
647
+ - `getReactRouterMetadata` — reads React router marker metadata from a router class.
648
+ - `getReactPathMetadata` — reads React render metadata from a router method.
649
+ - `ReactModule` — runtime-neutral module facade whose `forRoot(...)` registers React routers through
650
+ the existing fluo module/controller metadata path.
651
+ - `createReactServerEntry` — creates a runtime-neutral React server entry returned by page handlers
652
+ for Web Streams SSR.
653
+ - `renderReactResponse` — renders one React server entry to a fluo HTML response with lazy
654
+ `react-dom/server` loading.
655
+ - `ReactAssetMap`, `ReactBootstrapAsset`, and `ReactBootstrapScriptDescriptor` — type-only contracts
656
+ for build-produced asset maps and React DOM bootstrap script/module entries.
657
+ - `ReactModuleOptions` — options accepted by `ReactModule.forRoot(...)`, including `controllers`,
658
+ `imports`, `providers`, `exports`, and module-level `middleware`.
659
+ - `ReactServerEntry`, `ReactServerEntryOptions`, `ReactServerEntryHeaders`,
660
+ `ReactRecoverableErrorHandler`, `ReactRecoverableErrorContext`, `ReactRenderContext`,
661
+ `ReactReadableStream`, `ReactReadableStreamRenderer`, `ReactReadableStreamRenderOptions`, and
662
+ `RenderReactResponseOptions` — type-only contracts for streamed React SSR and hydration assets.
663
+ - `ReactScaffoldPhase` — type-only planning marker for the `0.1.0` scaffold surface.
664
+ - `ReactRouterMetadata`, `ReactPathMetadata`, `ReactPathOptions` — type-only metadata contracts for
665
+ diagnostics and future rendering integration.
666
+ - `@fluojs/react/vite` subpath — `createReactViteAssetManifest(...)` plus
667
+ `ReactViteBuildManifest`, `ReactViteBuildManifestChunk`, `ReactViteManifestOptions`,
668
+ `ReactViteManifestDiagnostic`, `ReactViteAssetManifest`, `ReactViteHydrationOptions`,
669
+ `ReactViteJavaScriptAssets`, `ReactViteBootstrapData`, and `ReactViteResolvedEntry` for parsing
670
+ Vite manifests into the stable hydration asset contract without importing Vite from the root.
671
+ - `@fluojs/react/client` subpath — `Link`, `ReactClientRouterProvider`,
672
+ `createReactRouteSnapshot(...)`, `useRouter()`, `usePathname()`, `useParams()`,
673
+ `useSearchParams()`, `useNavigation()`, and `useRouterState()` for progressive HTTP-first browser
674
+ navigation without widening the root package or adding a client route grammar.
675
+ - `@fluojs/react/experimental/rsc` subpath — `inspectReactRscEnvironment(...)`,
676
+ `createReactRscManifest(...)`, `createReactFlightResponse(...)`, exact-version and Flight content
677
+ type constants, diagnostics, client-reference/server-module mapping types, signed
678
+ `createReactServerFunctionRegistry(...)`, explicit `createReactServerFunctionClient(...)`, stable
679
+ Server Function error codes, and related transport types without any root or stable-client
680
+ re-export.
681
+
682
+ ## Related Packages
683
+
684
+ - `@fluojs/core`: Provides the standard `@Module` decorator used by the scaffold.
685
+ - `@fluojs/http`: Provides the controller, route, DTO, guard, interceptor, header, and version
686
+ metadata pipeline reused by `@Router(...)` and `@Path(...)`.
687
+ - `@fluojs/runtime`: Future React integration work is expected to compose with runtime bootstrap
688
+ contracts without widening the root import boundary.
689
+ - `@fluojs/vite`: Owns Vite's TC39 decorator transform boundary. It does not parse React hydration
690
+ manifests; use `@fluojs/react/vite` for React server/client asset mapping.
691
+ - Application-selected Flight renderer: Encodes RSC payloads and consumes renderer-specific build
692
+ manifests. The experimental subpath models the compatibility, manifest, and HTTP response seams
693
+ without importing a renderer package.
694
+
695
+ ## Example Sources
696
+
697
+ - `packages/react/src/index.ts`
698
+ - `packages/react/src/vite.ts`
699
+ - `packages/react/src/client.ts`
700
+ - `packages/react/src/client.test.ts`
701
+ - `packages/react/src/experimental/rsc.ts`
702
+ - `packages/react/src/experimental/rsc.test.ts`
703
+ - `packages/react/src/experimental/rsc-diagnostics.test.ts`
704
+ - `packages/react/src/experimental/rsc-flight.test.ts`
705
+ - `packages/react/src/experimental/rsc-manifest.test.ts`
706
+ - `packages/react/src/experimental/server-functions-server.ts`
707
+ - `packages/react/src/experimental/server-functions-client.ts`
708
+ - `packages/react/src/experimental/server-functions-dispatch.test.ts`
709
+ - `packages/react/src/experimental/server-functions-security.test.ts`
710
+ - `packages/react/src/experimental/server-functions-client.test.ts`
711
+ - `packages/react/src/vite/create-asset-manifest.ts`
712
+ - `packages/react/src/decorators.ts`
713
+ - `packages/react/src/server-entry.ts`
714
+ - `packages/react/src/render.ts`
715
+ - `packages/react/src/module.ts`
716
+ - `packages/react/src/render.test.ts`
717
+ - `packages/react/src/dispatcher-ssr.test.ts`
718
+ - `packages/react/src/hydration-assets.test.ts`
719
+ - `packages/react/src/vite.test.ts`
720
+ - `packages/react/src/lifecycle-pipeline.test.ts`
721
+ - `packages/react/src/module.test.ts`
722
+ - `packages/react/src/decorators.test.ts`
723
+ - `packages/react/src/index.test.ts`
724
+ - `examples/react-stable-ssr/README.md`
725
+ - `examples/react-stable-ssr/src/app.test.ts`
726
+ - `examples/react-vite-ssr/README.md`
727
+ - `examples/react-vite-ssr/src/app.test.ts`
728
+ - `examples/react-vite-ssr/src/hydration.test.ts`
729
+ - `examples/react-vite-ssr/tests/production-hydration.spec.ts`