@valbuild/tanstack 0.124.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 (50) hide show
  1. package/LICENSE.md +7 -0
  2. package/README.md +380 -0
  3. package/client/dist/valbuild-tanstack-client.cjs.d.ts +2 -0
  4. package/client/dist/valbuild-tanstack-client.cjs.dev.js +288 -0
  5. package/client/dist/valbuild-tanstack-client.cjs.js +7 -0
  6. package/client/dist/valbuild-tanstack-client.cjs.prod.js +288 -0
  7. package/client/dist/valbuild-tanstack-client.esm.js +280 -0
  8. package/client/package.json +4 -0
  9. package/dist/ValOverlayContext-87cb022d.esm.js +284 -0
  10. package/dist/ValOverlayContext-abd6ab9a.cjs.prod.js +292 -0
  11. package/dist/ValOverlayContext-d3bfacb1.cjs.dev.js +292 -0
  12. package/dist/createForOfIteratorHelper-0324e063.cjs.prod.js +157 -0
  13. package/dist/createForOfIteratorHelper-485c2ce1.esm.js +151 -0
  14. package/dist/createForOfIteratorHelper-e75681d7.cjs.dev.js +157 -0
  15. package/dist/declarations/src/ValApp.d.ts +6 -0
  16. package/dist/declarations/src/ValImage.d.ts +24 -0
  17. package/dist/declarations/src/ValModulesClient.d.ts +34 -0
  18. package/dist/declarations/src/ValProvider.d.ts +6 -0
  19. package/dist/declarations/src/ValTypes.d.ts +4 -0
  20. package/dist/declarations/src/client/index.d.ts +1 -0
  21. package/dist/declarations/src/client/initValClient.d.ts +27 -0
  22. package/dist/declarations/src/decodeValPathsOfString.d.ts +2 -0
  23. package/dist/declarations/src/external_exempt_from_val_quickjs.d.ts +41 -0
  24. package/dist/declarations/src/getUnpatchedUnencodedVal.d.ts +3 -0
  25. package/dist/declarations/src/index.d.ts +2 -0
  26. package/dist/declarations/src/initVal.d.ts +74 -0
  27. package/dist/declarations/src/server/index.d.ts +5 -0
  28. package/dist/declarations/src/server/initValContent.d.ts +122 -0
  29. package/dist/declarations/src/server/initValMcp.d.ts +27 -0
  30. package/dist/declarations/src/server/initValServer.d.ts +41 -0
  31. package/dist/declarations/src/server/valDraftMode.d.ts +49 -0
  32. package/dist/declarations/src/version.d.ts +1 -0
  33. package/dist/routeFromVal-6693e037.cjs.prod.js +218 -0
  34. package/dist/routeFromVal-728295a0.esm.js +212 -0
  35. package/dist/routeFromVal-a9147ceb.cjs.dev.js +218 -0
  36. package/dist/valbuild-tanstack.cjs.d.ts +2 -0
  37. package/dist/valbuild-tanstack.cjs.dev.js +2005 -0
  38. package/dist/valbuild-tanstack.cjs.js +7 -0
  39. package/dist/valbuild-tanstack.cjs.prod.js +2005 -0
  40. package/dist/valbuild-tanstack.esm.js +1950 -0
  41. package/dist/version-1a88fd21.cjs.dev.js +229 -0
  42. package/dist/version-5ac70ccf.cjs.prod.js +229 -0
  43. package/dist/version-ac8df696.esm.js +225 -0
  44. package/package.json +84 -0
  45. package/server/dist/valbuild-tanstack-server.cjs.d.ts +2 -0
  46. package/server/dist/valbuild-tanstack-server.cjs.dev.js +1032 -0
  47. package/server/dist/valbuild-tanstack-server.cjs.js +7 -0
  48. package/server/dist/valbuild-tanstack-server.cjs.prod.js +1032 -0
  49. package/server/dist/valbuild-tanstack-server.esm.js +1005 -0
  50. package/server/package.json +4 -0
package/LICENSE.md ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2025 Fredrik Ekholdt and Blank AS
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,380 @@
1
+ <p align="center">
2
+ <p align="center">
3
+ <a href="https://admin.val.build">
4
+ <svg width="100" height="100" viewBox="0 0 944 944" fill="none" xmlns="http://www.w3.org/2000/svg">
5
+ <circle cx="472" cy="472" r="472" fill="#1D1C28"/>
6
+ <g filter="url(#filter0_d_14_601)">
7
+ <path d="M181 348C181 345.791 182.791 344 185 344H320C322.209 344 324 345.791 324 348V602C324 604.209 322.209 606 320 606H185C182.791 606 181 604.209 181 602V348Z" fill="#38CD80"/>
8
+ </g>
9
+ <g filter="url(#filter1_i_14_601)">
10
+ <circle cx="252" cy="550" r="24" fill="#1E1F2A"/>
11
+ </g>
12
+ <path d="M659.085 550.374H658.585H654.427C652.095 550.374 650.434 549.729 649.347 548.522C648.25 547.306 647.658 545.431 647.658 542.807V439.857C647.658 437.924 646.091 436.357 644.158 436.357H629.16C627.227 436.357 625.66 437.924 625.66 439.857V455.828C625.66 456.658 624.986 457.332 624.155 457.332C623.593 457.332 623.072 457.015 622.798 456.508C618.502 448.559 612.704 442.661 605.399 438.838C597.962 434.671 589.622 432.592 580.394 432.592C571.897 432.592 563.846 434.128 556.247 437.2C548.643 440.274 541.944 444.796 536.154 450.761L536.153 450.761C530.537 456.552 526.106 463.693 522.854 472.174C519.598 480.668 517.975 490.411 517.975 501.395V505.697C517.975 516.86 519.597 526.693 522.854 535.187C526.105 543.667 530.535 550.895 536.148 556.864L536.153 556.869L536.159 556.875C541.95 562.659 548.647 567.088 556.246 570.161L556.256 570.165C563.856 573.057 572.083 574.5 580.932 574.5C589.456 574.5 597.527 572.325 605.137 567.982C612.625 563.807 618.519 557.469 622.822 548.992C623.085 548.475 623.609 548.147 624.176 548.147H624.546C625.161 548.147 625.66 548.646 625.66 549.261C625.66 555.468 627.583 560.617 631.452 564.665L631.451 564.665L631.46 564.673C635.511 568.72 640.668 570.735 646.889 570.735H658.585H659.085H661.157H661.657H760C761.933 570.735 763.5 569.168 763.5 567.235V553.874C763.5 551.941 761.933 550.374 760 550.374H733.542C732.161 550.374 731.042 549.255 731.042 547.874V385C731.042 383.067 729.475 381.5 727.542 381.5H680.701C678.768 381.5 677.201 383.067 677.201 385V398.361C677.201 400.294 678.768 401.861 680.701 401.861H706.543C707.924 401.861 709.043 402.981 709.043 404.361V547.874C709.043 549.255 707.924 550.374 706.543 550.374H661.657H661.157H659.085ZM600.117 550.146L600.111 550.149C594.977 552.448 589.304 553.601 583.086 553.601C570.468 553.601 560.194 549.435 552.222 541.12C544.436 532.633 540.512 520.847 540.512 505.697V501.395C540.512 494.274 541.581 487.79 543.712 481.936L543.714 481.931C545.849 475.89 548.778 470.842 552.495 466.775C556.398 462.521 560.92 459.246 566.061 456.944C571.195 454.645 576.867 453.492 583.086 453.492C589.117 453.492 594.696 454.731 599.829 457.207L599.838 457.211L599.848 457.215C605.166 459.517 609.681 462.79 613.4 467.035L613.4 467.035L613.408 467.044C617.306 471.292 620.324 476.431 622.458 482.469L622.459 482.474C624.59 488.328 625.66 494.812 625.66 501.933V505.16C625.66 512.46 624.59 519.125 622.458 525.159C620.324 531.022 617.393 536.075 613.669 540.326C609.95 544.571 605.435 547.844 600.117 550.146ZM464.902 570.735C466.39 570.735 467.716 569.794 468.206 568.389L512.685 441.011C513.479 438.736 511.79 436.357 509.38 436.357H491.006C489.496 436.357 488.157 437.325 487.683 438.758L447.951 558.864C447.716 559.575 447.051 560.055 446.303 560.055C445.554 560.055 444.89 559.575 444.655 558.864L404.923 438.758C404.449 437.325 403.109 436.357 401.6 436.357H383.225C380.815 436.357 379.126 438.736 379.921 441.011L424.399 568.389C424.89 569.794 426.215 570.735 427.704 570.735H464.902Z" fill="white" stroke="white"/>
13
+ <defs>
14
+ <filter id="filter0_d_14_601" x="127.464" y="290.464" width="250.072" height="369.072" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
15
+ <feFlood flood-opacity="0" result="BackgroundImageFix"/>
16
+ <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
17
+ <feOffset/>
18
+ <feGaussianBlur stdDeviation="26.768"/>
19
+ <feComposite in2="hardAlpha" operator="out"/>
20
+ <feColorMatrix type="matrix" values="0 0 0 0 0.219608 0 0 0 0 0.803922 0 0 0 0 0.501961 0 0 0 0.3 0"/>
21
+ <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_14_601"/>
22
+ <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_14_601" result="shape"/>
23
+ </filter>
24
+ <filter id="filter1_i_14_601" x="228" y="526" width="48" height="48" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
25
+ <feFlood flood-opacity="0" result="BackgroundImageFix"/>
26
+ <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
27
+ <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
28
+ <feOffset/>
29
+ <feGaussianBlur stdDeviation="6"/>
30
+ <feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
31
+ <feColorMatrix type="matrix" values="0 0 0 0 0.219608 0 0 0 0 0.803922 0 0 0 0 0.501961 0 0 0 0.3 0"/>
32
+ <feBlend mode="normal" in2="shape" result="effect1_innerShadow_14_601"/>
33
+ </filter>
34
+ </defs>
35
+ </svg>
36
+ </a>
37
+ <br/>
38
+
39
+ # @valbuild/tanstack
40
+
41
+ Val for [TanStack Start](https://tanstack.com/start) (React).
42
+
43
+ Content lives in `.val.ts` files in your repository — type-checked, refactorable,
44
+ reviewable in a pull request — and non-developers edit it in Val Studio, which
45
+ this package mounts at `/val`.
46
+
47
+ ## Table of contents
48
+
49
+ - [Installation](#installation)
50
+ - [Wiring it up](#wiring-it-up)
51
+ - [Reading content](#reading-content)
52
+ - [Routes](#routes)
53
+ - [Preview and draft mode](#preview-and-draft-mode)
54
+ - [Images](#images)
55
+ - [Coding agents (MCP)](#coding-agents-mcp)
56
+ - [Differences from `@valbuild/next`](#differences-from-valbuildnext)
57
+ - [Schema reference](#schema-reference)
58
+
59
+ ## Installation
60
+
61
+ ```sh
62
+ npm install @valbuild/tanstack
63
+ npm install --save-dev @valbuild/cli @valbuild/eslint-plugin
64
+ ```
65
+
66
+ Requires TanStack Start ≥ 1.130 and React 19.
67
+
68
+ **Tell the route generator that `*.val.ts` files are not routes.** A Val module
69
+ for a route lives beside the route file, and the generator scans everything
70
+ under `src/routes` — so without this it reads `posts.$postId.val.ts` as a route
71
+ at `/posts/$postId/val` and warns on every run:
72
+
73
+ ```ts
74
+ // vite.config.ts
75
+ tanstackStart({
76
+ router: { routeFileIgnorePattern: "\\.val\\.[tj]sx?$" },
77
+ });
78
+ ```
79
+
80
+ ```json
81
+ // tsr.config.json — the same, for the standalone `tsr generate` CLI
82
+ { "routeFileIgnorePattern": "\\.val\\.[tj]sx?$" }
83
+ ```
84
+
85
+ ## Wiring it up
86
+
87
+ ### `val.config.ts`
88
+
89
+ ```ts
90
+ import { initVal } from "@valbuild/tanstack";
91
+
92
+ const { s, c, val, config, tanstackRouter, externalPageRouter } = initVal({
93
+ project: "yourorg/your-project", // omit while running locally
94
+ });
95
+
96
+ export type { t } from "@valbuild/tanstack";
97
+ export { s, c, val, config, tanstackRouter, externalPageRouter };
98
+ ```
99
+
100
+ ### `val.modules.ts`
101
+
102
+ ```ts
103
+ import { modules } from "@valbuild/tanstack";
104
+ import { config } from "./val.config";
105
+
106
+ export default modules(config, [
107
+ { def: () => import("./src/routes/index.val") },
108
+ { def: () => import("./src/content/authors.val") },
109
+ ]);
110
+ ```
111
+
112
+ ### `src/val/server.ts` — the API and the server-side readers
113
+
114
+ ```ts
115
+ import { initValServer, initValContent } from "@valbuild/tanstack/server";
116
+ import { config } from "../../val.config";
117
+ import valModules from "../../val.modules";
118
+
119
+ const { valApiHandler, draftMode } = initValServer(valModules, { ...config });
120
+
121
+ export const {
122
+ fetchValStega: fetchVal,
123
+ fetchValRouteStega: fetchValRoute,
124
+ fetchValKeyStega: fetchValKey,
125
+ fetchValRouteUrl,
126
+ } = initValContent(config, valModules, { draftMode });
127
+
128
+ export { valApiHandler };
129
+ ```
130
+
131
+ Build both from one `draftMode` object. The API is what turns preview on for a
132
+ browser and the readers are what has to notice; two independently created
133
+ defaults would each work and disagree.
134
+
135
+ ### `src/routes/api/val.$.ts` — mount the API
136
+
137
+ ```ts
138
+ import { createFileRoute } from "@tanstack/react-router";
139
+ import { valApiHandler } from "../../val/server";
140
+
141
+ export const Route = createFileRoute("/api/val/$")({
142
+ server: {
143
+ handlers: {
144
+ GET: ({ request }) => valApiHandler(request),
145
+ POST: ({ request }) => valApiHandler(request),
146
+ PUT: ({ request }) => valApiHandler(request),
147
+ PATCH: ({ request }) => valApiHandler(request),
148
+ DELETE: ({ request }) => valApiHandler(request),
149
+ HEAD: ({ request }) => valApiHandler(request),
150
+ },
151
+ },
152
+ });
153
+ ```
154
+
155
+ `/api/val/$`, not `/api/val`: every endpoint has a sub-path, so the splat is the
156
+ whole surface.
157
+
158
+ ### `src/routes/val/` — Val Studio
159
+
160
+ Three files, because the Studio navigates within itself (it pushes paths like
161
+ `/val/~/...`, which have to resolve to the same page on a reload):
162
+
163
+ ```tsx
164
+ // src/routes/val/route.tsx
165
+ import { Outlet, createFileRoute } from "@tanstack/react-router";
166
+ import { ValApp, ValModulesClient } from "@valbuild/tanstack";
167
+ import { config } from "../../../val.config";
168
+ import valModules from "../../../val.modules";
169
+
170
+ export const Route = createFileRoute("/val")({
171
+ component: () => (
172
+ <ValApp config={config}>
173
+ <ValModulesClient modules={valModules} />
174
+ <Outlet />
175
+ </ValApp>
176
+ ),
177
+ });
178
+ ```
179
+
180
+ ```tsx
181
+ // src/routes/val/index.tsx and src/routes/val/$.tsx
182
+ import { createFileRoute } from "@tanstack/react-router";
183
+ export const Route = createFileRoute("/val/")({ component: () => null }); // and "/val/$"
184
+ ```
185
+
186
+ ### The site's layout — `ValProvider`
187
+
188
+ Put the site's own chrome and `ValProvider` in a **pathless layout route**, not
189
+ in `__root`. `__root` is the shell for every route including `/val`, and the
190
+ Studio should not be rendered inside the site it is editing.
191
+
192
+ ```tsx
193
+ // src/routes/_site.tsx
194
+ import { Outlet, createFileRoute } from "@tanstack/react-router";
195
+ import { Suspense } from "react";
196
+ import { ValModulesClient, ValProvider } from "@valbuild/tanstack";
197
+ import { config } from "../../val.config";
198
+ import valModules from "../../val.modules";
199
+
200
+ export const Route = createFileRoute("/_site")({
201
+ component: () => (
202
+ <ValProvider config={config} suspend>
203
+ <ValModulesClient modules={valModules} />
204
+ <Suspense fallback={null}>
205
+ <Outlet />
206
+ </Suspense>
207
+ </ValProvider>
208
+ ),
209
+ });
210
+ ```
211
+
212
+ A pathless layout adds no URL segment: `_site.index.tsx` is still `/`. Val
213
+ modules named after those files follow the same rule, so
214
+ `_site.posts.$postId.val.ts` holds `/posts/...` keys.
215
+
216
+ **The `<Suspense>` boundary is required when you pass `suspend`.** With none
217
+ between a suspending component and the root, React has nowhere to put a
218
+ fallback and the tree stops updating — which looks like the Studio failing to
219
+ load. TanStack Start provides no boundary of its own.
220
+
221
+ ### `src/val/client.ts` — the hooks
222
+
223
+ ```ts
224
+ import { initValClient } from "@valbuild/tanstack/client";
225
+ import { config } from "../../val.config";
226
+
227
+ export const {
228
+ useValStega: useVal,
229
+ useValRouteStega: useValRoute,
230
+ useValRouteUrl,
231
+ } = initValClient(config);
232
+ ```
233
+
234
+ ## Reading content
235
+
236
+ **Prefer the hooks.** They work in both places a component runs: during SSR they
237
+ resolve the published content, and in a browser with the Studio open they
238
+ resolve what the editor currently holds — so an edit appears as it is typed,
239
+ with no round trip and no loader.
240
+
241
+ ```tsx
242
+ function Page() {
243
+ const page = useValRoute(pageVal, Route.useParams());
244
+ const authors = useVal(authorsVal);
245
+ if (page === null) throw notFound();
246
+ return <h1>{page.title}</h1>;
247
+ }
248
+ ```
249
+
250
+ **Read on the server when the content has to exist before the component does** —
251
+ `head`/meta tags, a redirect decided by content, a `notFound()` that must happen
252
+ during the request. Two things to know:
253
+
254
+ 1. It has to go through `createServerFn`. A route `loader` runs in the browser
255
+ too — that is what makes a client navigation work — so importing
256
+ `src/val/server.ts` straight into a loader pulls `@valbuild/server`, and
257
+ Node's `fs` with it, into the client bundle. `createServerFn` is compiled
258
+ away on the client and everything only its handler uses goes with it.
259
+ 2. Content that arrives through a loader on a first, server-rendered load is not
260
+ click-to-editable. The edit tags are attached as JSX is created, and Val only
261
+ starts attaching them once hydration has told it the Studio is open — by
262
+ which time the component has already rendered its loader data. A client-side
263
+ navigation to the same route tags it normally.
264
+
265
+ ```tsx
266
+ const getDoc = createServerFn()
267
+ .validator((params: { slug: string }) => params)
268
+ .handler(async ({ data }) => ({ doc: await fetchValRoute(pageVal, data) }));
269
+
270
+ export const Route = createFileRoute("/_site/docs/$slug")({
271
+ loader: async ({ params }) => {
272
+ const { doc } = await getDoc({ data: params });
273
+ if (!doc) throw notFound();
274
+ return { doc };
275
+ },
276
+ component: Doc,
277
+ });
278
+ ```
279
+
280
+ ## Routes
281
+
282
+ A Val module for a route **is named after the route file it sits beside**: the
283
+ `.tsx` becomes `.val.ts`. Its keys are the URLs that route serves.
284
+
285
+ | Route file | Val module | Keys look like |
286
+ | ---------------------------------- | ------------------------------------- | -------------------- |
287
+ | `src/routes/index.tsx` | `src/routes/index.val.ts` | `/` |
288
+ | `src/routes/about.tsx` | `src/routes/about.val.ts` | `/about` |
289
+ | `src/routes/posts.$postId.tsx` | `src/routes/posts.$postId.val.ts` | `/posts/hello-world` |
290
+ | `src/routes/posts/$postId.tsx` | `src/routes/posts/$postId.val.ts` | the same route |
291
+ | `src/routes/docs.$.tsx` (splat) | `src/routes/docs.$.val.ts` | `/docs/a/b` |
292
+ | `src/routes/_site.posts.$id.tsx` | `src/routes/_site.posts.$id.val.ts` | `/posts/1` |
293
+ | `src/routes/(marketing)/about.tsx` | `src/routes/(marketing)/about.val.ts` | `/about` |
294
+
295
+ `.` and `/` both separate segments, `$param` is a parameter, `$` on its own is a
296
+ splat, and `index`, `route`, `(groups)` and `_pathless` layouts contribute no
297
+ URL segment — exactly TanStack Router's own rules.
298
+
299
+ ```ts
300
+ // src/routes/posts.$postId.val.ts
301
+ import { s, c, tanstackRouter } from "../../val.config";
302
+
303
+ export default c.define(
304
+ "/src/routes/posts.$postId.val.ts",
305
+ s.router(tanstackRouter, s.object({ title: s.string() })),
306
+ { "/posts/hello-world": { title: "Hello world" } },
307
+ );
308
+ ```
309
+
310
+ Then, in the route's component, hand `useValRoute` the route's own params
311
+ unchanged — including `_splat`, which is what TanStack calls a splat parameter
312
+ and what Val expects:
313
+
314
+ ```tsx
315
+ const post = useValRoute(pageVal, Route.useParams());
316
+ ```
317
+
318
+ Val validates every key against the route's pattern, so a key that no URL of
319
+ that route could produce is a content error rather than a page that silently
320
+ never renders. Val Studio shows these modules as a sitemap under **Pages**, and
321
+ an editor can add a page there — which creates the key.
322
+
323
+ For links out of your site, `externalPageRouter` takes whole URLs instead.
324
+
325
+ ## Preview and draft mode
326
+
327
+ Val's preview shows unpublished edits. TanStack Start has no `draftMode()` of
328
+ its own, so this package brings a cookie (`val_draft_mode`, `valDraftMode()` in
329
+ `@valbuild/tanstack/server`). It is a mode switch, not a credential: every draft
330
+ read also carries Val's session cookie, which is a signed JWT the server
331
+ verifies, so a forged draft cookie gets published content.
332
+
333
+ `suspend` on `ValProvider` makes `useValStega` / `useValRouteStega` wait for
334
+ draft data before rendering, so a page that exists **only** in an unpublished
335
+ draft renders instead of 404ing. Visitors without the Val Enable cookie pay
336
+ nothing for it. It needs React 19 and a `<Suspense>` boundary (see above).
337
+
338
+ ## Images
339
+
340
+ `ValImage` is a plain `<img>` that keeps the edit tags where the Studio can see
341
+ them, and carries the source's own `width`/`height` and its `hotspot` as
342
+ `object-position`:
343
+
344
+ ```tsx
345
+ import { ValImage } from "@valbuild/tanstack";
346
+
347
+ <ValImage src={page.hero.image} style={{ maxWidth: "20rem" }} />;
348
+ ```
349
+
350
+ Uploads land in `public/val` by default, which Vite serves at `/val/...`. That
351
+ shares a prefix with the Studio route, and Vite's static handling wins, so both
352
+ work — but if you would rather keep them apart, `s.image({ directory })` takes
353
+ another location.
354
+
355
+ ## Coding agents (MCP)
356
+
357
+ `initValMcp` from `@valbuild/tanstack/server` gives an MCP host Val's content
358
+ tools — read schemas, look content up, validate it, edit it. It is the same
359
+ implementation `@valbuild/next` uses; mount it on a server route with the MCP
360
+ SDK of your choice. See [`@valbuild/mcp`](https://www.npmjs.com/package/@valbuild/mcp).
361
+
362
+ ## Differences from `@valbuild/next`
363
+
364
+ | Next | TanStack Start |
365
+ | ----------------------------------- | -------------------------------------------------- |
366
+ | `nextAppRouter` | `tanstackRouter` |
367
+ | `app/blogs/[blog]/page.val.ts` | `src/routes/blogs.$blog.val.ts` |
368
+ | `initValRsc` (`@valbuild/next/rsc`) | `initValContent` (`@valbuild/tanstack/server`) |
369
+ | `draftMode()` from `next/headers` | `valDraftMode()` (a cookie this package owns) |
370
+ | `valNextAppRouter` | `valApiHandler` — a `Request` in, a `Response` out |
371
+ | `ValImage` wraps `next/image` | `ValImage` is an `<img>` |
372
+ | `fetchVal` in a Server Component | hooks in components; `createServerFn` for loaders |
373
+
374
+ ## Schema reference
375
+
376
+ The schema types — `s.string()`, `s.richtext()`, `s.image()`, `s.record()`,
377
+ `s.union()`, `s.keyOf()`, `s.route()`, and the rest — are the same in every Val
378
+ package and are documented in full at
379
+ [val.build/docs](https://val.build/docs) and in
380
+ [`@valbuild/next`'s README](https://github.com/valbuild/val/blob/main/packages/next/README.md#schema-types).
@@ -0,0 +1,2 @@
1
+ export * from "../../dist/declarations/src/client/index.js";
2
+ //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmFsYnVpbGQtdGFuc3RhY2stY2xpZW50LmNqcy5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vZGlzdC9kZWNsYXJhdGlvbnMvc3JjL2NsaWVudC9pbmRleC5kLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBIn0=
@@ -0,0 +1,288 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var routeFromVal = require('../../dist/routeFromVal-a9147ceb.cjs.dev.js');
6
+ var core = require('@valbuild/core');
7
+ var stega = require('@valbuild/react/stega');
8
+ var React = require('react');
9
+ var ValOverlayContext = require('../../dist/ValOverlayContext-d3bfacb1.cjs.dev.js');
10
+ require('../../dist/createForOfIteratorHelper-e75681d7.cjs.dev.js');
11
+ require('@valbuild/shared/client');
12
+ require('@valbuild/core/patch');
13
+ require('react/jsx-runtime');
14
+
15
+ function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
16
+
17
+ var React__default = /*#__PURE__*/_interopDefault(React);
18
+
19
+ function useValStega(selector) {
20
+ var valOverlayContext = ValOverlayContext.useValOverlayContext();
21
+ var moduleIds = React__default["default"].useMemo(function () {
22
+ return stega.getModuleIds(selector);
23
+ }, [selector]);
24
+ var store = valOverlayContext.store;
25
+ var moduleMap = React__default["default"].useSyncExternalStore(store ? store.subscribe(moduleIds) : function () {
26
+ return function () {};
27
+ }, store ? store.getSnapshot(moduleIds) : function () {
28
+ return;
29
+ }, store ? store.getServerSnapshot(moduleIds) : function () {
30
+ return;
31
+ });
32
+ // Suspense gate. `suspend` is false during SSR and hydration (so the static
33
+ // committed source is rendered, matching the server HTML exactly) and is
34
+ // activated by ValProvider after hydration — inside a transition — when the
35
+ // `suspend` prop is set AND the Val Enable cookie is present (checked
36
+ // client-side; the server store is never populated). It never deactivates.
37
+ // The production path (no cookie) skips the call entirely. The
38
+ // `draftMode !== false` check is a release valve: with draft mode off the
39
+ // store never receives source updates, so waitForLoad could only ever
40
+ // resolve via its timeout — and would then re-suspend on every subsequent
41
+ // render since the resolved promise is evicted from the cache. draftMode is
42
+ // null until the first /draft/stat poll resolves; null -> true keeps
43
+ // suspending, -> false only unblocks, and false -> true happens only on an
44
+ // explicit draft-mode enable which already refreshes the route.
45
+ // React.use is allowed inside conditionals — it is not a hook.
46
+ /**
47
+ * Wait until draft mode is KNOWN, before anything else.
48
+ *
49
+ * `draftMode === null` means `/draft/stat` has not answered yet, and the
50
+ * `getModule` below treats it as off — so a render that slips through here
51
+ * while it is unknown resolves against committed source. For an ordinary field
52
+ * that is a flash of published content; for `useValRoute` on a route that
53
+ * exists only in an uncommitted patch it is `notFound()`, which no later
54
+ * answer can undo. That was the 404 on a page you had just created.
55
+ */
56
+ if (valOverlayContext.suspend && valOverlayContext.draftMode === null && valOverlayContext.draftModeReady) {
57
+ React__default["default"].use(valOverlayContext.draftModeReady);
58
+ }
59
+ /**
60
+ * Then wait for the draft sources — but only while more might be coming.
61
+ *
62
+ * `draftSourcesSynced` is the editor saying it has sent everything it holds,
63
+ * and it only holds modules with patches: an unedited module has no draft, so
64
+ * nothing is ever sent for it. Without that signal this could not tell "not
65
+ * sent yet" from "nothing to send", and waited out `waitForLoad`'s ten second
66
+ * timeout once per unedited module the page reads — which is what left a newly
67
+ * created page sitting on its loading fallback.
68
+ */
69
+ if (valOverlayContext.suspend && valOverlayContext.draftMode !== false && !valOverlayContext.draftSourcesSynced && store && !store.hasAllLoaded(moduleIds)) {
70
+ React__default["default"].use(store.waitForLoad(moduleIds));
71
+ }
72
+ return stega.stegaEncode(selector, {
73
+ disabled: !valOverlayContext.draftMode,
74
+ getModule: function getModule(moduleId) {
75
+ if (moduleMap && valOverlayContext.draftMode) {
76
+ return moduleMap[moduleId];
77
+ }
78
+ }
79
+ });
80
+ }
81
+
82
+ /**
83
+ * The module's source as the overlay currently sees it — i.e. WITH the editor's
84
+ * unpublished changes — or undefined when there is no draft view to be had
85
+ * (production, draft mode off, or the overlay has not pushed this module yet).
86
+ *
87
+ * This is what lets the single-entry readers show drafts. They otherwise resolve
88
+ * an entry through its local import thunk, which is the content that was bundled:
89
+ * correct in production, and stale the moment anyone edits in the Studio.
90
+ */
91
+ function useDraftModuleSource(moduleFilePath) {
92
+ var valOverlayContext = ValOverlayContext.useValOverlayContext();
93
+ var store = valOverlayContext.store;
94
+ var moduleIds = React__default["default"].useMemo(function () {
95
+ return moduleFilePath ? [moduleFilePath] : [];
96
+ }, [moduleFilePath]);
97
+ var moduleMap = React__default["default"].useSyncExternalStore(store ? store.subscribe(moduleIds) : function () {
98
+ return function () {};
99
+ }, store ? store.getSnapshot(moduleIds) : function () {
100
+ return undefined;
101
+ }, store ? store.getServerSnapshot(moduleIds) : function () {
102
+ return undefined;
103
+ });
104
+ if (!valOverlayContext.draftMode || !moduleFilePath) {
105
+ return undefined;
106
+ }
107
+ return moduleMap === null || moduleMap === void 0 ? void 0 : moduleMap[moduleFilePath];
108
+ }
109
+
110
+ /**
111
+ * What the draft view says about one `.jsonValues()` entry.
112
+ *
113
+ * Mirrors the server-side rule in `fetchValKey`/`fetchValRoute`: a draft view
114
+ * that HAS an answer wins, including the answer "this entry is gone", and the
115
+ * bundled content is used only when there is no draft view.
116
+ */
117
+ function draftJsonEntry(draftSource, key) {
118
+ if (draftSource === undefined || draftSource === null || routeFromVal._typeof(draftSource) !== "object" || Array.isArray(draftSource)) {
119
+ return {
120
+ status: "unavailable"
121
+ };
122
+ }
123
+ var entry = draftSource[key];
124
+ if (entry === undefined) {
125
+ // The module IS in the draft view and this key is not: it was deleted.
126
+ return {
127
+ status: "absent"
128
+ };
129
+ }
130
+ if (core.Internal.isJson(entry)) {
131
+ // An un-loaded marker: the Studio has not fetched this entry's content, so
132
+ // the draft view cannot answer. (The engine asks for entries that pending
133
+ // patches touch, so this resolves itself for anything actually edited.)
134
+ return {
135
+ status: "unavailable"
136
+ };
137
+ }
138
+ return {
139
+ status: "content",
140
+ content: entry
141
+ };
142
+ }
143
+
144
+ // The (loosened) content type a single `.jsonValues()` entry resolves to.
145
+
146
+ // Module-level cache of in-flight/resolved entry loads, so `React.use` gets a
147
+ // stable promise across renders (keyed by module path + entry key).
148
+ var jsonEntryPromiseCache = new Map();
149
+
150
+ /**
151
+ * Client counterpart to `fetchValKey`: resolves a SINGLE `.jsonValues()` entry
152
+ * by key, loading only that entry's backing `*.val.json` (one dynamic import).
153
+ * Suspends (via `React.use`) until the entry loads, so it must be rendered
154
+ * inside a `<Suspense>` boundary.
155
+ *
156
+ * In draft mode it renders the editor's unpublished content, taken from the
157
+ * overlay; in production — and whenever there is no draft view — it resolves the
158
+ * entry's lazy import thunk from the local module.
159
+ */
160
+ function useValKeyStega(selector, key) {
161
+ var valOverlayContext = ValOverlayContext.useValOverlayContext();
162
+ var moduleFilePath = selector && core.Internal.getValPath(selector);
163
+ var draftSource = useDraftModuleSource(moduleFilePath || undefined);
164
+ var draft = draftJsonEntry(draftSource, key);
165
+ if (draft.status === "absent") {
166
+ // Deleted in the draft state. Falling back to the bundled entry here would
167
+ // render content the editor has just removed.
168
+ return undefined;
169
+ }
170
+ var content = draft.status === "content" ? draft.content : undefined;
171
+ if (content === undefined) {
172
+ content = readCommittedJsonEntry(selector, key);
173
+ }
174
+ return stega.stegaEncode(content, {
175
+ disabled: !valOverlayContext.draftMode,
176
+ root: routeFromVal.getJsonEntryStegaRoot(selector, key)
177
+ });
178
+ }
179
+
180
+ /**
181
+ * The entry's content as bundled: its lazy import thunk, resolved through
182
+ * `React.use` so the caller suspends until it lands.
183
+ *
184
+ * Deliberately not named `use*`: it is called conditionally, which is fine
185
+ * because `React.use` is not a hook, but a `use*` name would read like one.
186
+ */
187
+ function readCommittedJsonEntry(selector, key) {
188
+ var _Internal$getValPath;
189
+ var source = selector && core.Internal.getSource(selector);
190
+ var marker = source && routeFromVal._typeof(source) === "object" ? source[key] : undefined;
191
+ if (!core.Internal.isJson(marker)) {
192
+ return undefined;
193
+ }
194
+ var thunk = core.Internal.getJsonImport(marker);
195
+ if (!thunk) {
196
+ return undefined;
197
+ }
198
+ var cacheKey = "".concat((_Internal$getValPath = core.Internal.getValPath(selector)) !== null && _Internal$getValPath !== void 0 ? _Internal$getValPath : "", " ").concat(key);
199
+ var promise = jsonEntryPromiseCache.get(cacheKey);
200
+ if (!promise) {
201
+ promise = thunk().then(function (mod) {
202
+ return mod["default"];
203
+ });
204
+ jsonEntryPromiseCache.set(cacheKey, promise);
205
+ }
206
+ return React__default["default"].use(promise);
207
+ }
208
+ function resolveParams(params) {
209
+ if (!params) {
210
+ return null;
211
+ }
212
+ if ("then" in params) {
213
+ // Defensive guard: peerDependencies declare React >=19, but if a consumer
214
+ // somehow ends up on React 18 with a promise params arg, surface a
215
+ // diagnosable error instead of a cryptic `TypeError: React.use is not a
216
+ // function`. Callers treat null as the error sentinel.
217
+ if (!("use" in React__default["default"])) {
218
+ console.error("Val: useValRoute received a Promise params argument but React.use is unavailable. Upgrade to React 19+ or pre-resolve the promise before passing it.");
219
+ return null;
220
+ }
221
+ return React__default["default"].use(params);
222
+ }
223
+ return params;
224
+ }
225
+ function useValRouteStega(selector, params) {
226
+ var valOverlayContext = ValOverlayContext.useValOverlayContext();
227
+ // Both called unconditionally to keep hook order stable. For a `.jsonValues()`
228
+ // router `val` is unused (we resolve a single entry below instead); for any
229
+ // other router `draftSource` is.
230
+ var val = useValStega(selector);
231
+ var draftSource = useDraftModuleSource(selector && core.Internal.getValPath(selector) || undefined);
232
+ var resolvedParams = resolveParams(params);
233
+ // Careful: null means there was an error - undefined means no params
234
+ if (resolvedParams === null) {
235
+ return null;
236
+ }
237
+ var path = selector && core.Internal.getValPath(selector);
238
+ var schema = selector && core.Internal.getSchema(selector);
239
+ // `.jsonValues()` router: map params → the entry key and load ONLY that
240
+ // entry's backing `*.val.json` (one dynamic import), like `useValKey`.
241
+ if (routeFromVal.isJsonValuesRecordSchema(schema)) {
242
+ var source = selector && core.Internal.getSource(selector);
243
+ var url = routeFromVal.getValRouteUrlFromVal(resolvedParams || {}, "useValRoute", path, schema, source);
244
+ if (!url) {
245
+ return null;
246
+ }
247
+ var draft = draftJsonEntry(draftSource, url);
248
+ if (draft.status === "absent") {
249
+ // The draft state says this route is gone — see useValKeyStega.
250
+ return null;
251
+ }
252
+ var content = draft.status === "content" ? draft.content : undefined;
253
+ if (content === undefined) {
254
+ content = readCommittedJsonEntry(selector, url);
255
+ }
256
+ if (content === undefined) {
257
+ return null;
258
+ }
259
+ return stega.stegaEncode(content, {
260
+ disabled: !valOverlayContext.draftMode,
261
+ root: routeFromVal.getJsonEntryStegaRoot(selector, url)
262
+ });
263
+ }
264
+ var route = routeFromVal.initValRouteFromVal(resolvedParams || {}, "useValRoute", path, schema, val);
265
+ return route;
266
+ }
267
+ function useValRouteUrl(selector, params) {
268
+ var val = useValStega(selector);
269
+ var resolvedParams = params === undefined ? undefined : resolveParams(params);
270
+ // Careful: null means there was an error - undefined means no params
271
+ if (resolvedParams === null) {
272
+ return null;
273
+ }
274
+ var route = routeFromVal.getValRouteUrlFromVal(resolvedParams || {}, "useValRouteUrl", selector && core.Internal.getValPath(selector), selector && core.Internal.getSchema(selector), val);
275
+ return route;
276
+ }
277
+
278
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
279
+ function initValClient(config) {
280
+ return {
281
+ useValStega: useValStega,
282
+ useValKeyStega: useValKeyStega,
283
+ useValRouteStega: useValRouteStega,
284
+ useValRouteUrl: useValRouteUrl
285
+ };
286
+ }
287
+
288
+ exports.initValClient = initValClient;