@rsc-kit/core 0.23.0 → 0.24.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.
- package/dist/host.d.ts +2 -0
- package/dist/host.js +31 -9
- package/dist/host.js.map +1 -1
- package/dist/hostCalls.js +8 -0
- package/dist/hostCalls.js.map +1 -1
- package/dist/js/Form.d.ts +13 -0
- package/dist/js/Form.js +5 -0
- package/dist/js/Form.js.map +1 -1
- package/dist/js/navigate.js +5 -6
- package/dist/js/navigate.js.map +1 -1
- package/dist/prerender.js +15 -1
- package/dist/prerender.js.map +1 -1
- package/dist/request.js +2 -0
- package/dist/request.js.map +1 -1
- package/dist/routing.d.ts +18 -0
- package/dist/routing.js +35 -0
- package/dist/routing.js.map +1 -1
- package/dist/testing.d.ts +7 -0
- package/dist/testing.js +4 -0
- package/dist/testing.js.map +1 -1
- package/dist/vite.js +280 -48
- package/dist/vite.js.map +1 -1
- package/package.json +1 -1
package/dist/vite.js
CHANGED
|
@@ -201,6 +201,9 @@ let foundAssets = {
|
|
|
201
201
|
let hostActions;
|
|
202
202
|
/** What rpc() may be called with, from rsc-host-functions.json; null when the backend lists none. */
|
|
203
203
|
let hostFunctions = null;
|
|
204
|
+
/** The backend's typed signatures, by function name, and the named types they refer to. */
|
|
205
|
+
let hostTypes = {};
|
|
206
|
+
let hostDefs = {};
|
|
204
207
|
/**
|
|
205
208
|
* The manifest commands run in this process, by project and command.
|
|
206
209
|
*
|
|
@@ -241,30 +244,8 @@ function envRouteConfig() {
|
|
|
241
244
|
}
|
|
242
245
|
/** The file a backend writes its action names into. */
|
|
243
246
|
const HOST_FILE = "rsc-host.json";
|
|
244
|
-
/**
|
|
245
|
-
|
|
246
|
-
*
|
|
247
|
-
* {
|
|
248
|
-
* "actions": { "ordersCreate": "Orders.create" },
|
|
249
|
-
* "functions": ["Orders.create", "Orders.recent"]
|
|
250
|
-
* }
|
|
251
|
-
*
|
|
252
|
-
* `actions` are the server actions the build writes "use server" stubs for,
|
|
253
|
-
* by JavaScript name. `functions` are the names rpc() may be called with,
|
|
254
|
-
* which the build turns into a type so a misspelt one fails the typecheck.
|
|
255
|
-
*
|
|
256
|
-
* A file rather than an environment variable, because the backend no longer
|
|
257
|
-
* drives the build - `vite build` does. Discovery has to stay where the code
|
|
258
|
-
* is (reflection through Composer's autoloader finds what a class inherits; a
|
|
259
|
-
* regex would silently miss every inherited action), but the handoff is just
|
|
260
|
-
* names, and a JSON file is something any language can write:
|
|
261
|
-
*
|
|
262
|
-
* php artisan rsc:host-manifest
|
|
263
|
-
* go run ./backend -manifest rsc-host.json
|
|
264
|
-
*
|
|
265
|
-
* Absent is not an error: a backend-less app has no file. `hostManifest`
|
|
266
|
-
* runs the command as dev or a build starts.
|
|
267
|
-
*/
|
|
247
|
+
/** The "use server" module of the backend's action stubs, written beside the app. */
|
|
248
|
+
const HOST_ACTIONS_MODULE = "server-actions.generated.ts";
|
|
268
249
|
function fileHostManifest(root) {
|
|
269
250
|
const path = join(root, HOST_FILE);
|
|
270
251
|
if (!existsSync(path)) {
|
|
@@ -272,7 +253,7 @@ function fileHostManifest(root) {
|
|
|
272
253
|
throw new Error("[rsc-kit] rsc-host-actions.json is the old manifest. The backend now writes " +
|
|
273
254
|
HOST_FILE + " - { actions, functions } - so update the adapter and run its manifest command.");
|
|
274
255
|
}
|
|
275
|
-
return { actions: {}, functions: null };
|
|
256
|
+
return { actions: {}, functions: null, types: {}, defs: {} };
|
|
276
257
|
}
|
|
277
258
|
let parsed;
|
|
278
259
|
try {
|
|
@@ -291,7 +272,10 @@ function fileHostManifest(root) {
|
|
|
291
272
|
if (functions !== undefined && (!Array.isArray(functions) || functions.some((n) => typeof n !== "string"))) {
|
|
292
273
|
throw new Error(`[rsc-kit] ${HOST_FILE}: "functions" must be an array of names.`);
|
|
293
274
|
}
|
|
275
|
+
const isMap = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
294
276
|
return {
|
|
277
|
+
types: isMap(parsed?.types) ? parsed.types : {},
|
|
278
|
+
defs: isMap(parsed?.defs) ? parsed.defs : {},
|
|
295
279
|
actions: actions,
|
|
296
280
|
// The reserved guard call is the engine's to make, not the app's.
|
|
297
281
|
functions: functions ? functions.filter((n) => !n.startsWith("__rsc.")).sort() : null,
|
|
@@ -433,6 +417,8 @@ function loadHostManifest(options) {
|
|
|
433
417
|
const manifest = fileHostManifest(projectRoot);
|
|
434
418
|
hostActions = options.hostActions ?? manifest.actions;
|
|
435
419
|
hostFunctions = manifest.functions;
|
|
420
|
+
hostTypes = manifest.types;
|
|
421
|
+
hostDefs = manifest.defs;
|
|
436
422
|
}
|
|
437
423
|
/** Run the backend's manifest command, once per process; see the option. */
|
|
438
424
|
function runHostManifest(options, isBuild) {
|
|
@@ -790,7 +776,7 @@ function writeHostBindings(manifest) {
|
|
|
790
776
|
// which typechecks a server and fails a prerender script.
|
|
791
777
|
writeGenerated(join(typesDir, "rsc-engine.d.ts"), ENGINE_TYPES);
|
|
792
778
|
warnIfTypesUnreachable();
|
|
793
|
-
const target = join(sourceDir,
|
|
779
|
+
const target = join(sourceDir, HOST_ACTIONS_MODULE);
|
|
794
780
|
// A host with no functions of its own leaves no file behind: kept, its
|
|
795
781
|
// stubs would go on naming targets the host has stopped answering for.
|
|
796
782
|
if (Object.keys(hostActions).length === 0) {
|
|
@@ -1733,7 +1719,11 @@ async function prerenderAfterBundles(bundle, staticDir, assetsDir, knownActions
|
|
|
1733
1719
|
// an action nothing else in the app states — whether anything checks who
|
|
1734
1720
|
// calls it.
|
|
1735
1721
|
const audited = await auditActions(engine, knownActions);
|
|
1736
|
-
|
|
1722
|
+
// Not the host's stubs. Each forwards to a backend function, and the check
|
|
1723
|
+
// on who calls it runs there - Laravel's guards, Go's middleware - where
|
|
1724
|
+
// this build cannot see it. Counted, they made every backend action look
|
|
1725
|
+
// unguarded: "11 actions run no middleware" for an API that checks them all.
|
|
1726
|
+
const bare = audited.filter((a) => !a.client && basename(a.file) !== HOST_ACTIONS_MODULE);
|
|
1737
1727
|
if (bare.length > 0) {
|
|
1738
1728
|
const byFile = new Map();
|
|
1739
1729
|
for (const a of bare)
|
|
@@ -2020,6 +2010,105 @@ function renderRouteTypes(manifest) {
|
|
|
2020
2010
|
"",
|
|
2021
2011
|
].join("\n");
|
|
2022
2012
|
}
|
|
2013
|
+
/** A property name as TypeScript accepts it. */
|
|
2014
|
+
const tsKey = (name) => (/^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name));
|
|
2015
|
+
/**
|
|
2016
|
+
* A backend's JSON Schema as a TypeScript type. Named types are written once,
|
|
2017
|
+
* in the RscHost namespace, and referred to; anything the schema does not say
|
|
2018
|
+
* is unknown rather than any, so a gap is visible where it is used.
|
|
2019
|
+
*/
|
|
2020
|
+
function tsOf(schema) {
|
|
2021
|
+
if (!schema || Object.keys(schema).length === 0)
|
|
2022
|
+
return "unknown";
|
|
2023
|
+
const ref = schema.$ref;
|
|
2024
|
+
if (typeof ref === "string" && ref.startsWith("#/defs/"))
|
|
2025
|
+
return "RscHost." + ref.slice(7);
|
|
2026
|
+
if (Array.isArray(schema.anyOf)) {
|
|
2027
|
+
return [...new Set(schema.anyOf.map(tsOf))].join(" | ");
|
|
2028
|
+
}
|
|
2029
|
+
// A backed enum: its values, each a literal.
|
|
2030
|
+
if (Array.isArray(schema.enum)) {
|
|
2031
|
+
return schema.enum.map((v) => JSON.stringify(v)).join(" | ") || "never";
|
|
2032
|
+
}
|
|
2033
|
+
switch (schema.type) {
|
|
2034
|
+
case "string":
|
|
2035
|
+
return "string";
|
|
2036
|
+
case "integer":
|
|
2037
|
+
case "number":
|
|
2038
|
+
return "number";
|
|
2039
|
+
case "boolean":
|
|
2040
|
+
return "boolean";
|
|
2041
|
+
case "null":
|
|
2042
|
+
return "null";
|
|
2043
|
+
case "array":
|
|
2044
|
+
return "Array<" + tsOf(schema.items) + ">";
|
|
2045
|
+
case "object": {
|
|
2046
|
+
const properties = schema.properties;
|
|
2047
|
+
if (properties) {
|
|
2048
|
+
const required = new Set(schema.required ?? []);
|
|
2049
|
+
const fields = Object.entries(properties).map(([name, s]) => tsKey(name) + (required.has(name) ? "" : "?") + ": " + tsOf(s));
|
|
2050
|
+
return "{ " + fields.join("; ") + (fields.length ? " }" : "}");
|
|
2051
|
+
}
|
|
2052
|
+
return "Record<string, " + tsOf(schema.additionalProperties) + ">";
|
|
2053
|
+
}
|
|
2054
|
+
default:
|
|
2055
|
+
return "unknown";
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
/** A typed function's parameter list: positional, trailing pointers optional, a variadic rest. */
|
|
2059
|
+
function tsParams(sig) {
|
|
2060
|
+
const optionalFrom = sig.params.length - (sig.optional ?? 0);
|
|
2061
|
+
const params = sig.params.map((p, i) => "arg" + (i + 1) + (i >= optionalFrom ? "?" : "") + ": " + tsOf(p));
|
|
2062
|
+
if (sig.rest)
|
|
2063
|
+
params.push("...rest: Array<" + tsOf(sig.rest) + ">");
|
|
2064
|
+
return params.join(", ");
|
|
2065
|
+
}
|
|
2066
|
+
/** The first parameter's schema with its reference followed, for decoding a form into it. */
|
|
2067
|
+
function firstParamSchema(sig) {
|
|
2068
|
+
const first = sig?.params[0];
|
|
2069
|
+
const ref = first?.$ref;
|
|
2070
|
+
if (typeof ref === "string" && ref.startsWith("#/defs/"))
|
|
2071
|
+
return hostDefs[ref.slice(7)] ?? null;
|
|
2072
|
+
return first ?? null;
|
|
2073
|
+
}
|
|
2074
|
+
/**
|
|
2075
|
+
* A form posted to a backend action. React hands the action a FormData; on
|
|
2076
|
+
* the wire to the backend it is JSON, and JSON.stringify of a FormData is {} -
|
|
2077
|
+
* so a form posting straight to a backend action used to send nothing. Its
|
|
2078
|
+
* fields become one object, typed by the first parameter where the backend
|
|
2079
|
+
* described it: numbers and booleans coerced, a repeated name a list.
|
|
2080
|
+
*/
|
|
2081
|
+
const FORM_ARGS = `function formArgs(args: unknown[], schema: Record<string, any> | null): unknown[] {
|
|
2082
|
+
if (args.length !== 1 || !(args[0] instanceof FormData)) return args
|
|
2083
|
+
|
|
2084
|
+
const form = args[0] as FormData
|
|
2085
|
+
const props: Record<string, any> = schema?.properties ?? {}
|
|
2086
|
+
const coerce = (type: unknown, v: string): unknown =>
|
|
2087
|
+
type === 'integer' || type === 'number' ? (v === '' ? null : Number(v))
|
|
2088
|
+
: type === 'boolean' ? v === 'on' || v === 'true' || v === '1'
|
|
2089
|
+
: v
|
|
2090
|
+
const out: Record<string, unknown> = {}
|
|
2091
|
+
|
|
2092
|
+
for (const key of new Set(form.keys())) {
|
|
2093
|
+
// React's own fields for a form action, not the form's.
|
|
2094
|
+
if (key.startsWith('$ACTION_')) continue
|
|
2095
|
+
|
|
2096
|
+
const values = form.getAll(key).filter((v): v is string => typeof v === 'string')
|
|
2097
|
+
const prop = props[key]
|
|
2098
|
+
|
|
2099
|
+
out[key] = prop?.type === 'array' || (!prop && values.length > 1)
|
|
2100
|
+
? values.map((v) => coerce(prop?.items?.type, v))
|
|
2101
|
+
: coerce(prop?.type, values[0] ?? '')
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
// An unchecked checkbox sends nothing: false, not missing.
|
|
2105
|
+
for (const [key, prop] of Object.entries(props)) {
|
|
2106
|
+
if (!(key in out) && prop?.type === 'boolean') out[key] = false
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
return [out]
|
|
2110
|
+
}
|
|
2111
|
+
`;
|
|
2023
2112
|
/** The "use server" module exposing each host function as a plain async call. */
|
|
2024
2113
|
function renderHostActions() {
|
|
2025
2114
|
const lines = [
|
|
@@ -2027,13 +2116,24 @@ function renderHostActions() {
|
|
|
2027
2116
|
"// @generated — do not edit. Written by the RSC build from the host action map.",
|
|
2028
2117
|
"",
|
|
2029
2118
|
];
|
|
2119
|
+
lines.push(FORM_ARGS);
|
|
2030
2120
|
for (const [name, target] of Object.entries(hostActions)) {
|
|
2121
|
+
const sig = hostTypes[target];
|
|
2122
|
+
// Typed where the backend described it: its own parameters, or the form
|
|
2123
|
+
// that posts to it, and what it returns. Untyped otherwise, as before.
|
|
2124
|
+
if (sig) {
|
|
2125
|
+
const result = "Promise<" + (sig.result ? tsOf(sig.result) : "void") + ">";
|
|
2126
|
+
lines.push("export async function " + name + "(" + tsParams(sig) + "): " + result + ";");
|
|
2127
|
+
lines.push("export async function " + name + "(form: FormData): " + result + ";");
|
|
2128
|
+
}
|
|
2031
2129
|
lines.push("export async function " + name + "(...args: unknown[]) {");
|
|
2032
2130
|
lines.push(" return await (globalThis as any)." +
|
|
2033
2131
|
hostGlobal +
|
|
2034
2132
|
"(" +
|
|
2035
2133
|
JSON.stringify(target) +
|
|
2036
|
-
", ...args
|
|
2134
|
+
", ...formArgs(args, " +
|
|
2135
|
+
JSON.stringify(firstParamSchema(sig)) +
|
|
2136
|
+
"));");
|
|
2037
2137
|
lines.push("}");
|
|
2038
2138
|
lines.push("");
|
|
2039
2139
|
}
|
|
@@ -2053,22 +2153,48 @@ function renderHostGlobalTypes() {
|
|
|
2053
2153
|
"// Deliberately not a module — no import/export — so the declaration is",
|
|
2054
2154
|
"// global to the project without every file having to reference it.",
|
|
2055
2155
|
"",
|
|
2056
|
-
|
|
2057
|
-
// rather than the render. Without a list, any string.
|
|
2058
|
-
...(hostFunctions?.length
|
|
2059
|
-
? [
|
|
2060
|
-
"// From " + HOST_FILE + ", which the backend writes.",
|
|
2061
|
-
"type RscHostFunction =",
|
|
2062
|
-
...hostFunctions.map((name, i) => " | " + JSON.stringify(name) + (i === hostFunctions.length - 1 ? ";" : "")),
|
|
2063
|
-
"",
|
|
2064
|
-
]
|
|
2065
|
-
: []),
|
|
2066
|
-
"declare function " +
|
|
2067
|
-
hostGlobal +
|
|
2068
|
-
"<T = unknown>(name: " + (hostFunctions?.length ? "RscHostFunction" : "string") + ", ...args: unknown[]): Promise<T>;",
|
|
2069
|
-
"",
|
|
2156
|
+
...renderHostSignatures(),
|
|
2070
2157
|
].join("\n");
|
|
2071
2158
|
}
|
|
2159
|
+
/**
|
|
2160
|
+
* The rpc() declaration, as precisely as the backend described itself.
|
|
2161
|
+
*
|
|
2162
|
+
* A function the backend typed gets its own overload: its parameters, and
|
|
2163
|
+
* its result as the default type, so rpc('Orders.recent', 5) is Order[]
|
|
2164
|
+
* without a type argument. One it named but did not type takes anything and
|
|
2165
|
+
* returns what the caller says. With no list at all, any name.
|
|
2166
|
+
*/
|
|
2167
|
+
function renderHostSignatures() {
|
|
2168
|
+
const out = [];
|
|
2169
|
+
const typed = Object.keys(hostTypes).filter((name) => !name.startsWith("__rsc.")).sort();
|
|
2170
|
+
if (Object.keys(hostDefs).length > 0) {
|
|
2171
|
+
out.push("// The backend's own types, from " + HOST_FILE + ".", "declare namespace RscHost {");
|
|
2172
|
+
for (const name of Object.keys(hostDefs).sort()) {
|
|
2173
|
+
const schema = hostDefs[name];
|
|
2174
|
+
out.push(schema.type === "object" && schema.properties
|
|
2175
|
+
? " interface " + name + " " + tsOf(schema)
|
|
2176
|
+
: " type " + name + " = " + tsOf(schema) + ";");
|
|
2177
|
+
}
|
|
2178
|
+
out.push("}", "");
|
|
2179
|
+
}
|
|
2180
|
+
for (const name of typed) {
|
|
2181
|
+
const sig = hostTypes[name];
|
|
2182
|
+
const params = tsParams(sig);
|
|
2183
|
+
out.push("declare function " + hostGlobal + "<T = " + (sig.result ? tsOf(sig.result) : "void") + ">(name: " +
|
|
2184
|
+
JSON.stringify(name) + (params ? ", " + params : "") + "): Promise<T>;");
|
|
2185
|
+
}
|
|
2186
|
+
const untyped = hostFunctions?.filter((name) => !hostTypes[name]) ?? null;
|
|
2187
|
+
if (untyped === null) {
|
|
2188
|
+
out.push("declare function " + hostGlobal + "<T = unknown>(name: string, ...args: unknown[]): Promise<T>;");
|
|
2189
|
+
}
|
|
2190
|
+
else if (untyped.length > 0) {
|
|
2191
|
+
// The names the backend listed, so a misspelt one fails the typecheck
|
|
2192
|
+
// rather than the render.
|
|
2193
|
+
out.push("// From " + HOST_FILE + ", which the backend writes.", "type RscHostFunction =", ...untyped.map((name, i) => " | " + JSON.stringify(name) + (i === untyped.length - 1 ? ";" : "")), "declare function " + hostGlobal + "<T = unknown>(name: RscHostFunction, ...args: unknown[]): Promise<T>;");
|
|
2194
|
+
}
|
|
2195
|
+
out.push("");
|
|
2196
|
+
return out;
|
|
2197
|
+
}
|
|
2072
2198
|
// ── Discovery ────────────────────────────────────────────────────────────────
|
|
2073
2199
|
const ROUTE_FILES = [
|
|
2074
2200
|
"page",
|
|
@@ -2391,10 +2517,25 @@ const FALLBACK_CONSTS = `const FALLBACK_ORIGIN = __ORIGIN__
|
|
|
2391
2517
|
const FALLBACK_MARKER = 'x-rsc-renderer-fallback'
|
|
2392
2518
|
const PROXIED_MARKER = 'x-rsc-proxied-by-backend'
|
|
2393
2519
|
`;
|
|
2520
|
+
/** With no backend configured: this app's not-found page, or a test's backend. */
|
|
2521
|
+
const NO_FALLBACK_BODY = ` const answer = await devHandler(request)
|
|
2522
|
+
|
|
2523
|
+
if (answer) return answer
|
|
2524
|
+
|
|
2525
|
+
if (backendForward && !pageSaidNotFound(request)) return await backendForward(request)
|
|
2526
|
+
|
|
2527
|
+
return await notFound(request)
|
|
2528
|
+
`;
|
|
2394
2529
|
const FALLBACK_BODY = ` const answer = await devHandler(request)
|
|
2395
2530
|
|
|
2396
2531
|
if (answer) return answer
|
|
2397
2532
|
|
|
2533
|
+
// A page of this app said there is nothing here - notFound() in it or in
|
|
2534
|
+
// its guard, or a backend's 404. That is this app's not-found.tsx, not a
|
|
2535
|
+
// url for the backend: forwarded, the backend answered with its own 404
|
|
2536
|
+
// and the app's never showed.
|
|
2537
|
+
if (pageSaidNotFound(request)) return await notFound(request)
|
|
2538
|
+
|
|
2398
2539
|
// Nothing here owns this url. In development the backend usually does — a
|
|
2399
2540
|
// Blade page, /login, a webhook, an uploaded file under /storage — so the
|
|
2400
2541
|
// request is handed on rather than refused, and this origin is the whole
|
|
@@ -2452,6 +2593,18 @@ const FALLBACK_BODY = ` const answer = await devHandler(request)
|
|
|
2452
2593
|
|
|
2453
2594
|
const hasBody = request.method !== 'GET' && request.method !== 'HEAD'
|
|
2454
2595
|
|
|
2596
|
+
// A test's backend: the same request, answered in-process.
|
|
2597
|
+
if (backendForward) {
|
|
2598
|
+
return await backendForward(
|
|
2599
|
+
new Request(target, {
|
|
2600
|
+
method: request.method,
|
|
2601
|
+
headers,
|
|
2602
|
+
body: hasBody ? request.body : undefined,
|
|
2603
|
+
...(hasBody ? { duplex: 'half' } : {}),
|
|
2604
|
+
} as RequestInit),
|
|
2605
|
+
)
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2455
2608
|
try {
|
|
2456
2609
|
return await fetch(target, {
|
|
2457
2610
|
method: request.method,
|
|
@@ -2684,7 +2837,7 @@ import { notFoundDigest, isNotFoundSignal } from ${JSON.stringify(join(packageDi
|
|
|
2684
2837
|
import { noteRequestRead, urlOf } from ${JSON.stringify(join(packageDir, "request"))}
|
|
2685
2838
|
import { redirectDigest } from ${JSON.stringify(join(packageDir, "redirectDigest"))}
|
|
2686
2839
|
import { cancelledByConsumer } from ${JSON.stringify(join(packageDir, "js/fallbackReport"))}
|
|
2687
|
-
import { createRscHandler, queryAndParams } from ${JSON.stringify(join(packageDir, "host"))}
|
|
2840
|
+
import { createRscHandler, pageSaidNotFound, queryAndParams } from ${JSON.stringify(join(packageDir, "host"))}
|
|
2688
2841
|
import { httpHostCalls } from ${JSON.stringify(join(packageDir, "hostCalls"))}
|
|
2689
2842
|
import { prerenderedBeside } from ${JSON.stringify(join(packageDir, "files"))}
|
|
2690
2843
|
import { renderToReadableStream, decodeReply, decodeAction, decodeFormState, loadServerAction } from '@vitejs/plugin-rsc/rsc'
|
|
@@ -2693,7 +2846,7 @@ import { isActionValidationError, isClientBuilt } from ${JSON.stringify(join(pac
|
|
|
2693
2846
|
import { forgetCached } from ${JSON.stringify(join(packageDir, "cache"))}
|
|
2694
2847
|
import { noteFallback as noteCaughtRead } from ${JSON.stringify(join(packageDir, "request"))}
|
|
2695
2848
|
import { isOutdatedOptimizedDep, outdatedDepResponse } from ${JSON.stringify(join(packageDir, "devReload"))}
|
|
2696
|
-
import { sharedDepth } from ${JSON.stringify(join(packageDir, "routing"))}
|
|
2849
|
+
import { paramsFor, segmentNames, sharedDepth } from ${JSON.stringify(join(packageDir, "routing"))}
|
|
2697
2850
|
import { Suspense, createElement, Fragment } from 'react'
|
|
2698
2851
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
2699
2852
|
${imports.join("\n")}
|
|
@@ -3133,6 +3286,19 @@ if (__instrumentation.register && !isolateRuntime) {
|
|
|
3133
3286
|
|
|
3134
3287
|
let currentHost: HostFn | null = null
|
|
3135
3288
|
|
|
3289
|
+
/**
|
|
3290
|
+
* What answers a url this app does not own, in place of the backend.
|
|
3291
|
+
*
|
|
3292
|
+
* For createTestApp({ backend }): a url the app forwards to its backend
|
|
3293
|
+
* reaches the test instead of a server that is not running, which answered
|
|
3294
|
+
* every one with a 502.
|
|
3295
|
+
*/
|
|
3296
|
+
let backendForward: ((request: Request) => Response | Promise<Response>) | null = null
|
|
3297
|
+
|
|
3298
|
+
export function installBackendForward(fn: ((request: Request) => Response | Promise<Response>) | null) {
|
|
3299
|
+
backendForward = fn
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3136
3302
|
export function installHostFn(fn: HostFn) {
|
|
3137
3303
|
currentHost = fn
|
|
3138
3304
|
return () => {
|
|
@@ -3306,6 +3472,48 @@ function errorChain(component: string): string[] {
|
|
|
3306
3472
|
return errorChains[component] ?? []
|
|
3307
3473
|
}
|
|
3308
3474
|
|
|
3475
|
+
/**
|
|
3476
|
+
* The params of a render for a route's pattern rather than one url: they
|
|
3477
|
+
* never settle, so whatever awaits them suspends and the rest is the shell.
|
|
3478
|
+
* Marked, so a layout awaiting them can be named in the build's refusal.
|
|
3479
|
+
*/
|
|
3480
|
+
const unknownParams = new WeakSet<Promise<unknown>>()
|
|
3481
|
+
|
|
3482
|
+
function patternParams(): Promise<Record<string, unknown>> {
|
|
3483
|
+
const never = new Promise<Record<string, unknown>>(() => {})
|
|
3484
|
+
|
|
3485
|
+
unknownParams.add(never)
|
|
3486
|
+
|
|
3487
|
+
return never
|
|
3488
|
+
}
|
|
3489
|
+
|
|
3490
|
+
/**
|
|
3491
|
+
* What a layout is handed as params: its own segments' and those above it.
|
|
3492
|
+
*
|
|
3493
|
+
* During the pattern render, a layout with no dynamic segment of its own has
|
|
3494
|
+
* nothing to wait for and gets its empty params at once. One with a segment
|
|
3495
|
+
* waits forever, and says so - "params in app/[team]/layout" - so the build
|
|
3496
|
+
* names the layout rather than whichever read happened to be last.
|
|
3497
|
+
*/
|
|
3498
|
+
function layoutParams(component: string, params: Promise<Record<string, unknown>>): Promise<Record<string, unknown>> {
|
|
3499
|
+
if (!unknownParams.has(params)) return params.then((all) => paramsFor(component, all))
|
|
3500
|
+
|
|
3501
|
+
if (segmentNames(component).length === 0) return Promise.resolve({})
|
|
3502
|
+
|
|
3503
|
+
const by = 'params in ' + (component.startsWith('app/') ? component.slice(4) : component)
|
|
3504
|
+
|
|
3505
|
+
return {
|
|
3506
|
+
then(resolve, reject) {
|
|
3507
|
+
noteRequestRead(by)
|
|
3508
|
+
|
|
3509
|
+
return params.then(resolve, reject)
|
|
3510
|
+
},
|
|
3511
|
+
catch: (reject) => params.catch(reject),
|
|
3512
|
+
finally: (done) => params.finally(done),
|
|
3513
|
+
[Symbol.toStringTag]: 'Promise',
|
|
3514
|
+
} as Promise<Record<string, unknown>>
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3309
3517
|
// Composition: layout(outer..inner) > Suspense(loading, innermost-first) > page.
|
|
3310
3518
|
function buildElement(
|
|
3311
3519
|
component: string,
|
|
@@ -3465,8 +3673,13 @@ function buildElement(
|
|
|
3465
3673
|
element = createElement(SegmentBoundary, { depth: i + 1, pageKey }, withPathname(element))
|
|
3466
3674
|
}
|
|
3467
3675
|
|
|
3676
|
+
// Its own segments' params and those above it, awaitable as a page's
|
|
3677
|
+
// are: a layout above [team] is kept mounted across teams, so a deeper
|
|
3678
|
+
// value handed to it would go stale. See paramsFor.
|
|
3679
|
+
const own = layouts[i].component
|
|
3468
3680
|
element = createElement(Layout, {
|
|
3469
3681
|
...(layouts[i].props ?? {}),
|
|
3682
|
+
params: layoutParams(own, params),
|
|
3470
3683
|
...(slotsByLayout.get(i) ?? {}),
|
|
3471
3684
|
children: element,
|
|
3472
3685
|
})
|
|
@@ -4216,7 +4429,18 @@ interface PageContext {
|
|
|
4216
4429
|
* A slot is the only unit smaller than a page the server can name, which is
|
|
4217
4430
|
* why two tables have to be slots to be refreshed apart from each other.
|
|
4218
4431
|
*/
|
|
4219
|
-
|
|
4432
|
+
/** What renderRevalidated answers for a region the calling page does not have. */
|
|
4433
|
+
const SKIPPED = Symbol('rsc-kit.revalidate-skipped')
|
|
4434
|
+
|
|
4435
|
+
/** Whether any page in the app has a section or slot by this name. */
|
|
4436
|
+
function regionOfSomePage(target: string): boolean {
|
|
4437
|
+
return manifest().routes.some((route: any) =>
|
|
4438
|
+
(route.sections ?? []).some((name: string) => name.split('/').pop() === target + '.section') ||
|
|
4439
|
+
Object.hasOwn(route.slots ?? {}, target),
|
|
4440
|
+
)
|
|
4441
|
+
}
|
|
4442
|
+
|
|
4443
|
+
async function renderRevalidated(target: string, page: PageContext, skipElsewhere = false): Promise<unknown> {
|
|
4220
4444
|
// Every target below 'all' renders without the layout chain above it, which
|
|
4221
4445
|
// is the same skip a navigation performs and needs the same guard run.
|
|
4222
4446
|
await runMiddleware(page.component, page.props)
|
|
@@ -4279,6 +4503,12 @@ async function renderRevalidated(target: string, page: PageContext): Promise<unk
|
|
|
4279
4503
|
? page.parallelSlots[target]
|
|
4280
4504
|
: undefined
|
|
4281
4505
|
|
|
4506
|
+
// A region of another page. An action names what it changed, not where
|
|
4507
|
+
// it was called from - a backend's above all, which cannot know - so a
|
|
4508
|
+
// region the calling page does not show is nothing to refresh here. A name
|
|
4509
|
+
// no page has is still a mistake, and refused below.
|
|
4510
|
+
if (!slotComponent && skipElsewhere && regionOfSomePage(target)) return SKIPPED
|
|
4511
|
+
|
|
4282
4512
|
if (!slotComponent) {
|
|
4283
4513
|
throw new Error(
|
|
4284
4514
|
'Cannot revalidate ' + target + ': no section or slot of this page by that name. ' +
|
|
@@ -4457,7 +4687,9 @@ export async function handleAction(
|
|
|
4457
4687
|
const revalidated: Record<string, unknown> = {}
|
|
4458
4688
|
|
|
4459
4689
|
for (const target of targets) {
|
|
4460
|
-
|
|
4690
|
+
const tree = await renderRevalidated(target, page, true)
|
|
4691
|
+
|
|
4692
|
+
if (tree !== SKIPPED) revalidated[target] = tree
|
|
4461
4693
|
}
|
|
4462
4694
|
|
|
4463
4695
|
// Marked, so an action whose own result happens to be an object with a
|
|
@@ -4923,7 +5155,7 @@ export async function handleRscPprShell(
|
|
|
4923
5155
|
0,
|
|
4924
5156
|
pageKey,
|
|
4925
5157
|
true,
|
|
4926
|
-
pageKey ? Promise.resolve(props) :
|
|
5158
|
+
pageKey ? Promise.resolve(props) : patternParams(),
|
|
4927
5159
|
)
|
|
4928
5160
|
// Quiet about a redirect: during the probe it is a classification, not
|
|
4929
5161
|
// a failure, and React would otherwise print a stack for every one.
|
|
@@ -5047,7 +5279,7 @@ async function serve(request: Request): Promise<Response> {
|
|
|
5047
5279
|
${NITRO_HANDLER_OPTIONS}${NITRO_PRERENDERED} maxActionBody: ${maxActionBody === undefined ? "undefined" : String(maxActionBody)},
|
|
5048
5280
|
})
|
|
5049
5281
|
|
|
5050
|
-
${fallbackOrigin ? FALLBACK_BODY :
|
|
5282
|
+
${fallbackOrigin ? FALLBACK_BODY : NO_FALLBACK_BODY}}
|
|
5051
5283
|
|
|
5052
5284
|
/**
|
|
5053
5285
|
* The page for a url nothing answers.
|