@k8ordo/framework 0.0.0-stage → 1.0.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 (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +176 -2
  3. package/dist/generated.d.mts +2 -0
  4. package/dist/generated.mjs +2 -0
  5. package/dist/index.d.mts +2 -0
  6. package/dist/index.mjs +2 -0
  7. package/dist/runtime/app-router.d.mts +26 -0
  8. package/dist/runtime/app-router.mjs +272 -0
  9. package/dist/runtime/browser-payloads.d.mts +8 -0
  10. package/dist/runtime/browser-payloads.mjs +23 -0
  11. package/dist/runtime/cancel.d.mts +11 -0
  12. package/dist/runtime/cancel.mjs +25 -0
  13. package/dist/runtime/cookies.d.mts +50 -0
  14. package/dist/runtime/cookies.mjs +86 -0
  15. package/dist/runtime/entry.browser.d.mts +1 -0
  16. package/dist/runtime/entry.browser.mjs +24 -0
  17. package/dist/runtime/entry.rsc.d.mts +22 -0
  18. package/dist/runtime/entry.rsc.mjs +377 -0
  19. package/dist/runtime/entry.ssr.d.mts +16 -0
  20. package/dist/runtime/entry.ssr.mjs +51 -0
  21. package/dist/runtime/fallback-boundary.d.mts +42 -0
  22. package/dist/runtime/fallback-boundary.mjs +64 -0
  23. package/dist/runtime/guard.d.mts +29 -0
  24. package/dist/runtime/guard.mjs +18 -0
  25. package/dist/runtime/is-payload.d.mts +13 -0
  26. package/dist/runtime/is-payload.mjs +18 -0
  27. package/dist/runtime/mount.d.mts +17 -0
  28. package/dist/runtime/mount.mjs +24 -0
  29. package/dist/runtime/page-boundary.d.mts +39 -0
  30. package/dist/runtime/page-boundary.mjs +57 -0
  31. package/dist/runtime/page-shown.d.mts +13 -0
  32. package/dist/runtime/page-shown.mjs +19 -0
  33. package/dist/runtime/page-watch.d.mts +33 -0
  34. package/dist/runtime/page-watch.mjs +51 -0
  35. package/dist/runtime/params.d.mts +49 -0
  36. package/dist/runtime/params.mjs +43 -0
  37. package/dist/runtime/pathname.d.mts +23 -0
  38. package/dist/runtime/pathname.mjs +32 -0
  39. package/dist/runtime/payload-path.d.mts +5 -0
  40. package/dist/runtime/payload-path.mjs +21 -0
  41. package/dist/runtime/payload.d.mts +77 -0
  42. package/dist/runtime/payload.mjs +30 -0
  43. package/dist/runtime/prefetch.d.mts +44 -0
  44. package/dist/runtime/prefetch.mjs +88 -0
  45. package/dist/runtime/recover.d.mts +36 -0
  46. package/dist/runtime/recover.mjs +49 -0
  47. package/dist/runtime/redirect-file.d.mts +21 -0
  48. package/dist/runtime/redirect-file.mjs +43 -0
  49. package/dist/runtime/redirect.d.mts +28 -0
  50. package/dist/runtime/redirect.mjs +35 -0
  51. package/dist/runtime/reload.d.mts +9 -0
  52. package/dist/runtime/reload.mjs +12 -0
  53. package/dist/runtime/render.d.mts +74 -0
  54. package/dist/runtime/render.mjs +77 -0
  55. package/dist/runtime/request-scope.d.mts +64 -0
  56. package/dist/runtime/request-scope.mjs +134 -0
  57. package/dist/runtime/request.d.mts +22 -0
  58. package/dist/runtime/request.mjs +38 -0
  59. package/dist/runtime/revealed.d.mts +7 -0
  60. package/dist/runtime/revealed.mjs +35 -0
  61. package/dist/runtime/route.d.mts +35 -0
  62. package/dist/runtime/route.mjs +59 -0
  63. package/dist/runtime/says-not-found.d.mts +7 -0
  64. package/dist/runtime/says-not-found.mjs +10 -0
  65. package/dist/runtime/shell.d.mts +7 -0
  66. package/dist/runtime/shell.mjs +13 -0
  67. package/dist/serve.d.mts +23 -0
  68. package/dist/serve.mjs +278 -0
  69. package/dist/server.d.mts +126 -0
  70. package/dist/server.mjs +99 -0
  71. package/dist/shared-dir-CkF4QCWc.mjs +10 -0
  72. package/dist/static-host-dgtttTuZ.mjs +407 -0
  73. package/dist/vercel.d.mts +15 -0
  74. package/dist/vercel.mjs +195 -0
  75. package/dist/vite.d.mts +77 -0
  76. package/dist/vite.mjs +2263 -0
  77. package/docs/GUIDE.md +290 -0
  78. package/docs/llms.txt +185 -0
  79. package/docs/references/actions.md +113 -0
  80. package/docs/references/boundaries.md +171 -0
  81. package/docs/references/csp.md +103 -0
  82. package/docs/references/deploy.md +573 -0
  83. package/docs/references/errors.md +157 -0
  84. package/docs/references/guards.md +163 -0
  85. package/docs/references/params.md +531 -0
  86. package/docs/references/routing.md +497 -0
  87. package/package.json +104 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Koki Sakano
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,177 @@
1
- # Temporary Holding Version
1
+ # @k8ordo/framework
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Turns `src/routes/` into an application of React Server Components, built
4
+ with Vite, and lets the same routes run with or without a server.
5
+ `mode: 'static'` renders every page at build time into files any static host
6
+ serves. `mode: 'server'` renders per request, which is what gives an
7
+ application Server Actions, guards, cookies and a real 404. The routes, the
8
+ boundaries and the request handler are the same in both.
9
+
10
+ Like every [k8ordo](https://ordo.k8o.me) package it assumes React 19 and Server
11
+ Components, uses only what has reached Baseline newly available, and ships no
12
+ polyfills or legacy fallbacks.
13
+
14
+ - **Documentation**: https://ordo.k8o.me/en/framework
15
+ - **Design guide**: [docs/GUIDE.md](docs/GUIDE.md) — shipped inside this package
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install react react-dom server-only
21
+ npm install -D @k8ordo/framework @k8ordo/router vite typescript @types/node @types/react @types/react-dom
22
+ # or
23
+ pnpm add react react-dom server-only
24
+ pnpm add -D @k8ordo/framework @k8ordo/router vite typescript @types/node @types/react @types/react-dom
25
+ ```
26
+
27
+ `@k8ordo/router` is a required peer, so the application and the framework
28
+ share one copy, but the application imports from `@k8ordo/framework`, which
29
+ re-exports what it needs (`href`, `notFound`, `PageProps`, …). Both are dev
30
+ dependencies in either mode: the build bundles what the application runs.
31
+
32
+ ## Peer Dependencies
33
+
34
+ <!-- peers -->
35
+
36
+ | Package | Version | Required | Needed for |
37
+ | ---------------- | ------- | -------- | ------------------------------------------ |
38
+ | `@k8ordo/router` | ^1.0.0 | yes | routing, re-exported by the framework |
39
+ | `react` | ≥19.3.0 | yes | rendering |
40
+ | `react-dom` | ≥19.3.0 | yes | rendering |
41
+ | `vite` | ≥8.0.0 | yes | the build (`framework()` is a Vite plugin) |
42
+
43
+ <!-- /peers -->
44
+
45
+ ## Quick Start
46
+
47
+ ```ts
48
+ // vite.config.ts
49
+ import { framework } from '@k8ordo/framework/vite';
50
+ import { defineConfig } from 'vite';
51
+
52
+ export default defineConfig({
53
+ plugins: [
54
+ framework({
55
+ mode: 'static',
56
+ paths: () => ['/products/1', '/products/2'], // for routes with parameters
57
+ site: 'https://example.com', // and a sitemap.xml
58
+ }),
59
+ ],
60
+ });
61
+ // or: framework({ mode: 'server' }) — rendered per request
62
+ ```
63
+
64
+ ```
65
+ src/routes/
66
+ layout.tsx <html> and <body>; wraps everything through `children`
67
+ page.tsx /
68
+ not-found.tsx static: 404.html — server: a real 404
69
+ error.tsx shown in place of what is below when it throws
70
+ products/[id]/
71
+ page.tsx /products/:id — `export const paramsSchema` types :id
72
+ fallback.tsx static: the shell for the ids paths does not list
73
+ old/redirect.ts /old sends the visitor elsewhere
74
+ feed.xml/route.ts /feed.xml answered by its GET — a Response, not a page
75
+ admin/guard.ts server mode: runs before everything under /admin
76
+ ```
77
+
78
+ ```tsx
79
+ // src/routes/products/[id]/page.tsx
80
+ import { notFound } from '@k8ordo/framework';
81
+ import type { PageProps } from '@k8ordo/framework';
82
+
83
+ export default async function ProductPage({
84
+ params,
85
+ }: PageProps<'/products/:id'>) {
86
+ const product = await findProduct(params.id);
87
+ if (product === undefined) notFound();
88
+ return <h1>{product.name}</h1>;
89
+ }
90
+ ```
91
+
92
+ ```bash
93
+ vite dev # renders per request, with Fast Refresh
94
+ vite build # static: dist/client/ is the site
95
+ ```
96
+
97
+ Under `mode: 'server'` the build is a `dist/` that runs on its own — every
98
+ dependency bundled in, nothing to install where it is deployed — a form
99
+ posts to a Server Action, and a page receives `request`, the headers and the
100
+ cookies, read-only:
101
+
102
+ ```bash
103
+ vite build
104
+ PORT=3000 node dist/server.js
105
+ ```
106
+
107
+ ```ts
108
+ // src/lib/actions.ts
109
+ 'use server';
110
+
111
+ import { href } from '@k8ordo/framework';
112
+ import { redirect } from '@k8ordo/framework/server';
113
+
114
+ export async function createTalk(_previous: FormState, formData: FormData) {
115
+ const parsed = parseForm(talkSchema, formData);
116
+ if (!parsed.success) return parsed.state;
117
+ await insertTalk(parsed.data);
118
+ redirect(href('/talks')); // 303 without JavaScript, a navigation with it
119
+ }
120
+ ```
121
+
122
+ The built handler, `dist/rsc/index.js`, is a plain
123
+ `(request: Request) => Promise<Response>` that needs nothing from its runtime
124
+ but `AsyncLocalStorage`, so Node.js, Bun, Deno and Cloudflare Workers
125
+ (`nodejs_compat`) all run it, and `vercel()` from `@k8ordo/framework/vercel`
126
+ writes a build for Vercel.
127
+
128
+ Under `mode: 'static'` there is no request, so the build refuses by name
129
+ whatever needs one — a `'use server'` module, a `guard.ts`, an import of
130
+ `@k8ordo/framework/server` — in `vite dev` as well: `this application wants
131
+ mode: 'server'`.
132
+
133
+ A value `paths` does not list — a product added after the build — is
134
+ answered by a `fallback.tsx` beside the page: the build writes it once as a
135
+ shell, the layouts in HTML, and the browser draws the page from the URL.
136
+ The build also writes the `_redirects` through which Netlify and Cloudflare
137
+ reach the shell, and `vercel()` the same rewrites for Vercel.
138
+
139
+ ## AI Agent Documentation
140
+
141
+ The docs ship **inside the package**, so an agent always reads the exact
142
+ version you installed — there is no snapshot to copy or re-sync on upgrade.
143
+
144
+ Point your agent at them once by pasting this into your project's `CLAUDE.md` /
145
+ `AGENTS.md`:
146
+
147
+ ```markdown
148
+ This application is built with `@k8ordo/framework` (see `mode` in
149
+ `vite.config.ts`). Before adding or changing a route, read
150
+ `node_modules/@k8ordo/framework/docs/GUIDE.md`, then only the
151
+ `docs/references/*.md` it lists that the task needs. `src/routes/` is the
152
+ pathname space and holds only page.tsx, layout.tsx, not-found.tsx,
153
+ error.tsx, loading.tsx, fallback.tsx, redirect.ts, guard.ts and route.ts;
154
+ components go in `src/components/` and every other module (Server Actions,
155
+ schemas, state definitions, data, helpers) in `src/lib/`, since a
156
+ `_`-prefixed directory there is a URL segment, not a private one. Never
157
+ edit `.k8ordo/` — it is generated, with the `Register` augmentations: the
158
+ route table's, and `@k8ordo/i18n`'s when `src/i18n.ts` exports the locale
159
+ set by the name `locales` (write that one by hand only when the set lives
160
+ elsewhere). Import from `@k8ordo/framework` (never
161
+ `@k8ordo/router`), and the request API from `@k8ordo/framework/server`,
162
+ which `mode: 'static'` refuses. Build links with `href()`; search params
163
+ are `@k8ordo/state`'s.
164
+ ```
165
+
166
+ What each surface gives an agent:
167
+
168
+ | Surface | Where |
169
+ | -------------------------- | ----------------------------------------------------- |
170
+ | Design guide (entry point) | `node_modules/@k8ordo/framework/docs/GUIDE.md` |
171
+ | Reference docs | `node_modules/@k8ordo/framework/docs/references/*.md` |
172
+ | Docs index for LLMs | `docs/llms.txt` · https://ordo.k8o.me/llms.txt |
173
+ | Markdown twin on the web | https://ordo.k8o.me/framework/docs/GUIDE.md |
174
+
175
+ ## License
176
+
177
+ MIT License - see [LICENSE](https://github.com/k35o/k8ordo/blob/main/LICENSE) for details.
@@ -0,0 +1,2 @@
1
+ import { ErrorComponent, ParamsOf, ParamsSchemaFor, ParsedParams, ParsedParamsMap, Register, defineRoutes } from "@k8ordo/router";
2
+ export { type ErrorComponent, type ParamsOf, type ParamsSchemaFor, type ParsedParams, type ParsedParamsMap, type Register, defineRoutes };
@@ -0,0 +1,2 @@
1
+ import { defineRoutes } from "@k8ordo/router";
2
+ export { defineRoutes };
@@ -0,0 +1,2 @@
1
+ import { BoundLinks, BoundParams, ErrorProps, LayoutProps, MatchOptions, MatchablePattern, NavigateToOptions, PageProps, RegisteredNavigablePattern, RegisteredPageParams, RegisteredParams, RegisteredPattern, RouteContext, bindParams, href, isNotFound, matchPath, navigateTo, normalizePathname, notFound, useMatch, usePathname, usePendingPathname, withBase } from "@k8ordo/router";
2
+ export { type BoundLinks, type BoundParams, type ErrorProps, type LayoutProps, type MatchOptions, type MatchablePattern, type NavigateToOptions, type PageProps, type RegisteredNavigablePattern, type RegisteredPageParams, type RegisteredParams, type RegisteredPattern, type RouteContext, bindParams, href, isNotFound, matchPath, navigateTo, normalizePathname, notFound, useMatch, usePathname, usePendingPathname, withBase };
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { bindParams, href, isNotFound, matchPath, navigateTo, normalizePathname, notFound, useMatch, usePathname, usePendingPathname, withBase } from "@k8ordo/router";
2
+ export { bindParams, href, isNotFound, matchPath, navigateTo, normalizePathname, notFound, useMatch, usePathname, usePendingPathname, withBase };
@@ -0,0 +1,26 @@
1
+ import { Payload } from "./payload.mjs";
2
+ import { ReactNode } from "react";
3
+ //#region src/runtime/app-router.d.ts
4
+ export declare const setDocumentClient: (client: string) => void;
5
+ /** What a Server Action's answer does to the document, once it is read. */
6
+ export declare const answerAction: (payload: Payload) => Promise<unknown>;
7
+ export declare const setPayloadReader: (read: (stream: ReadableStream<Uint8Array>) => Promise<Payload>) => void;
8
+ /**
9
+ * The client half under the framework: the tree comes from the server, so
10
+ * there is no route table in the browser at all — only navigation. The
11
+ * router supplies that (interception, a deferred render, and resolving the
12
+ * platform's `finished` once the new tree is on screen); this component
13
+ * supplies what to load, which is the next page's RSC payload.
14
+ *
15
+ * A pathname the application does not have is still the server's answer: it
16
+ * renders `not-found.tsx` and this renders whatever came back.
17
+ */
18
+ export declare function AppRouter({ pathname, search, tree, notFound }: {
19
+ pathname: string;
20
+ /** The search the page was rendered with, when it reads the search. */
21
+ search?: string | undefined;
22
+ tree: ReactNode;
23
+ /** A shell's not-found, when the tree is a shell. */
24
+ notFound?: ReactNode | undefined;
25
+ }): ReactNode;
26
+ //#endregion
@@ -0,0 +1,272 @@
1
+ "use client";
2
+ import { payloadPathFor } from "./payload-path.mjs";
3
+ import { ShowNotFound } from "./fallback-boundary.mjs";
4
+ import { isPayload } from "./is-payload.mjs";
5
+ import { createPrefetchCache, prefetchTargetOf } from "./prefetch.mjs";
6
+ import { Recover, markNavigated, reloadInstead } from "./recover.mjs";
7
+ import { NavigationGeneration, PathnameProvider, useInterceptedNavigation, withoutBase } from "@k8ordo/router";
8
+ import { startTransition, useDeferredValue, useEffect, useRef, useState } from "react";
9
+ import { jsx } from "react/jsx-runtime";
10
+ //#region src/runtime/app-router.tsx
11
+ /**
12
+ * Where a Server Action's answer lands. The callback has to be registered
13
+ * before any client component can call one, which is earlier than a component
14
+ * can offer its own setter — so `answerAction` is module-level and the
15
+ * mounted router lends it a way to apply what comes back, and to drop the
16
+ * pages it fetched ahead, which the action may have changed.
17
+ */
18
+ let mounted = null;
19
+ /**
20
+ * The client this document runs, as named by the payload its HTML was
21
+ * rendered from — the same render that told the HTML which script to load.
22
+ * A fact about the document, not about a component, which is why it is not
23
+ * a prop.
24
+ */
25
+ let documentClient;
26
+ const setDocumentClient = (client) => {
27
+ documentClient = client;
28
+ };
29
+ /**
30
+ * A payload rendered for another client comes from a deploy this document
31
+ * predates, and may name client components its script has no entry for.
32
+ * That fails only once the tree renders, where the page's `error.tsx` catches
33
+ * it before `Recover` can — so it is decided before anything renders, and a
34
+ * document load brings the script that can.
35
+ */
36
+ const canRender = (payload) => payload.client === documentClient;
37
+ /** What a Server Action's answer does to the document, once it is read. */
38
+ const answerAction = async (payload) => {
39
+ mounted?.forgetPrefetched();
40
+ if (payload.redirect !== void 0) {
41
+ await navigation.navigate(payload.redirect).finished;
42
+ return;
43
+ }
44
+ if (!canRender(payload)) return reloadInstead();
45
+ mounted?.apply(payload);
46
+ return payload.returnValue;
47
+ };
48
+ let readPayload = () => Promise.reject(/* @__PURE__ */ new Error("k8ordo: payloads are read in the browser only"));
49
+ const setPayloadReader = (read) => {
50
+ readPayload = read;
51
+ };
52
+ /**
53
+ * The request each fetched payload is still streaming in on. A payload
54
+ * resolves once its top arrives, and a page that awaits its data arrives
55
+ * after — so once its tree is applied, what is still to come is part of a
56
+ * page React is rendering. Cancelled then, the abort would be thrown inside
57
+ * that page, where `Recover` takes it for a page that failed and loads the
58
+ * document. The signal a payload was fetched under cancels it only until its
59
+ * tree is applied (`keep`); after that, the router cancels it once another
60
+ * tree has replaced it.
61
+ */
62
+ const streams = /* @__PURE__ */ new WeakMap();
63
+ /**
64
+ * A page's payload, or `null` when what came back is not one — a file the
65
+ * host serves, or a URL nothing answers. Reading it imports the client
66
+ * components it names, so a prefetch has those in hand too.
67
+ */
68
+ const fetchPage = async (payloadPath, signal) => {
69
+ signal.throwIfAborted();
70
+ const request = new AbortController();
71
+ const cancel = () => {
72
+ request.abort(signal.reason);
73
+ };
74
+ signal.addEventListener("abort", cancel, { once: true });
75
+ const response = await fetch(payloadPath, { signal: request.signal });
76
+ if (!isPayload(response)) return null;
77
+ const payload = await readPayload(response.body);
78
+ streams.set(payload, {
79
+ cancel: () => {
80
+ request.abort();
81
+ },
82
+ keep: () => {
83
+ signal.removeEventListener("abort", cancel);
84
+ }
85
+ });
86
+ return payload;
87
+ };
88
+ /** How many payloads this document has applied. It has one router. */
89
+ let applies = 0;
90
+ /**
91
+ * Takes over the request of a payload whose tree is being applied, and
92
+ * returns what applying it puts in state.
93
+ */
94
+ const hold = (streaming, payload) => {
95
+ applies += 1;
96
+ const stream = streams.get(payload);
97
+ if (stream !== void 0) {
98
+ stream.keep();
99
+ streaming.push({
100
+ trees: payload.notFound === void 0 ? [payload.tree] : [payload.tree, payload.notFound],
101
+ cancel: stream.cancel,
102
+ order: applies
103
+ });
104
+ }
105
+ return {
106
+ tree: payload.tree,
107
+ notFound: payload.notFound,
108
+ order: applies
109
+ };
110
+ };
111
+ /**
112
+ * The router's prefetch cache, made the first time it is asked for. It is
113
+ * only ever asked for in an event or an effect; made as `useRef`'s initial
114
+ * value it would be built, and thrown away, on every render.
115
+ */
116
+ const cacheIn = (ref) => ref.current ??= createPrefetchCache((payloadPath, signal) => fetchPage(payloadPath, signal));
117
+ /**
118
+ * Where a page's payload is asked for: its path, with the URL's search. A page
119
+ * that reads the search is rendered for it; for any other page the server
120
+ * ignores it — and a static host, which has no such page, ignores it anyway.
121
+ */
122
+ const payloadUrlFor = (url) => `${payloadPathFor(url.pathname)}${url.search}`;
123
+ /** What starts a prefetch: a pointer onto a link, focus on one, a press. */
124
+ const INTENTS = [
125
+ "pointerover",
126
+ "focusin",
127
+ "pointerdown"
128
+ ];
129
+ /**
130
+ * The client half under the framework: the tree comes from the server, so
131
+ * there is no route table in the browser at all — only navigation. The
132
+ * router supplies that (interception, a deferred render, and resolving the
133
+ * platform's `finished` once the new tree is on screen); this component
134
+ * supplies what to load, which is the next page's RSC payload.
135
+ *
136
+ * A pathname the application does not have is still the server's answer: it
137
+ * renders `not-found.tsx` and this renders whatever came back.
138
+ */
139
+ function AppRouter({ pathname, search, tree, notFound }) {
140
+ const [latest, setLatest] = useState(() => ({
141
+ tree,
142
+ notFound,
143
+ order: 0
144
+ }));
145
+ const current = useDeferredValue(latest);
146
+ const prefetched = useRef(null);
147
+ const searchApplied = useRef(search);
148
+ const streaming = useRef([]);
149
+ const loading = useRef(0);
150
+ useEffect(() => {
151
+ streaming.current = streaming.current.filter((entry) => {
152
+ if (entry.order > latest.order) return true;
153
+ if (entry.trees.includes(latest.tree) || entry.trees.includes(current.tree)) return true;
154
+ entry.cancel();
155
+ return false;
156
+ });
157
+ }, [latest, current]);
158
+ useEffect(() => {
159
+ mounted = {
160
+ apply: (payload) => {
161
+ searchApplied.current = payload.search;
162
+ const next = hold(streaming.current, payload);
163
+ startTransition(() => {
164
+ setLatest(next);
165
+ });
166
+ },
167
+ forgetPrefetched: () => {
168
+ cacheIn(prefetched).clear();
169
+ }
170
+ };
171
+ return () => {
172
+ mounted = null;
173
+ };
174
+ }, []);
175
+ useEffect(() => {
176
+ if (import.meta.hot === void 0) return void 0;
177
+ let inFlight;
178
+ const onServerUpdate = async () => {
179
+ inFlight?.abort();
180
+ const controller = new AbortController();
181
+ inFlight = controller;
182
+ cacheIn(prefetched).clear();
183
+ const url = location.href;
184
+ let payload;
185
+ try {
186
+ payload = await fetchPage(payloadUrlFor(new URL(url)), controller.signal);
187
+ } catch (error) {
188
+ if (controller.signal.aborted) return;
189
+ throw error;
190
+ }
191
+ if (payload === null || location.href !== url) return;
192
+ if (!canRender(payload)) return reloadInstead();
193
+ markNavigated();
194
+ const next = hold(streaming.current, payload);
195
+ searchApplied.current = payload.search;
196
+ startTransition(() => {
197
+ setLatest(next);
198
+ });
199
+ };
200
+ const listener = () => {
201
+ onServerUpdate();
202
+ };
203
+ import.meta.hot.on("rsc:update", listener);
204
+ return () => {
205
+ inFlight?.abort();
206
+ import.meta.hot?.off("rsc:update", listener);
207
+ };
208
+ }, []);
209
+ useEffect(() => {
210
+ const onIntent = (event) => {
211
+ const url = prefetchTargetOf(event.target);
212
+ if (url !== null) cacheIn(prefetched).prefetch(payloadUrlFor(url));
213
+ };
214
+ for (const type of INTENTS) document.addEventListener(type, onIntent, {
215
+ capture: true,
216
+ passive: true
217
+ });
218
+ return () => {
219
+ for (const type of INTENTS) document.removeEventListener(type, onIntent, { capture: true });
220
+ };
221
+ }, []);
222
+ const { generation } = useInterceptedNavigation({
223
+ claim: (url) => url.origin === location.origin && withoutBase(url.pathname) !== null,
224
+ load: async (url, signal) => {
225
+ loading.current += 1;
226
+ try {
227
+ const payloadPath = payloadUrlFor(url);
228
+ let payload;
229
+ try {
230
+ payload = await (cacheIn(prefetched).take(payloadPath, signal) ?? fetchPage(payloadPath, signal));
231
+ } catch (error) {
232
+ if (signal.aborted) throw error;
233
+ return await reloadInstead();
234
+ }
235
+ signal.throwIfAborted();
236
+ if (payload === null) return await reloadInstead();
237
+ if (!canRender(payload)) return await reloadInstead();
238
+ return payload;
239
+ } finally {
240
+ loading.current -= 1;
241
+ }
242
+ },
243
+ apply: (next) => {
244
+ markNavigated();
245
+ searchApplied.current = next.search;
246
+ setLatest(hold(streaming.current, next));
247
+ },
248
+ refresh: (url) => searchApplied.current !== void 0 && url.search !== searchApplied.current
249
+ });
250
+ const shown = current;
251
+ const show = () => {
252
+ if (loading.current > 0) return false;
253
+ setLatest((previous) => previous === shown ? {
254
+ tree: shown.notFound,
255
+ notFound: void 0,
256
+ order: shown.order
257
+ } : previous);
258
+ return true;
259
+ };
260
+ return /* @__PURE__ */ jsx(PathnameProvider, {
261
+ pathname,
262
+ children: /* @__PURE__ */ jsx(NavigationGeneration, {
263
+ value: generation,
264
+ children: /* @__PURE__ */ jsx(Recover, { children: /* @__PURE__ */ jsx(ShowNotFound, {
265
+ value: current.notFound === void 0 ? null : show,
266
+ children: current.tree
267
+ }) })
268
+ })
269
+ });
270
+ }
271
+ //#endregion
272
+ export { AppRouter, answerAction, setDocumentClient, setPayloadReader };
@@ -0,0 +1,8 @@
1
+ //#region src/runtime/browser-payloads.d.ts
2
+ /**
3
+ * Hands app-router the browser's Flight client: how a fetched payload is
4
+ * read, and where a Server Action is sent. Called by the browser entry
5
+ * before anything renders.
6
+ */
7
+ export declare const connectAppRouter: () => void;
8
+ //#endregion
@@ -0,0 +1,23 @@
1
+ import { ACTION_ID_HEADER } from "./payload.mjs";
2
+ import { answerAction, setPayloadReader } from "./app-router.mjs";
3
+ import { createFromFetch, createFromReadableStream, createTemporaryReferenceSet, encodeReply, setServerCallback } from "@vitejs/plugin-rsc/browser";
4
+ //#region src/runtime/browser-payloads.ts
5
+ /**
6
+ * Hands app-router the browser's Flight client: how a fetched payload is
7
+ * read, and where a Server Action is sent. Called by the browser entry
8
+ * before anything renders.
9
+ */
10
+ const connectAppRouter = () => {
11
+ setPayloadReader((stream) => createFromReadableStream(stream));
12
+ setServerCallback(async (id, args) => {
13
+ const temporaryReferences = createTemporaryReferenceSet();
14
+ const payload = await createFromFetch(fetch(location.href, {
15
+ method: "POST",
16
+ headers: { [ACTION_ID_HEADER]: id },
17
+ body: await encodeReply(args, { temporaryReferences })
18
+ }), { temporaryReferences });
19
+ return answerAction(payload);
20
+ });
21
+ };
22
+ //#endregion
23
+ export { connectAppRouter };
@@ -0,0 +1,11 @@
1
+ //#region src/runtime/cancel.d.ts
2
+ /**
3
+ * `stream` as its reader reads it, with `cancelled` told first when that
4
+ * reader gives up — a visitor who left, a navigation overtaken — before the
5
+ * cancellation reaches the render writing into `stream`. A render that is
6
+ * cancelled reports everything it was still rendering as failed, with the
7
+ * reason the host gave, and only what is told first can tell that apart from
8
+ * a page that failed.
9
+ */
10
+ export declare const noticeCancel: <T>(stream: ReadableStream<T>, cancelled: (reason: unknown) => void) => ReadableStream<T>;
11
+ //#endregion
@@ -0,0 +1,25 @@
1
+ //#region src/runtime/cancel.ts
2
+ /**
3
+ * `stream` as its reader reads it, with `cancelled` told first when that
4
+ * reader gives up — a visitor who left, a navigation overtaken — before the
5
+ * cancellation reaches the render writing into `stream`. A render that is
6
+ * cancelled reports everything it was still rendering as failed, with the
7
+ * reason the host gave, and only what is told first can tell that apart from
8
+ * a page that failed.
9
+ */
10
+ const noticeCancel = (stream, cancelled) => {
11
+ const reader = stream.getReader();
12
+ return new ReadableStream({
13
+ async pull(controller) {
14
+ const { done, value } = await reader.read();
15
+ if (done) controller.close();
16
+ else controller.enqueue(value);
17
+ },
18
+ cancel(reason) {
19
+ cancelled(reason);
20
+ return reader.cancel(reason);
21
+ }
22
+ });
23
+ };
24
+ //#endregion
25
+ export { noticeCancel };
@@ -0,0 +1,50 @@
1
+ //#region src/runtime/cookies.d.ts
2
+ /**
3
+ * How a cookie is kept. The defaults are the ones a session wants: sent on
4
+ * every path, never readable from a page's script, only over HTTPS, and not
5
+ * on another site's POST.
6
+ */
7
+ export type CookieOptions = {
8
+ /** Default `/`. */
9
+ readonly path?: string;
10
+ readonly domain?: string;
11
+ /** Seconds until it expires; `0` expires it now. */
12
+ readonly maxAge?: number;
13
+ readonly expires?: Date;
14
+ /** Default `true`: a page's script cannot read it. */
15
+ readonly httpOnly?: boolean;
16
+ /**
17
+ * Default `true`: sent only over HTTPS. On plain HTTP to this machine —
18
+ * `localhost`, `127.0.0.1` or `[::1]` — the default is `false`, because
19
+ * Safari drops a `Secure` cookie there. Say `false` for plain HTTP
20
+ * anywhere else.
21
+ */
22
+ readonly secure?: boolean;
23
+ /** Default `'lax'`. `'none'` needs `secure`, and defaults to it. */
24
+ readonly sameSite?: 'strict' | 'lax' | 'none';
25
+ };
26
+ /** Where a cookie lives, which is what deleting one has to name again. */
27
+ export type CookieScope = Pick<CookieOptions, 'path' | 'domain'>;
28
+ /**
29
+ * The request's cookies, and what this answer does to them. A read sees the
30
+ * writes made earlier in the same request; the browser sees them as
31
+ * `Set-Cookie` on the answer.
32
+ */
33
+ export type Cookies = {
34
+ readonly get: (name: string) => string | undefined;
35
+ readonly has: (name: string) => boolean;
36
+ readonly set: (name: string, value: string, options?: CookieOptions) => void;
37
+ readonly delete: (name: string, scope?: CookieScope) => void;
38
+ };
39
+ /**
40
+ * The jar a request's answer writes through: the cookies the request
41
+ * carried, overlaid with what was set or deleted since, and the
42
+ * `Set-Cookie` lines that say so. A cookie set twice at the same path and
43
+ * domain is said once, the last way. `url` is the request's, which decides
44
+ * whether a cookie is `Secure` by default.
45
+ */
46
+ export declare const createCookies: (incoming: ReadonlyMap<string, string>, url: URL) => {
47
+ readonly cookies: Cookies;
48
+ readonly lines: () => string[];
49
+ };
50
+ //#endregion