@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/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
- * What the backend offers the app, read from a file it wrote:
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, "server-actions.generated.ts");
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
- const bare = audited.filter((a) => !a.client);
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
- // The names the backend listed, so a misspelt one fails the typecheck
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
- async function renderRevalidated(target: string, page: PageContext): Promise<unknown> {
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
- revalidated[target] = await renderRevalidated(target, page)
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) : new Promise(() => {}),
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 : " return (await devHandler(request)) ?? (await notFound(request))\n"}}
5282
+ ${fallbackOrigin ? FALLBACK_BODY : NO_FALLBACK_BODY}}
5051
5283
 
5052
5284
  /**
5053
5285
  * The page for a url nothing answers.