@rsc-kit/core 0.18.1 → 0.19.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 (60) hide show
  1. package/dist/action.d.ts +16 -2
  2. package/dist/action.js +21 -12
  3. package/dist/action.js.map +1 -1
  4. package/dist/apiPrerender.js +24 -6
  5. package/dist/apiPrerender.js.map +1 -1
  6. package/dist/clientPackages.d.ts +30 -0
  7. package/dist/clientPackages.js +234 -0
  8. package/dist/clientPackages.js.map +1 -0
  9. package/dist/compress.d.ts +12 -0
  10. package/dist/compress.js +134 -0
  11. package/dist/compress.js.map +1 -0
  12. package/dist/compressRuntime.d.ts +8 -0
  13. package/dist/compressRuntime.js +62 -0
  14. package/dist/compressRuntime.js.map +1 -0
  15. package/dist/files.d.ts +20 -0
  16. package/dist/files.js +38 -0
  17. package/dist/files.js.map +1 -1
  18. package/dist/formSubmit.d.ts +1 -0
  19. package/dist/formSubmit.js +14 -0
  20. package/dist/formSubmit.js.map +1 -0
  21. package/dist/host.d.ts +23 -0
  22. package/dist/host.js +175 -31
  23. package/dist/host.js.map +1 -1
  24. package/dist/hostCalls.d.ts +15 -0
  25. package/dist/hostCalls.js +75 -8
  26. package/dist/hostCalls.js.map +1 -1
  27. package/dist/js/Form.d.ts +19 -2
  28. package/dist/js/Form.js +110 -83
  29. package/dist/js/Form.js.map +1 -1
  30. package/dist/js/createViteRscApp.d.ts +1 -0
  31. package/dist/js/createViteRscApp.js +34 -5
  32. package/dist/js/createViteRscApp.js.map +1 -1
  33. package/dist/js/errors.d.ts +3 -1
  34. package/dist/js/errors.js +26 -3
  35. package/dist/js/errors.js.map +1 -1
  36. package/dist/js/formEncoding.d.ts +72 -3
  37. package/dist/js/formEncoding.js +284 -20
  38. package/dist/js/formEncoding.js.map +1 -1
  39. package/dist/js/navigate.d.ts +3 -0
  40. package/dist/js/navigate.js +56 -6
  41. package/dist/js/navigate.js.map +1 -1
  42. package/dist/js/updateStore.js +8 -2
  43. package/dist/js/updateStore.js.map +1 -1
  44. package/dist/openapi.d.ts +74 -0
  45. package/dist/openapi.js +172 -0
  46. package/dist/openapi.js.map +1 -0
  47. package/dist/redirect.d.ts +2 -2
  48. package/dist/redirect.js.map +1 -1
  49. package/dist/request.d.ts +56 -2
  50. package/dist/request.js +68 -4
  51. package/dist/request.js.map +1 -1
  52. package/dist/routes.d.ts +15 -1
  53. package/dist/routes.js.map +1 -1
  54. package/dist/testing.d.ts +14 -0
  55. package/dist/testing.js +56 -2
  56. package/dist/testing.js.map +1 -1
  57. package/dist/vite.d.ts +69 -1
  58. package/dist/vite.js +451 -45
  59. package/dist/vite.js.map +1 -1
  60. package/package.json +5 -1
@@ -1,37 +1,301 @@
1
- // Turning an object back into FormData.
1
+ // Turning an object into FormData, and FormData back into an object.
2
2
  //
3
3
  // The one place the encoding decisions live, so a value sent through `<Form>`'s
4
- // `transform` is encoded exactly as one sent any other way. It used to exist
5
- // twice — here and inline in Form.tsx — which is two places for a boolean to
6
- // start meaning something different.
4
+ // `transform` is encoded exactly as one sent any other way, and - the part
5
+ // that mattered - the object an ACTION decodes is the same object the FORM
6
+ // validated. The decoder lived in Form.tsx and understood nested names;
7
+ // the action had a flat one of its own. A form with `items[0].name` passed
8
+ // validation in the browser and failed the same schema on the server, and
9
+ // nothing said why. Both sides call this now.
7
10
  /**
8
11
  * Serialize form state into FormData for a server action.
9
12
  *
10
- * The contract between a form and the action it calls — booleans become "1"/"0" so PHP sees something truthy, arrays repeat under
11
- * `key[]`, Files pass through untouched for native uploads, and null/undefined
12
- * are dropped rather than sent as the string "null".
13
+ * The contract between a form and the action it calls: booleans become "1"/"0"
14
+ * so PHP sees something truthy, Files pass through untouched for native
15
+ * uploads, null/undefined are dropped rather than sent as the string "null",
16
+ * and a nested value nests - `{ items: [{ name }] }` is `items[0][name]`,
17
+ * `{ address: { city } }` is `address[city]`, a list of scalars repeats under
18
+ * `key[]`. Every one of those decodes back to what was given.
13
19
  */
14
20
  export function buildFormData(data) {
15
21
  const formData = new FormData();
16
- for (const [key, val] of Object.entries(data)) {
17
- if (val === null || val === undefined) {
22
+ for (const [key, val] of Object.entries(data))
23
+ appendValue(formData, key, val);
24
+ return formData;
25
+ }
26
+ function appendValue(formData, key, val) {
27
+ if (val === null || val === undefined)
28
+ return;
29
+ if (val instanceof File || val instanceof Blob) {
30
+ formData.append(key, val);
31
+ }
32
+ else if (typeof val === "boolean") {
33
+ formData.append(key, val ? "1" : "0");
34
+ }
35
+ else if (Array.isArray(val)) {
36
+ // Scalars repeat under key[]; objects and nested lists take an index, so
37
+ // each element's fields stay together on the way back.
38
+ val.forEach((item, i) => {
39
+ if (item !== null && typeof item === "object" && !(item instanceof File) && !(item instanceof Blob)) {
40
+ appendValue(formData, `${key}[${i}]`, item);
41
+ }
42
+ else {
43
+ appendValue(formData, `${key}[]`, item);
44
+ }
45
+ });
46
+ }
47
+ else if (typeof val === "object" && !(val instanceof Date)) {
48
+ for (const [prop, inner] of Object.entries(val)) {
49
+ appendValue(formData, `${key}[${prop}]`, inner);
50
+ }
51
+ }
52
+ else if (val instanceof Date) {
53
+ formData.append(key, val.toISOString());
54
+ }
55
+ else {
56
+ formData.append(key, String(val));
57
+ }
58
+ }
59
+ /**
60
+ * The pieces of a field name: `items[0].name` is items, 0, name.
61
+ *
62
+ * Both spellings, because both are in use and a form should not care which one
63
+ * a person reached for: `items[0].name` and `items[0][name]` are the same
64
+ * field. A trailing `[]` is a piece of its own - see below.
65
+ */
66
+ function pathOf(name) {
67
+ return name
68
+ .replace(/\[(\w*)\]/g, ".$1")
69
+ .split(".")
70
+ .filter((piece, index, all) => piece !== "" || index === all.length - 1);
71
+ }
72
+ /** Whether a piece names an array index rather than a property. */
73
+ const isIndex = (piece) => /^\d+$/.test(piece);
74
+ /**
75
+ * Put one value at one path, making the containers it passes through.
76
+ *
77
+ * Whether a container is an array or an object is decided by the NEXT piece, so
78
+ * `items[0].name` makes an array holding an object without being told which is
79
+ * which.
80
+ */
81
+ function place(root, path, value) {
82
+ let node = root;
83
+ for (let i = 0; i < path.length - 1; i++) {
84
+ const key = path[i];
85
+ const container = node;
86
+ if (container[key] === undefined || typeof container[key] !== "object") {
87
+ // An index makes an array, and so does the empty piece a trailing `[]`
88
+ // leaves - `tags[]` has to reach an array to be pushed into, and building
89
+ // an object there is how this first went wrong.
90
+ const next = path[i + 1];
91
+ container[key] = isIndex(next) || next === "" ? [] : {};
92
+ }
93
+ node = container[key];
94
+ }
95
+ const last = path[path.length - 1];
96
+ // The empty piece a trailing `[]` leaves: push rather than assign, so
97
+ // `tags[]` twice is two entries rather than one overwriting the other.
98
+ if (last === "")
99
+ node.push(value);
100
+ else
101
+ node[last] = value;
102
+ }
103
+ /**
104
+ * A FormData as the object a schema expects.
105
+ *
106
+ * Four things beyond copying entries across, and each of them was a bug or a
107
+ * gap someone would meet on their first non-trivial form:
108
+ *
109
+ * A repeated name is an array. Three checkboxes sharing a name, a multiple
110
+ * select, a list of tags - this used to keep the LAST one and drop the rest
111
+ * silently, so a schema validated an object the person had not submitted.
112
+ *
113
+ * A name ending in `[]` is always an array, even with one value selected.
114
+ * Otherwise a list of checkboxes is a string when one is ticked and an array
115
+ * when two are, and no schema can describe both. It is also what `useForm`
116
+ * writes when it serialises an array, so the two round-trip.
117
+ *
118
+ * Nested names nest. `address.city` and `items[0].name` build the object they
119
+ * describe, which is the shape the schema was written against - and the shape
120
+ * whose validation errors come back keyed the same way, because Standard
121
+ * Schema issue paths are joined with dots too.
122
+ *
123
+ * Files are kept. They were dropped for being non-strings, which meant a schema
124
+ * checking an upload was handed undefined and refused a file that was there.
125
+ */
126
+ export function formDataToObject(formData) {
127
+ const obj = {};
128
+ for (const name of new Set(formData.keys())) {
129
+ const all = formData.getAll(name);
130
+ const path = pathOf(name);
131
+ // A plain name used more than once is the array case, and it has no
132
+ // brackets to say so - `tags` twice is `['a', 'b']`.
133
+ if (path.length === 1 && path[0] !== "" && all.length > 1) {
134
+ obj[path[0]] = all;
18
135
  continue;
19
136
  }
20
- if (val instanceof File) {
21
- formData.append(key, val);
137
+ for (const value of all)
138
+ place(obj, path, value);
139
+ }
140
+ return obj;
141
+ }
142
+ /**
143
+ * What a schema is asked for. A leaf JSON Schema cannot say - a Date, a
144
+ * Map, a custom check - is `{}` rather than a refusal of the whole schema:
145
+ * Zod reads `unrepresentable` from here, and a form whose schema has one
146
+ * `z.date()` beside twenty fields still reads the twenty the way they mean.
147
+ * A library that does not know the option ignores it.
148
+ */
149
+ const JSON_SCHEMA_OPTIONS = { target: "draft-2020-12", libraryOptions: { unrepresentable: "any" } };
150
+ const jsonSchemas = new WeakMap();
151
+ /**
152
+ * The JSON Schema a Standard Schema describes itself with, if it can.
153
+ *
154
+ * Zod 4 and ArkType can; Valibot needs its separate converter and answers
155
+ * nothing here, in which case the form's values are handed to the schema as
156
+ * posted and the schema has to read strings.
157
+ */
158
+ function jsonSchemaOf(schema) {
159
+ if (schema === null || typeof schema !== "object")
160
+ return null;
161
+ const cached = jsonSchemas.get(schema);
162
+ if (cached !== undefined)
163
+ return cached;
164
+ let json = null;
165
+ const produce = schema["~standard"]?.jsonSchema?.input;
166
+ try {
167
+ if (typeof produce === "function") {
168
+ json = produce(JSON_SCHEMA_OPTIONS);
169
+ }
170
+ }
171
+ catch (error) {
172
+ json = null;
173
+ // A schema that has the method and still refused: every field of this
174
+ // form now arrives as the string it was posted, and a z.boolean() in it
175
+ // fails on "on" - which looks like the form's fault. Said once, in
176
+ // development, with the library's own reason.
177
+ if (typeof produce === "function" && import.meta.env?.DEV) {
178
+ console.warn("[rsc-kit] this form's schema cannot describe itself as JSON Schema, so its values are " +
179
+ "validated as the strings the form posted: " +
180
+ (error instanceof Error ? error.message : String(error)));
22
181
  }
23
- else if (typeof val === "boolean") {
24
- formData.append(key, val ? "1" : "0");
182
+ }
183
+ jsonSchemas.set(schema, json);
184
+ return json;
185
+ }
186
+ const hasType = (schema, type) => Array.isArray(schema.type) ? schema.type.includes(type) : schema.type === type;
187
+ /**
188
+ * Which branch of a union the value is for.
189
+ *
190
+ * A discriminated union says so with a `const` on the discriminator, which
191
+ * is what a form posts as a string and can be matched before coercion.
192
+ * Otherwise the first branch whose type the value could be.
193
+ */
194
+ function branchFor(branches, value) {
195
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
196
+ const record = value;
197
+ const byDiscriminator = branches.find((branch) => Object.entries(branch.properties ?? {}).some(([key, prop]) => prop.const !== undefined && record[key] === String(prop.const)));
198
+ if (byDiscriminator)
199
+ return byDiscriminator;
200
+ }
201
+ return (branches.find((branch) => typeof value === "string"
202
+ ? hasType(branch, "string") || hasType(branch, "number") || hasType(branch, "integer") || hasType(branch, "boolean")
203
+ : Array.isArray(value)
204
+ ? hasType(branch, "array")
205
+ : hasType(branch, "object")) ?? null);
206
+ }
207
+ /**
208
+ * A form's values as the types its schema means.
209
+ *
210
+ * Everything a form posts is a string or a file, and a control that is off
211
+ * is not there at all - so a schema written for the shape it wants, `boolean`
212
+ * or `number`, refused a form that was perfectly filled in, and the guide
213
+ * asked for `z.enum(['on', '1']).optional().transform(...)` per checkbox. The
214
+ * schema knows what it means; this reads the form the way the schema does:
215
+ *
216
+ * - a `boolean` that is absent is `false` (an unchecked box posts nothing);
217
+ * `"on"`, `"1"`, `"true"` are true, `"0"`, `"off"`, `"false"` are false
218
+ * - a `number` or `integer` given as a numeric string is the number; an
219
+ * empty string for one that is not required is absent
220
+ * - an `array` given one value is that value in a list, and one given
221
+ * nothing - no checkbox ticked - is the empty list
222
+ * - an object nests, a list of objects nests per item, and a union takes the
223
+ * branch its discriminator names
224
+ *
225
+ * Nothing else is touched: a string stays the string it was, a file stays a
226
+ * file, and a value the schema does not describe is passed through as posted.
227
+ */
228
+ export function coerceToSchema(value, schema) {
229
+ if (!schema)
230
+ return value;
231
+ const branches = schema.anyOf ?? schema.oneOf;
232
+ if (branches) {
233
+ const branch = branchFor(branches, value);
234
+ return branch ? coerceToSchema(value, branch) : value;
235
+ }
236
+ if (hasType(schema, "boolean")) {
237
+ if (value === undefined)
238
+ return false;
239
+ if (typeof value === "string") {
240
+ const v = value.trim().toLowerCase();
241
+ if (v === "on" || v === "1" || v === "true")
242
+ return true;
243
+ if (v === "" || v === "0" || v === "off" || v === "false")
244
+ return false;
25
245
  }
26
- else if (Array.isArray(val)) {
27
- for (const item of val) {
28
- formData.append(`${key}[]`, String(item));
29
- }
246
+ return value;
247
+ }
248
+ if (hasType(schema, "number") || hasType(schema, "integer")) {
249
+ if (typeof value === "string") {
250
+ const v = value.trim();
251
+ if (v === "")
252
+ return undefined;
253
+ if (/^[-+]?(\d+\.?\d*|\.\d+)(e[-+]?\d+)?$/i.test(v))
254
+ return Number(v);
30
255
  }
31
- else {
32
- formData.append(key, String(val));
256
+ return value;
257
+ }
258
+ if (hasType(schema, "array")) {
259
+ // A list with nothing in it posts nothing at all: no checkbox ticked, a
260
+ // list the person emptied. That is an empty list, not a missing field.
261
+ if (value === undefined)
262
+ return [];
263
+ const list = Array.isArray(value) ? value : [value];
264
+ const items = Array.isArray(schema.items) ? null : schema.items;
265
+ return items ? list.map((item) => coerceToSchema(item, items)) : list;
266
+ }
267
+ if (hasType(schema, "object") || schema.properties) {
268
+ if (value === null || typeof value !== "object" || Array.isArray(value))
269
+ return value;
270
+ const record = { ...value };
271
+ const required = schema.required ?? [];
272
+ for (const [key, prop] of Object.entries(schema.properties ?? {})) {
273
+ // A control left blank posts "", and a field the schema does not
274
+ // require is then absent, whatever its type: z.email().optional()
275
+ // would refuse "" and the person typed nothing; a union with a
276
+ // boolean branch would read "" as false and write a value nobody
277
+ // chose. Decided before coercion, so no branch gets to. A required
278
+ // field keeps its "" so the schema can say it is required.
279
+ const coerced = record[key] === "" && !required.includes(key) ? undefined : coerceToSchema(record[key], prop);
280
+ if (coerced === undefined)
281
+ delete record[key];
282
+ else
283
+ record[key] = coerced;
33
284
  }
285
+ return record;
34
286
  }
35
- return formData;
287
+ return value;
288
+ }
289
+ /**
290
+ * A FormData as the object a schema expects, typed the way the schema means.
291
+ *
292
+ * What `<Form>` validates and what an action decodes: the same call, so a
293
+ * form that passes in the browser passes on the server. Without a schema, or
294
+ * with one that cannot describe itself, the values are the strings the form
295
+ * posted.
296
+ */
297
+ export function decodeFormData(formData, schema) {
298
+ const values = formDataToObject(formData);
299
+ return coerceToSchema(values, jsonSchemaOf(schema));
36
300
  }
37
301
  //# sourceMappingURL=formEncoding.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"formEncoding.js","sourceRoot":"","sources":["../../src/js/formEncoding.ts"],"names":[],"mappings":"AAAA,wCAAwC;AACxC,EAAE;AACF,gFAAgF;AAChF,6EAA6E;AAC7E,6EAA6E;AAC7E,qCAAqC;AAErC;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,IAA6B;IACzD,MAAM,QAAQ,GAAG,IAAI,QAAQ,EAAE,CAAC;IAEhC,KAAK,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9C,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YACtC,SAAS;QACX,CAAC;QACD,IAAI,GAAG,YAAY,IAAI,EAAE,CAAC;YACxB,QAAQ,CAAC,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QAC5B,CAAC;aAAM,IAAI,OAAO,GAAG,KAAK,SAAS,EAAE,CAAC;YACpC,QAAQ,CAAC,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QACxC,CAAC;aAAM,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;YAC9B,KAAK,MAAM,IAAI,IAAI,GAAG,EAAE,CAAC;gBACvB,QAAQ,CAAC,MAAM,CAAC,GAAG,GAAG,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;YAC5C,CAAC;QACH,CAAC;aAAM,CAAC;YACN,QAAQ,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;QACpC,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC","sourcesContent":["// Turning an object back into FormData.\n//\n// The one place the encoding decisions live, so a value sent through `<Form>`'s\n// `transform` is encoded exactly as one sent any other way. It used to exist\n// twice — here and inline in Form.tsx — which is two places for a boolean to\n// start meaning something different.\n\n/**\n * Serialize form state into FormData for a server action.\n *\n * The contract between a form and the action it calls — booleans become \"1\"/\"0\" so PHP sees something truthy, arrays repeat under\n * `key[]`, Files pass through untouched for native uploads, and null/undefined\n * are dropped rather than sent as the string \"null\".\n */\nexport function buildFormData(data: Record<string, unknown>): FormData {\n const formData = new FormData();\n\n for (const [key, val] of Object.entries(data)) {\n if (val === null || val === undefined) {\n continue;\n }\n if (val instanceof File) {\n formData.append(key, val);\n } else if (typeof val === \"boolean\") {\n formData.append(key, val ? \"1\" : \"0\");\n } else if (Array.isArray(val)) {\n for (const item of val) {\n formData.append(`${key}[]`, String(item));\n }\n } else {\n formData.append(key, String(val));\n }\n }\n\n return formData;\n}\n"]}
1
+ {"version":3,"file":"formEncoding.js","sourceRoot":"","sources":["../../src/js/formEncoding.ts"],"names":[],"mappings":"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","sourcesContent":["// Turning an object into FormData, and FormData back into an object.\n//\n// The one place the encoding decisions live, so a value sent through `<Form>`'s\n// `transform` is encoded exactly as one sent any other way, and - the part\n// that mattered - the object an ACTION decodes is the same object the FORM\n// validated. The decoder lived in Form.tsx and understood nested names;\n// the action had a flat one of its own. A form with `items[0].name` passed\n// validation in the browser and failed the same schema on the server, and\n// nothing said why. Both sides call this now.\n\n/**\n * Serialize form state into FormData for a server action.\n *\n * The contract between a form and the action it calls: booleans become \"1\"/\"0\"\n * so PHP sees something truthy, Files pass through untouched for native\n * uploads, null/undefined are dropped rather than sent as the string \"null\",\n * and a nested value nests - `{ items: [{ name }] }` is `items[0][name]`,\n * `{ address: { city } }` is `address[city]`, a list of scalars repeats under\n * `key[]`. Every one of those decodes back to what was given.\n */\nexport function buildFormData(data: Record<string, unknown>): FormData {\n const formData = new FormData();\n\n for (const [key, val] of Object.entries(data)) appendValue(formData, key, val);\n\n return formData;\n}\n\nfunction appendValue(formData: FormData, key: string, val: unknown): void {\n if (val === null || val === undefined) return;\n\n if (val instanceof File || val instanceof Blob) {\n formData.append(key, val);\n } else if (typeof val === \"boolean\") {\n formData.append(key, val ? \"1\" : \"0\");\n } else if (Array.isArray(val)) {\n // Scalars repeat under key[]; objects and nested lists take an index, so\n // each element's fields stay together on the way back.\n val.forEach((item, i) => {\n if (item !== null && typeof item === \"object\" && !(item instanceof File) && !(item instanceof Blob)) {\n appendValue(formData, `${key}[${i}]`, item);\n } else {\n appendValue(formData, `${key}[]`, item);\n }\n });\n } else if (typeof val === \"object\" && !(val instanceof Date)) {\n for (const [prop, inner] of Object.entries(val as Record<string, unknown>)) {\n appendValue(formData, `${key}[${prop}]`, inner);\n }\n } else if (val instanceof Date) {\n formData.append(key, val.toISOString());\n } else {\n formData.append(key, String(val));\n }\n}\n\n/**\n * The pieces of a field name: `items[0].name` is items, 0, name.\n *\n * Both spellings, because both are in use and a form should not care which one\n * a person reached for: `items[0].name` and `items[0][name]` are the same\n * field. A trailing `[]` is a piece of its own - see below.\n */\nfunction pathOf(name: string): string[] {\n return name\n .replace(/\\[(\\w*)\\]/g, \".$1\")\n .split(\".\")\n .filter((piece, index, all) => piece !== \"\" || index === all.length - 1);\n}\n\n/** Whether a piece names an array index rather than a property. */\nconst isIndex = (piece: string): boolean => /^\\d+$/.test(piece);\n\n/**\n * Put one value at one path, making the containers it passes through.\n *\n * Whether a container is an array or an object is decided by the NEXT piece, so\n * `items[0].name` makes an array holding an object without being told which is\n * which.\n */\nfunction place(\n root: Record<string, unknown>,\n path: string[],\n value: unknown,\n): void {\n let node: Record<string, unknown> | unknown[] = root;\n\n for (let i = 0; i < path.length - 1; i++) {\n const key = path[i];\n const container = node as Record<string, unknown>;\n\n if (container[key] === undefined || typeof container[key] !== \"object\") {\n // An index makes an array, and so does the empty piece a trailing `[]`\n // leaves - `tags[]` has to reach an array to be pushed into, and building\n // an object there is how this first went wrong.\n const next = path[i + 1];\n\n container[key] = isIndex(next) || next === \"\" ? [] : {};\n }\n\n node = container[key] as Record<string, unknown> | unknown[];\n }\n\n const last = path[path.length - 1];\n\n // The empty piece a trailing `[]` leaves: push rather than assign, so\n // `tags[]` twice is two entries rather than one overwriting the other.\n if (last === \"\") (node as unknown[]).push(value);\n else (node as Record<string, unknown>)[last] = value;\n}\n\n/**\n * A FormData as the object a schema expects.\n *\n * Four things beyond copying entries across, and each of them was a bug or a\n * gap someone would meet on their first non-trivial form:\n *\n * A repeated name is an array. Three checkboxes sharing a name, a multiple\n * select, a list of tags - this used to keep the LAST one and drop the rest\n * silently, so a schema validated an object the person had not submitted.\n *\n * A name ending in `[]` is always an array, even with one value selected.\n * Otherwise a list of checkboxes is a string when one is ticked and an array\n * when two are, and no schema can describe both. It is also what `useForm`\n * writes when it serialises an array, so the two round-trip.\n *\n * Nested names nest. `address.city` and `items[0].name` build the object they\n * describe, which is the shape the schema was written against - and the shape\n * whose validation errors come back keyed the same way, because Standard\n * Schema issue paths are joined with dots too.\n *\n * Files are kept. They were dropped for being non-strings, which meant a schema\n * checking an upload was handed undefined and refused a file that was there.\n */\nexport function formDataToObject<T extends Record<string, unknown> = Record<string, unknown>>(\n formData: FormData,\n): T {\n const obj: Record<string, unknown> = {};\n\n for (const name of new Set(formData.keys())) {\n const all = formData.getAll(name);\n const path = pathOf(name);\n\n // A plain name used more than once is the array case, and it has no\n // brackets to say so - `tags` twice is `['a', 'b']`.\n if (path.length === 1 && path[0] !== \"\" && all.length > 1) {\n obj[path[0]] = all;\n continue;\n }\n\n for (const value of all) place(obj, path, value);\n }\n\n return obj as T;\n}\n\n// ── Coercing what a form posts to what a schema means ─────────────────────\n\ntype JsonSchema = {\n type?: string | string[];\n properties?: Record<string, JsonSchema>;\n items?: JsonSchema | JsonSchema[];\n required?: string[];\n anyOf?: JsonSchema[];\n oneOf?: JsonSchema[];\n const?: unknown;\n enum?: unknown[];\n};\n\ntype WithJsonSchema = {\n \"~standard\"?: {\n jsonSchema?: {\n input?: (options: { target: string; libraryOptions?: Record<string, unknown> }) => unknown;\n };\n };\n};\n\n/**\n * What a schema is asked for. A leaf JSON Schema cannot say - a Date, a\n * Map, a custom check - is `{}` rather than a refusal of the whole schema:\n * Zod reads `unrepresentable` from here, and a form whose schema has one\n * `z.date()` beside twenty fields still reads the twenty the way they mean.\n * A library that does not know the option ignores it.\n */\nconst JSON_SCHEMA_OPTIONS = { target: \"draft-2020-12\", libraryOptions: { unrepresentable: \"any\" } };\n\nconst jsonSchemas = new WeakMap<object, JsonSchema | null>();\n\n/**\n * The JSON Schema a Standard Schema describes itself with, if it can.\n *\n * Zod 4 and ArkType can; Valibot needs its separate converter and answers\n * nothing here, in which case the form's values are handed to the schema as\n * posted and the schema has to read strings.\n */\nfunction jsonSchemaOf(schema: unknown): JsonSchema | null {\n if (schema === null || typeof schema !== \"object\") return null;\n\n const cached = jsonSchemas.get(schema);\n\n if (cached !== undefined) return cached;\n\n let json: JsonSchema | null = null;\n\n const produce = (schema as WithJsonSchema)[\"~standard\"]?.jsonSchema?.input;\n\n try {\n if (typeof produce === \"function\") {\n json = produce(JSON_SCHEMA_OPTIONS) as JsonSchema;\n }\n } catch (error) {\n json = null;\n\n // A schema that has the method and still refused: every field of this\n // form now arrives as the string it was posted, and a z.boolean() in it\n // fails on \"on\" - which looks like the form's fault. Said once, in\n // development, with the library's own reason.\n if (typeof produce === \"function\" && import.meta.env?.DEV) {\n console.warn(\n \"[rsc-kit] this form's schema cannot describe itself as JSON Schema, so its values are \" +\n \"validated as the strings the form posted: \" +\n (error instanceof Error ? error.message : String(error)),\n );\n }\n }\n\n jsonSchemas.set(schema, json);\n\n return json;\n}\n\nconst hasType = (schema: JsonSchema, type: string): boolean =>\n Array.isArray(schema.type) ? schema.type.includes(type) : schema.type === type;\n\n/**\n * Which branch of a union the value is for.\n *\n * A discriminated union says so with a `const` on the discriminator, which\n * is what a form posts as a string and can be matched before coercion.\n * Otherwise the first branch whose type the value could be.\n */\nfunction branchFor(branches: JsonSchema[], value: unknown): JsonSchema | null {\n if (value !== null && typeof value === \"object\" && !Array.isArray(value)) {\n const record = value as Record<string, unknown>;\n const byDiscriminator = branches.find((branch) =>\n Object.entries(branch.properties ?? {}).some(\n ([key, prop]) => prop.const !== undefined && record[key] === String(prop.const),\n ),\n );\n\n if (byDiscriminator) return byDiscriminator;\n }\n\n return (\n branches.find((branch) =>\n typeof value === \"string\"\n ? hasType(branch, \"string\") || hasType(branch, \"number\") || hasType(branch, \"integer\") || hasType(branch, \"boolean\")\n : Array.isArray(value)\n ? hasType(branch, \"array\")\n : hasType(branch, \"object\"),\n ) ?? null\n );\n}\n\n/**\n * A form's values as the types its schema means.\n *\n * Everything a form posts is a string or a file, and a control that is off\n * is not there at all - so a schema written for the shape it wants, `boolean`\n * or `number`, refused a form that was perfectly filled in, and the guide\n * asked for `z.enum(['on', '1']).optional().transform(...)` per checkbox. The\n * schema knows what it means; this reads the form the way the schema does:\n *\n * - a `boolean` that is absent is `false` (an unchecked box posts nothing);\n * `\"on\"`, `\"1\"`, `\"true\"` are true, `\"0\"`, `\"off\"`, `\"false\"` are false\n * - a `number` or `integer` given as a numeric string is the number; an\n * empty string for one that is not required is absent\n * - an `array` given one value is that value in a list, and one given\n * nothing - no checkbox ticked - is the empty list\n * - an object nests, a list of objects nests per item, and a union takes the\n * branch its discriminator names\n *\n * Nothing else is touched: a string stays the string it was, a file stays a\n * file, and a value the schema does not describe is passed through as posted.\n */\nexport function coerceToSchema(value: unknown, schema: JsonSchema | null | undefined): unknown {\n if (!schema) return value;\n\n const branches = schema.anyOf ?? schema.oneOf;\n\n if (branches) {\n const branch = branchFor(branches, value);\n\n return branch ? coerceToSchema(value, branch) : value;\n }\n\n if (hasType(schema, \"boolean\")) {\n if (value === undefined) return false;\n if (typeof value === \"string\") {\n const v = value.trim().toLowerCase();\n\n if (v === \"on\" || v === \"1\" || v === \"true\") return true;\n if (v === \"\" || v === \"0\" || v === \"off\" || v === \"false\") return false;\n }\n\n return value;\n }\n\n if (hasType(schema, \"number\") || hasType(schema, \"integer\")) {\n if (typeof value === \"string\") {\n const v = value.trim();\n\n if (v === \"\") return undefined;\n if (/^[-+]?(\\d+\\.?\\d*|\\.\\d+)(e[-+]?\\d+)?$/i.test(v)) return Number(v);\n }\n\n return value;\n }\n\n if (hasType(schema, \"array\")) {\n // A list with nothing in it posts nothing at all: no checkbox ticked, a\n // list the person emptied. That is an empty list, not a missing field.\n if (value === undefined) return [];\n\n const list = Array.isArray(value) ? value : [value];\n const items = Array.isArray(schema.items) ? null : schema.items;\n\n return items ? list.map((item) => coerceToSchema(item, items)) : list;\n }\n\n if (hasType(schema, \"object\") || schema.properties) {\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) return value;\n\n const record = { ...(value as Record<string, unknown>) };\n const required = schema.required ?? [];\n\n for (const [key, prop] of Object.entries(schema.properties ?? {})) {\n // A control left blank posts \"\", and a field the schema does not\n // require is then absent, whatever its type: z.email().optional()\n // would refuse \"\" and the person typed nothing; a union with a\n // boolean branch would read \"\" as false and write a value nobody\n // chose. Decided before coercion, so no branch gets to. A required\n // field keeps its \"\" so the schema can say it is required.\n const coerced =\n record[key] === \"\" && !required.includes(key) ? undefined : coerceToSchema(record[key], prop);\n\n if (coerced === undefined) delete record[key];\n else record[key] = coerced;\n }\n\n return record;\n }\n\n return value;\n}\n\n/**\n * A FormData as the object a schema expects, typed the way the schema means.\n *\n * What `<Form>` validates and what an action decodes: the same call, so a\n * form that passes in the browser passes on the server. Without a schema, or\n * with one that cannot describe itself, the values are the strings the form\n * posted.\n */\nexport function decodeFormData<T extends Record<string, unknown> = Record<string, unknown>>(\n formData: FormData,\n schema?: unknown,\n): T {\n const values = formDataToObject(formData);\n\n return coerceToSchema(values, jsonSchemaOf(schema)) as T;\n}\n"]}
@@ -46,6 +46,9 @@ export declare function setNavigateHandler(fn: (tree: ReactNode, key: string, se
46
46
  export declare function setRestoreHandler(fn: (key: string, maxAge?: number) => boolean): void;
47
47
  export declare function setDeserializer(fn: Deserializer): void;
48
48
  export declare function setCallServer(fn: CallServerFn): void;
49
+ export declare function setApiRoutes(patterns: string[]): void;
50
+ /** Whether a url is one a route.ts answers. */
51
+ export declare function isApiRoute(url: string): boolean;
49
52
  export declare function setInterceptManifest(entries: InterceptEntry[]): void;
50
53
  export declare function renderTree(tree: ReactNode): void;
51
54
  /**
@@ -201,6 +201,33 @@ export function setDeserializer(fn) {
201
201
  export function setCallServer(fn) {
202
202
  callServerFn = fn;
203
203
  }
204
+ /**
205
+ * The urls a route.ts answers, so a link to one is treated as the anchor it
206
+ * is: no prefetch - a hover must not run a route - and a full navigation
207
+ * rather than a payload fetch, since the answer is a Response, not a page.
208
+ * Baked into the generated browser entry from the route tree.
209
+ */
210
+ let apiRoutePatterns = [];
211
+ export function setApiRoutes(patterns) {
212
+ apiRoutePatterns = patterns.map((pattern) => new RegExp("^" +
213
+ pattern
214
+ .split("/")
215
+ .map((part) => part.startsWith("[...") ? ".+" : part.startsWith("[") ? "[^/]+" : part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
216
+ .join("/") +
217
+ "/?$"));
218
+ }
219
+ /** Whether a url is one a route.ts answers. */
220
+ export function isApiRoute(url) {
221
+ if (apiRoutePatterns.length === 0)
222
+ return false;
223
+ try {
224
+ const { pathname } = new URL(url, window.location.origin);
225
+ return apiRoutePatterns.some((pattern) => pattern.test(pathname));
226
+ }
227
+ catch {
228
+ return false;
229
+ }
230
+ }
204
231
  export function setInterceptManifest(entries) {
205
232
  interceptManifest = entries;
206
233
  }
@@ -406,6 +433,12 @@ export async function navigate(url, opts) {
406
433
  window.location.href = url;
407
434
  return;
408
435
  }
436
+ // A route.ts answers with a Response, not a page: a download, a redirect
437
+ // that decides where someone belongs, a sign-out. The browser goes there.
438
+ if (isApiRoute(url)) {
439
+ window.location.href = url;
440
+ return;
441
+ }
409
442
  // Hash-only URLs — let the browser handle scrolling natively
410
443
  if (url.startsWith("#")) {
411
444
  window.location.hash = url;
@@ -413,11 +446,13 @@ export async function navigate(url, opts) {
413
446
  }
414
447
  // Abort any in-flight navigation
415
448
  activeController?.abort();
416
- // If the initial HTML stream is still loading (Suspense completions streaming),
417
- // stop it so the single-threaded PHP server can handle the new request.
418
- if (document.readyState === "loading") {
419
- window.stop();
420
- }
449
+ // Not window.stop(). It used to be called here while the document was
450
+ // still loading, to free a single-threaded server for the new request -
451
+ // and it cancels every load the page has in flight: the client chunks a
452
+ // just-decoded tree still needs, the stylesheet, the boundaries still
453
+ // streaming. A navigation that left the page before the runtime had
454
+ // finished arriving rendered a document with nothing in it. The server
455
+ // has workers; the page keeps loading.
421
456
  const controller = new AbortController();
422
457
  activeController = controller;
423
458
  // Check if this URL matches an intercept pattern.
@@ -565,7 +600,19 @@ export async function navigate(url, opts) {
565
600
  }
566
601
  treePromise = deserializeResponse(response);
567
602
  }
568
- const tree = await treePromise;
603
+ let tree;
604
+ try {
605
+ tree = await treePromise;
606
+ }
607
+ catch (error) {
608
+ // A navigation another one overtook: its request was aborted, and
609
+ // the decoder reports that as a failure of the payload. It is not one
610
+ // - nothing of it was going to be shown - so it ends here, quietly,
611
+ // rather than as a rejection nobody is waiting on.
612
+ if (controller.signal.aborted)
613
+ return;
614
+ throw error;
615
+ }
569
616
  if (reused) {
570
617
  segmentDepth = reused.segmentDepth;
571
618
  nextLayouts = reused.layouts;
@@ -771,6 +818,9 @@ function restoreScroll(positions) {
771
818
  apply(1);
772
819
  }
773
820
  export function prefetch(url, cacheForMs) {
821
+ // Never a route.ts: fetching one runs it, and a hover is not a click.
822
+ if (isApiRoute(url))
823
+ return;
774
824
  if (isExternalUrl(url))
775
825
  return;
776
826
  const ttl = cacheForMs ?? DEFAULT_PREFETCH_TTL;