@rsc-kit/core 0.20.6 → 0.20.8
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/cache.d.ts +11 -0
- package/dist/cache.js +13 -0
- package/dist/cache.js.map +1 -1
- package/dist/clientEntries.js +4 -1
- package/dist/clientEntries.js.map +1 -1
- package/dist/files.d.ts +38 -0
- package/dist/files.js +109 -0
- package/dist/files.js.map +1 -1
- package/dist/host.d.ts +11 -2
- package/dist/host.js +37 -6
- package/dist/host.js.map +1 -1
- package/dist/js/ActivityRouter.d.ts +11 -0
- package/dist/js/ActivityRouter.js +24 -4
- package/dist/js/ActivityRouter.js.map +1 -1
- package/dist/js/Form.js +24 -2
- package/dist/js/Form.js.map +1 -1
- package/dist/js/Link.d.ts +1 -1
- package/dist/js/Link.js +25 -9
- package/dist/js/Link.js.map +1 -1
- package/dist/js/PageTransition.d.ts +27 -0
- package/dist/js/PageTransition.js +57 -0
- package/dist/js/PageTransition.js.map +1 -0
- package/dist/js/PathnameProvider.d.ts +15 -2
- package/dist/js/PathnameProvider.js +17 -2
- package/dist/js/PathnameProvider.js.map +1 -1
- package/dist/js/SegmentBoundary.js +27 -3
- package/dist/js/SegmentBoundary.js.map +1 -1
- package/dist/js/activityMarkers.d.ts +7 -0
- package/dist/js/activityMarkers.js +18 -0
- package/dist/js/activityMarkers.js.map +1 -0
- package/dist/js/createViteRscApp.js +122 -76
- package/dist/js/createViteRscApp.js.map +1 -1
- package/dist/js/earlyClicks.d.ts +3 -1
- package/dist/js/earlyClicks.js +28 -5
- package/dist/js/earlyClicks.js.map +1 -1
- package/dist/js/errors.d.ts +2 -0
- package/dist/js/errors.js +11 -0
- package/dist/js/errors.js.map +1 -1
- package/dist/js/imagePreload.d.ts +37 -0
- package/dist/js/imagePreload.js +116 -0
- package/dist/js/imagePreload.js.map +1 -0
- package/dist/js/navigate.d.ts +47 -1
- package/dist/js/navigate.js +249 -10
- package/dist/js/navigate.js.map +1 -1
- package/dist/js/perf.d.ts +25 -0
- package/dist/js/perf.js +57 -0
- package/dist/js/perf.js.map +1 -0
- package/dist/js/router.d.ts +10 -1
- package/dist/js/router.js +9 -2
- package/dist/js/router.js.map +1 -1
- package/dist/js/segmentStore.d.ts +52 -0
- package/dist/js/segmentStore.js +123 -2
- package/dist/js/segmentStore.js.map +1 -1
- package/dist/js/staleAssets.d.ts +10 -0
- package/dist/js/staleAssets.js +23 -2
- package/dist/js/staleAssets.js.map +1 -1
- package/dist/js/viewportPrefetch.d.ts +19 -10
- package/dist/js/viewportPrefetch.js +50 -15
- package/dist/js/viewportPrefetch.js.map +1 -1
- package/dist/shellHead.d.ts +22 -0
- package/dist/shellHead.js +43 -0
- package/dist/shellHead.js.map +1 -0
- package/dist/vite.js +250 -55
- package/dist/vite.js.map +1 -1
- package/package.json +5 -1
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The head of a stored shell, corrected for the url it is served for.
|
|
3
|
+
*
|
|
4
|
+
* A shell for a route that listed no urls is one file for every url the
|
|
5
|
+
* route matches, so its <title> is whatever the build could know without a
|
|
6
|
+
* url: the layouts' metadata, once the page's own generateMetadata was seen
|
|
7
|
+
* to read the params and left out. The host serving it does know the url,
|
|
8
|
+
* and the page's metadata is a function of it - so the title and the
|
|
9
|
+
* description are written into the head here, as a string edit on the way
|
|
10
|
+
* out, before React resumes the holes below. The client's DocumentTitle
|
|
11
|
+
* sets the title again after hydration; this is for the tab before that,
|
|
12
|
+
* and for whoever reads the document without running it.
|
|
13
|
+
*/
|
|
14
|
+
function escapeHtml(text) {
|
|
15
|
+
return text
|
|
16
|
+
.replace(/&/g, "&")
|
|
17
|
+
.replace(/</g, "<")
|
|
18
|
+
.replace(/>/g, ">")
|
|
19
|
+
.replace(/"/g, """);
|
|
20
|
+
}
|
|
21
|
+
const TITLE = /<title(?:\s[^>]*)?>[\s\S]*?<\/title>/i;
|
|
22
|
+
const DESCRIPTION = /<meta\s+name=(?:"description"|'description'|description)\s+content=(?:"[^"]*"|'[^']*'|[^\s>]*)\s*\/?>/i;
|
|
23
|
+
const HEAD_END = /<\/head>/i;
|
|
24
|
+
/**
|
|
25
|
+
* `shell` with the given title and description in its head. Only the two
|
|
26
|
+
* that every share preview and every tab reads; the rest of a page's
|
|
27
|
+
* metadata rides with the payload.
|
|
28
|
+
*/
|
|
29
|
+
export function withHead(shell, metadata) {
|
|
30
|
+
if (!metadata)
|
|
31
|
+
return shell;
|
|
32
|
+
let out = shell;
|
|
33
|
+
if (metadata.title != null) {
|
|
34
|
+
const tag = `<title>${escapeHtml(String(metadata.title))}</title>`;
|
|
35
|
+
out = TITLE.test(out) ? out.replace(TITLE, tag) : out.replace(HEAD_END, `${tag}</head>`);
|
|
36
|
+
}
|
|
37
|
+
if (metadata.description != null) {
|
|
38
|
+
const tag = `<meta name="description" content="${escapeHtml(String(metadata.description))}"/>`;
|
|
39
|
+
out = DESCRIPTION.test(out) ? out.replace(DESCRIPTION, tag) : out.replace(HEAD_END, `${tag}</head>`);
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=shellHead.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shellHead.js","sourceRoot":"","sources":["../src/shellHead.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,SAAS,UAAU,CAAC,IAAY;IAC9B,OAAO,IAAI;SACR,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AAC7B,CAAC;AAED,MAAM,KAAK,GAAG,uCAAuC,CAAC;AACtD,MAAM,WAAW,GAAG,wGAAwG,CAAC;AAC7H,MAAM,QAAQ,GAAG,WAAW,CAAC;AAE7B;;;;GAIG;AACH,MAAM,UAAU,QAAQ,CACtB,KAAa,EACb,QAA2D;IAE3D,IAAI,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAC;IAE5B,IAAI,GAAG,GAAG,KAAK,CAAC;IAEhB,IAAI,QAAQ,CAAC,KAAK,IAAI,IAAI,EAAE,CAAC;QAC3B,MAAM,GAAG,GAAG,UAAU,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,UAAU,CAAC;QAEnE,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,GAAG,SAAS,CAAC,CAAC;IAC3F,CAAC;IAED,IAAI,QAAQ,CAAC,WAAW,IAAI,IAAI,EAAE,CAAC;QACjC,MAAM,GAAG,GAAG,qCAAqC,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,KAAK,CAAC;QAE/F,GAAG,GAAG,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,GAAG,SAAS,CAAC,CAAC;IACvG,CAAC;IAED,OAAO,GAAG,CAAC;AACb,CAAC","sourcesContent":["/**\n * The head of a stored shell, corrected for the url it is served for.\n *\n * A shell for a route that listed no urls is one file for every url the\n * route matches, so its <title> is whatever the build could know without a\n * url: the layouts' metadata, once the page's own generateMetadata was seen\n * to read the params and left out. The host serving it does know the url,\n * and the page's metadata is a function of it - so the title and the\n * description are written into the head here, as a string edit on the way\n * out, before React resumes the holes below. The client's DocumentTitle\n * sets the title again after hydration; this is for the tab before that,\n * and for whoever reads the document without running it.\n */\n\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\");\n}\n\nconst TITLE = /<title(?:\\s[^>]*)?>[\\s\\S]*?<\\/title>/i;\nconst DESCRIPTION = /<meta\\s+name=(?:\"description\"|'description'|description)\\s+content=(?:\"[^\"]*\"|'[^']*'|[^\\s>]*)\\s*\\/?>/i;\nconst HEAD_END = /<\\/head>/i;\n\n/**\n * `shell` with the given title and description in its head. Only the two\n * that every share preview and every tab reads; the rest of a page's\n * metadata rides with the payload.\n */\nexport function withHead(\n shell: string,\n metadata: { title?: unknown; description?: unknown } | null,\n): string {\n if (!metadata) return shell;\n\n let out = shell;\n\n if (metadata.title != null) {\n const tag = `<title>${escapeHtml(String(metadata.title))}</title>`;\n\n out = TITLE.test(out) ? out.replace(TITLE, tag) : out.replace(HEAD_END, `${tag}</head>`);\n }\n\n if (metadata.description != null) {\n const tag = `<meta name=\"description\" content=\"${escapeHtml(String(metadata.description))}\"/>`;\n\n out = DESCRIPTION.test(out) ? out.replace(DESCRIPTION, tag) : out.replace(HEAD_END, `${tag}</head>`);\n }\n\n return out;\n}\n"]}
|
package/dist/vite.js
CHANGED
|
@@ -42,8 +42,23 @@ let inlineStylesheets = "auto";
|
|
|
42
42
|
let resolvedConfig = null;
|
|
43
43
|
/** Server files importing a client library, read off the rsc graph when it is built. */
|
|
44
44
|
let clientLibraryImports = [];
|
|
45
|
-
/**
|
|
46
|
-
|
|
45
|
+
/**
|
|
46
|
+
* The Nitro preset this build targets, from the module's setup, for the
|
|
47
|
+
* decisions buildApp makes about where stored pages can live.
|
|
48
|
+
*/
|
|
49
|
+
let nitroPreset = null;
|
|
50
|
+
/**
|
|
51
|
+
* Modules a runtime provides and no bundle should try to carry.
|
|
52
|
+
*
|
|
53
|
+
* Bun's, and the Workers runtime's: `cloudflare:workers` is where a Worker
|
|
54
|
+
* reads its bindings - `import { env } from 'cloudflare:workers'` for the
|
|
55
|
+
* D1 database or the R2 bucket wrangler.jsonc names - and a bundle that
|
|
56
|
+
* tried to resolve it at build time found nothing to resolve. Import it
|
|
57
|
+
* where the code runs on Workers; a `vite dev` under Bun or Node has no such
|
|
58
|
+
* module, so an app that also runs there imports it lazily, behind the
|
|
59
|
+
* check for the runtime.
|
|
60
|
+
*/
|
|
61
|
+
const RUNTIME_BUILTINS = ["bun", /^bun:/, /^cloudflare:/];
|
|
47
62
|
/**
|
|
48
63
|
* Packages the server bundles import rather than inline, by default.
|
|
49
64
|
*
|
|
@@ -394,7 +409,9 @@ function log(...args) {
|
|
|
394
409
|
* pattern is the host's dialect: Laravel writes {slug}, Hono writes :slug, and
|
|
395
410
|
* neither is the plugin's business.
|
|
396
411
|
*/
|
|
397
|
-
/**
|
|
412
|
+
/** The names a top-level parameter directory can have to bind the host rather than a path segment. */
|
|
413
|
+
const HOST_SEGMENTS = new Set(["domain", "host"]);
|
|
414
|
+
/** `[...path]` → catchAll, `[id]` → param, `[domain]` at the top → host, `(group)` → nothing at all. */
|
|
398
415
|
function urlSegments(componentName) {
|
|
399
416
|
const parts = componentName.split("/").slice(1, -1);
|
|
400
417
|
const segments = [];
|
|
@@ -410,11 +427,15 @@ function urlSegments(componentName) {
|
|
|
410
427
|
continue;
|
|
411
428
|
}
|
|
412
429
|
if (part.startsWith("[") && part.endsWith("]")) {
|
|
413
|
-
|
|
414
|
-
// the
|
|
430
|
+
const value = part.slice(1, -1);
|
|
431
|
+
// At the top of app/ - the first segment the url has - a [domain] or a
|
|
432
|
+
// [host] is the host's: bound from acme.example.com, never from
|
|
433
|
+
// example.com/acme. Any other name there is a path parameter, as it is
|
|
434
|
+
// in Next: a port's app/[collection]/page.tsx was read as a tenant tree
|
|
435
|
+
// and every collection url answered 404.
|
|
415
436
|
segments.push({
|
|
416
|
-
type: segments.length === 0 ? "host" : "param",
|
|
417
|
-
value
|
|
437
|
+
type: segments.length === 0 && HOST_SEGMENTS.has(value) ? "host" : "param",
|
|
438
|
+
value,
|
|
418
439
|
});
|
|
419
440
|
continue;
|
|
420
441
|
}
|
|
@@ -2453,6 +2474,7 @@ import { prerenderedBeside } from ${JSON.stringify(join(packageDir, "files"))}
|
|
|
2453
2474
|
import { renderToReadableStream, decodeReply, decodeAction, decodeFormState, loadServerAction } from '@vitejs/plugin-rsc/rsc'
|
|
2454
2475
|
import { isQuery, queryCacheControl, isQueryValidationError } from ${JSON.stringify(join(packageDir, "query"))}
|
|
2455
2476
|
import { isActionValidationError, isClientBuilt } from ${JSON.stringify(join(packageDir, "action"))}
|
|
2477
|
+
import { forgetCached } from ${JSON.stringify(join(packageDir, "cache"))}
|
|
2456
2478
|
import { noteFallback as noteCaughtRead } from ${JSON.stringify(join(packageDir, "request"))}
|
|
2457
2479
|
import { isOutdatedOptimizedDep, outdatedDepResponse } from ${JSON.stringify(join(packageDir, "devReload"))}
|
|
2458
2480
|
import { sharedDepth } from ${JSON.stringify(join(packageDir, "routing"))}
|
|
@@ -3076,10 +3098,27 @@ function buildElement(
|
|
|
3076
3098
|
// it reads, everything above it still paints, and that is a shell one file
|
|
3077
3099
|
// can serve for every url the route matches.
|
|
3078
3100
|
params: Promise<Record<string, unknown>> = Promise.resolve(props),
|
|
3101
|
+
// The url the client hooks answer with during a server render, when it is
|
|
3102
|
+
// not the page key. Undefined: the page key, or unknown when that is
|
|
3103
|
+
// empty. Null: unknown, whatever the key - the payload a pattern shell's
|
|
3104
|
+
// document boots from is keyed by its url and still must not know it.
|
|
3105
|
+
pathname: string | null | undefined = undefined,
|
|
3079
3106
|
) {
|
|
3080
3107
|
const Component = components[component]
|
|
3081
3108
|
if (!Component) throw new Error('Unknown RSC component: ' + component)
|
|
3082
3109
|
|
|
3110
|
+
// Rendered at the top and again inside every boundary, and always with
|
|
3111
|
+
// the runtime - so a segment has the same shape whichever way it arrived.
|
|
3112
|
+
// A page reached by a partial navigation had the provider at the root of
|
|
3113
|
+
// its segment, a page reached by a document load had it outside every
|
|
3114
|
+
// boundary, and a revalidation rendered with no page key had none: React
|
|
3115
|
+
// saw a different component at the root of the segment and remounted
|
|
3116
|
+
// everything under it. A port lost the one-time secret in a modal that
|
|
3117
|
+
// way. With the provider at every level the three trees agree.
|
|
3118
|
+
const rendered = pathname === undefined ? pageKey || null : pathname
|
|
3119
|
+
const withPathname = (node: unknown): unknown =>
|
|
3120
|
+
bootstrap ? createElement(PathnameProvider, { value: rendered }, node) : node
|
|
3121
|
+
|
|
3083
3122
|
// Awaitable rather than spread. Spread, a page reads its slug synchronously
|
|
3084
3123
|
// and renders to completion during the probe — producing a page about an
|
|
3085
3124
|
// invented value, right for nothing — which is why such a route could only
|
|
@@ -3198,7 +3237,7 @@ function buildElement(
|
|
|
3198
3237
|
// one — otherwise every page would drag React in for a seam nothing can
|
|
3199
3238
|
// use, and no page could ever be JS-free.
|
|
3200
3239
|
if (bootstrap) {
|
|
3201
|
-
element = createElement(SegmentBoundary, { depth: i + 1, pageKey }, element)
|
|
3240
|
+
element = createElement(SegmentBoundary, { depth: i + 1, pageKey }, withPathname(element))
|
|
3202
3241
|
}
|
|
3203
3242
|
|
|
3204
3243
|
element = createElement(Layout, {
|
|
@@ -3208,14 +3247,8 @@ function buildElement(
|
|
|
3208
3247
|
})
|
|
3209
3248
|
}
|
|
3210
3249
|
|
|
3211
|
-
//
|
|
3212
|
-
|
|
3213
|
-
// the runtime, since a route shipping none has nothing to read it.
|
|
3214
|
-
if (bootstrap && pageKey) {
|
|
3215
|
-
element = createElement(PathnameProvider, { value: pageKey }, element)
|
|
3216
|
-
}
|
|
3217
|
-
|
|
3218
|
-
return element
|
|
3250
|
+
// And outside the boundaries, for the root layout, which is above them all.
|
|
3251
|
+
return withPathname(element)
|
|
3219
3252
|
}
|
|
3220
3253
|
|
|
3221
3254
|
// Resolve route metadata into React elements. React 19 hoists <title>/<meta>
|
|
@@ -3232,11 +3265,21 @@ async function renderTree(
|
|
|
3232
3265
|
pageKey = '',
|
|
3233
3266
|
bootstrap = true,
|
|
3234
3267
|
params?: Promise<Record<string, unknown>>,
|
|
3268
|
+
pathname: string | null | undefined = undefined,
|
|
3269
|
+
// What generateMetadata is given, when it is not what the page is given.
|
|
3270
|
+
// A resume of a pattern shell renders the page for its real params and
|
|
3271
|
+
// the metadata as the shell did - left out where it read them - because
|
|
3272
|
+
// a tree with a <title> the shell had not got is a tree that does not
|
|
3273
|
+
// match, and React then fills nothing.
|
|
3274
|
+
metadataParams: Promise<Record<string, unknown>> | undefined = params,
|
|
3235
3275
|
) {
|
|
3236
3276
|
// The FULL chain, always: a title template lives on an outer layout, and a
|
|
3237
3277
|
// partial render still has to produce the same <title> the whole document
|
|
3238
3278
|
// would have.
|
|
3239
|
-
|
|
3279
|
+
// The params promise travels too: during the pattern probe it never
|
|
3280
|
+
// settles, and a generateMetadata that reads it is then left out of the
|
|
3281
|
+
// shell rather than run against the placeholder - see resolveMetadata.
|
|
3282
|
+
const md = await resolveMetadata(component, props, layouts, metadataParams)
|
|
3240
3283
|
const head: unknown[] = []
|
|
3241
3284
|
|
|
3242
3285
|
// Rendered into the tree rather than written into the app's layout: React
|
|
@@ -3483,7 +3526,7 @@ async function renderTree(
|
|
|
3483
3526
|
|
|
3484
3527
|
// Metadata elements are rendered INSIDE the document tree so React 19 hoists
|
|
3485
3528
|
// <title>/<meta> into <head> (hoisting only works from within the tree).
|
|
3486
|
-
return buildElement(component, props, layouts, loadings, parallelSlots, slotOverrides, head, from, pageKey, bootstrap, params)
|
|
3529
|
+
return buildElement(component, props, layouts, loadings, parallelSlots, slotOverrides, head, from, pageKey, bootstrap, params, pathname)
|
|
3487
3530
|
}
|
|
3488
3531
|
|
|
3489
3532
|
/**
|
|
@@ -3655,6 +3698,10 @@ export async function handleRscStream(
|
|
|
3655
3698
|
slotOverrides: Record<string, SlotOverride> = {},
|
|
3656
3699
|
from = 0,
|
|
3657
3700
|
pageKey = '',
|
|
3701
|
+
// The url the client hooks answer with. Null for the payload a document
|
|
3702
|
+
// served from a pattern shell boots from: the shell did not know its url,
|
|
3703
|
+
// and hydration has to agree with the shell. See PathnameProvider.
|
|
3704
|
+
pathname: string | null | undefined = undefined,
|
|
3658
3705
|
): Promise<{ stream: ReadableStream; clientChunks: unknown; segmentDepth: number }> {
|
|
3659
3706
|
await instrumented()
|
|
3660
3707
|
applyHost()
|
|
@@ -3668,7 +3715,7 @@ export async function handleRscStream(
|
|
|
3668
3715
|
|
|
3669
3716
|
return {
|
|
3670
3717
|
stream: renderToReadableStream(
|
|
3671
|
-
await renderTree(component, props, layouts, loadings, parallelSlots, slotOverrides, start, pageKey),
|
|
3718
|
+
await renderTree(component, props, layouts, loadings, parallelSlots, slotOverrides, start, pageKey, true, undefined, pathname ?? null),
|
|
3672
3719
|
{ onError: flightOnError },
|
|
3673
3720
|
),
|
|
3674
3721
|
clientChunks: {},
|
|
@@ -3795,6 +3842,11 @@ export async function handleRscResume(
|
|
|
3795
3842
|
postponed: unknown = null,
|
|
3796
3843
|
nonce?: string,
|
|
3797
3844
|
pageKey = '',
|
|
3845
|
+
// The request's url, for the hooks: a pattern shell is keyed with none,
|
|
3846
|
+
// and the holes it leaves are still rendered for a real url. A breadcrumb
|
|
3847
|
+
// in one streamed empty and the client, knowing the url, disagreed on
|
|
3848
|
+
// every document load.
|
|
3849
|
+
pathname: string | null | undefined = undefined,
|
|
3798
3850
|
): Promise<{ htmlStream: ReadableStream }> {
|
|
3799
3851
|
await instrumented()
|
|
3800
3852
|
applyHost()
|
|
@@ -3811,7 +3863,28 @@ export async function handleRscResume(
|
|
|
3811
3863
|
// all, so the shell's remains the only one. What this flag actually decides
|
|
3812
3864
|
// is whether the tree carries its SegmentBoundary, and the shell's did.
|
|
3813
3865
|
const flight = renderToReadableStream(
|
|
3814
|
-
await renderTree(
|
|
3866
|
+
await renderTree(
|
|
3867
|
+
component,
|
|
3868
|
+
props,
|
|
3869
|
+
layouts,
|
|
3870
|
+
loadings,
|
|
3871
|
+
parallelSlots,
|
|
3872
|
+
slotOverrides,
|
|
3873
|
+
0,
|
|
3874
|
+
pageKey,
|
|
3875
|
+
true,
|
|
3876
|
+
undefined,
|
|
3877
|
+
// A pattern shell was rendered for no url, and the holes that resume
|
|
3878
|
+
// it render the same way: a breadcrumb in one would otherwise be
|
|
3879
|
+
// rendered here for the url and hydrated against a payload that, like
|
|
3880
|
+
// the shell, does not know it. The browser fills it in.
|
|
3881
|
+
pageKey === '' ? null : pathname,
|
|
3882
|
+
// And with params that never settled, its metadata left out where it
|
|
3883
|
+
// read them; the same here, or the tree has a <title> the shell had
|
|
3884
|
+
// not and the slots stop matching. The host writes the real title into
|
|
3885
|
+
// the head as it serves the shell.
|
|
3886
|
+
pageKey === '' ? new Promise<Record<string, unknown>>(() => {}) : undefined,
|
|
3887
|
+
),
|
|
3815
3888
|
{ onError: flightOnError },
|
|
3816
3889
|
)
|
|
3817
3890
|
|
|
@@ -3826,6 +3899,8 @@ export async function handleRscResume(
|
|
|
3826
3899
|
interface PageContext {
|
|
3827
3900
|
component: string
|
|
3828
3901
|
props: Record<string, unknown>
|
|
3902
|
+
/** The page's url, as the host matched it: the key a navigation to it renders under. */
|
|
3903
|
+
url?: string
|
|
3829
3904
|
layouts: LayoutEntry[]
|
|
3830
3905
|
loadings: string[]
|
|
3831
3906
|
parallelSlots: Record<string, string>
|
|
@@ -3856,6 +3931,9 @@ async function renderRevalidated(target: string, page: PageContext): Promise<unk
|
|
|
3856
3931
|
await runMiddleware(page.component, page.props)
|
|
3857
3932
|
|
|
3858
3933
|
if (target === 'all' || target === 'page') {
|
|
3934
|
+
// Keyed by the page's url, as a navigation to it is: the tree replaces
|
|
3935
|
+
// the one on screen in place only if it has the same shape, and the
|
|
3936
|
+
// key decides the shape.
|
|
3859
3937
|
return renderTree(
|
|
3860
3938
|
page.component,
|
|
3861
3939
|
page.props,
|
|
@@ -3864,7 +3942,7 @@ async function renderRevalidated(target: string, page: PageContext): Promise<unk
|
|
|
3864
3942
|
page.parallelSlots,
|
|
3865
3943
|
{},
|
|
3866
3944
|
target === 'all' ? 0 : page.layouts.length,
|
|
3867
|
-
'',
|
|
3945
|
+
page.url ?? '',
|
|
3868
3946
|
)
|
|
3869
3947
|
}
|
|
3870
3948
|
|
|
@@ -4073,6 +4151,12 @@ export async function handleAction(
|
|
|
4073
4151
|
return { stream: renderToReadableStream(result) }
|
|
4074
4152
|
}
|
|
4075
4153
|
|
|
4154
|
+
// The render is of the world after the write. Everything cache() answered
|
|
4155
|
+
// before it - the agent the middleware loaded, the settings the action
|
|
4156
|
+
// read to compare - was true before the write, and the sidebar rendered
|
|
4157
|
+
// from those memos still showed the old name after a rename.
|
|
4158
|
+
forgetCached()
|
|
4159
|
+
|
|
4076
4160
|
const revalidated: Record<string, unknown> = {}
|
|
4077
4161
|
|
|
4078
4162
|
for (const target of targets) {
|
|
@@ -4084,25 +4168,68 @@ export async function handleAction(
|
|
|
4084
4168
|
return { stream: renderToReadableStream({ __rscRevalidated: revalidated, result }) }
|
|
4085
4169
|
}
|
|
4086
4170
|
|
|
4171
|
+
/**
|
|
4172
|
+
* How long a generateMetadata gets to answer during the pattern probe before
|
|
4173
|
+
* it is taken to have awaited the params, which never settle there.
|
|
4174
|
+
*
|
|
4175
|
+
* Not zero: a function that awaits nothing of the request still crosses a
|
|
4176
|
+
* microtask or two. Anything still pending after this has read the params or
|
|
4177
|
+
* reached for the host - both are the page's data, and neither is in a shell.
|
|
4178
|
+
*/
|
|
4179
|
+
const METADATA_PROBE_GRACE_MS = 20
|
|
4180
|
+
|
|
4181
|
+
const METADATA_POSTPONED = Symbol('metadata postponed')
|
|
4182
|
+
|
|
4087
4183
|
export async function resolveMetadata(
|
|
4088
4184
|
component: string,
|
|
4089
4185
|
props: Record<string, unknown> = {},
|
|
4090
4186
|
layouts: LayoutEntry[] = [],
|
|
4187
|
+
// The page's own params promise, when the caller has one. During the
|
|
4188
|
+
// pattern probe it never settles - see handleRscPprShell.
|
|
4189
|
+
params?: Promise<Record<string, unknown>>,
|
|
4091
4190
|
): Promise<Record<string, unknown> | null> {
|
|
4092
4191
|
await instrumented()
|
|
4093
4192
|
|
|
4094
4193
|
const pageEntry = metadataMap[component]
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4194
|
+
|
|
4195
|
+
// What a route file declares, static or generated. A generateMetadata is
|
|
4196
|
+
// given the same awaitables the page receives - and, during the pattern
|
|
4197
|
+
// probe, params that never settle and a moment: one that answers did not
|
|
4198
|
+
// need them and is the shell's; one still pending did, and is left out.
|
|
4199
|
+
// A shell for a route that listed no urls is rendered for the pattern, and
|
|
4200
|
+
// a title read from the params would be read from the placeholder the
|
|
4201
|
+
// build invented: a port's training-sessions/[id] shell said "Training
|
|
4202
|
+
// Session" over a payload for /new that said "New Training Session". Next
|
|
4203
|
+
// treats params as dynamic under PPR and postpones the metadata. The host
|
|
4204
|
+
// puts the real title in when it serves the shell for a url, and the
|
|
4205
|
+
// client's DocumentTitle sets it again after hydration.
|
|
4206
|
+
const declared = async (entry: (typeof metadataMap)[string] | undefined): Promise<Record<string, unknown>> => {
|
|
4207
|
+
if (!entry) return {}
|
|
4208
|
+
|
|
4209
|
+
if (!entry.generate) return entry.static ? { ...entry.static } : {}
|
|
4210
|
+
|
|
4211
|
+
const generated = entry.generate({
|
|
4212
|
+
params: params ?? Promise.resolve(props),
|
|
4213
|
+
searchParams: pageSearchParams(),
|
|
4214
|
+
})
|
|
4215
|
+
|
|
4216
|
+
const settled = params
|
|
4217
|
+
? await Promise.race([
|
|
4218
|
+
Promise.resolve(generated),
|
|
4219
|
+
new Promise<typeof METADATA_POSTPONED>((resolve) =>
|
|
4220
|
+
setTimeout(() => resolve(METADATA_POSTPONED), METADATA_PROBE_GRACE_MS),
|
|
4221
|
+
),
|
|
4222
|
+
])
|
|
4223
|
+
: await generated
|
|
4224
|
+
|
|
4225
|
+
return settled === METADATA_POSTPONED ? {} : ((settled as Record<string, unknown> | undefined) ?? {})
|
|
4226
|
+
}
|
|
4227
|
+
|
|
4228
|
+
// Layouts too, outer to inner: Next runs a layout's generateMetadata, and
|
|
4229
|
+
// a port had the category's title in its layout - which rendered as the
|
|
4230
|
+
// site's name on every category page until this read it.
|
|
4231
|
+
const layoutMeta = await Promise.all(layouts.map((l) => declared(metadataMap[l.component])))
|
|
4232
|
+
const page = await declared(pageEntry)
|
|
4106
4233
|
|
|
4107
4234
|
// Non-title metadata: layout defaults (outer→inner), page overrides.
|
|
4108
4235
|
//
|
|
@@ -4121,26 +4248,30 @@ export async function resolveMetadata(
|
|
|
4121
4248
|
}
|
|
4122
4249
|
}
|
|
4123
4250
|
|
|
4124
|
-
for (const
|
|
4125
|
-
const s = metadataMap[l.component]?.static
|
|
4126
|
-
|
|
4127
|
-
if (s) take(s as Record<string, unknown>)
|
|
4128
|
-
}
|
|
4251
|
+
for (const m of layoutMeta) take(m)
|
|
4129
4252
|
|
|
4130
4253
|
take(page)
|
|
4131
4254
|
|
|
4132
4255
|
if (Object.keys(other).length > 0) merged.other = other
|
|
4133
4256
|
|
|
4134
|
-
// Title: the page
|
|
4135
|
-
//
|
|
4257
|
+
// Title: the page's, or the nearest layout's own string or default - and
|
|
4258
|
+
// then the nearest template ABOVE whichever supplied it, as Next applies a
|
|
4259
|
+
// layout's template to the titles of the segments below it. A layout's
|
|
4260
|
+
// title used to stop the walk before any template: a category named in
|
|
4261
|
+
// its layout lost the site's suffix every page had.
|
|
4136
4262
|
let title: string | undefined = typeof page.title === 'string' ? page.title : undefined
|
|
4137
4263
|
for (let i = layouts.length - 1; i >= 0; i--) {
|
|
4138
|
-
const lt =
|
|
4264
|
+
const lt = layoutMeta[i]?.title as
|
|
4139
4265
|
| string | { template?: string; default?: string } | undefined
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
if (
|
|
4143
|
-
|
|
4266
|
+
|
|
4267
|
+
if (title == null) {
|
|
4268
|
+
if (lt && typeof lt === 'object' && lt.default) title = lt.default
|
|
4269
|
+
else if (typeof lt === 'string') title = lt
|
|
4270
|
+
|
|
4271
|
+
continue
|
|
4272
|
+
}
|
|
4273
|
+
|
|
4274
|
+
if (lt && typeof lt === 'object' && lt.template) { title = lt.template.replace('%s', title); break }
|
|
4144
4275
|
}
|
|
4145
4276
|
if (title != null) merged.title = title
|
|
4146
4277
|
|
|
@@ -4389,6 +4520,17 @@ export async function handleRscPprShell(
|
|
|
4389
4520
|
// page, stored, with the build reporting success.
|
|
4390
4521
|
let renderFailure: string | undefined
|
|
4391
4522
|
|
|
4523
|
+
// A read the server could not answer, caught at a boundary: the query,
|
|
4524
|
+
// which a server never has, or the pathname, which a shell for a pattern
|
|
4525
|
+
// does not. Each hook throws with its own digest; the build names the hook.
|
|
4526
|
+
const caughtReadOf = (error: unknown): { hook: string; digest: string } | null => {
|
|
4527
|
+
const digest = (error as { digest?: string } | null)?.digest
|
|
4528
|
+
|
|
4529
|
+
if (digest === 'rsc-kit:search-params-fallback') return { hook: 'useSearchParams()', digest }
|
|
4530
|
+
|
|
4531
|
+
return null
|
|
4532
|
+
}
|
|
4533
|
+
|
|
4392
4534
|
const noteFailure = (e: unknown, info?: { componentStack?: string }): string | undefined => {
|
|
4393
4535
|
const digest = redirectDigest(e)
|
|
4394
4536
|
|
|
@@ -4399,10 +4541,12 @@ export async function handleRscPprShell(
|
|
|
4399
4541
|
// there is what gets stored. Noted with its component so the build can
|
|
4400
4542
|
// say so on the route's line; the digest goes into the document for the
|
|
4401
4543
|
// browser to recognise.
|
|
4402
|
-
|
|
4544
|
+
const caught = caughtReadOf(e)
|
|
4545
|
+
|
|
4546
|
+
if (caught) {
|
|
4403
4547
|
const where = /at ([A-Z_$][\\w$]*)/.exec(info?.componentStack ?? '')?.[1]
|
|
4404
|
-
noteCaughtRead(
|
|
4405
|
-
return
|
|
4548
|
+
noteCaughtRead(caught.hook + (where ? ' in ' + where : ''))
|
|
4549
|
+
return caught.digest
|
|
4406
4550
|
}
|
|
4407
4551
|
|
|
4408
4552
|
// Every probe ends by aborting, so React reports that abort. Asked of the
|
|
@@ -4729,10 +4873,11 @@ export async function handleSsrPrerender(
|
|
|
4729
4873
|
// Aborting is how this ends, so React's report of it is not news. A read
|
|
4730
4874
|
// caught at a boundary is: the build attaches it to the route.
|
|
4731
4875
|
onError: (error: unknown, info?: { componentStack?: string }) => {
|
|
4732
|
-
|
|
4876
|
+
const caught = caughtReadOf(error)
|
|
4877
|
+
if (!caught) return
|
|
4733
4878
|
const where = /at ([A-Z_$][\\w$]*)/.exec(info?.componentStack ?? '')?.[1]
|
|
4734
|
-
noteFallback(
|
|
4735
|
-
return
|
|
4879
|
+
noteFallback(caught.hook + (where ? ' in ' + where : ''))
|
|
4880
|
+
return caught.digest
|
|
4736
4881
|
},
|
|
4737
4882
|
})
|
|
4738
4883
|
|
|
@@ -4750,6 +4895,19 @@ export async function handleSsrPrerender(
|
|
|
4750
4895
|
* so React writes it into the document. Under a boundary the developer wrote
|
|
4751
4896
|
* there is nothing to say; with nothing closer than a loading.tsx, one line.
|
|
4752
4897
|
*/
|
|
4898
|
+
/**
|
|
4899
|
+
* A read the server could not answer, caught at a boundary: the query, which
|
|
4900
|
+
* a server never has, or the pathname, which a shell for a pattern does not.
|
|
4901
|
+
* Each hook throws with its own digest; the build names the hook.
|
|
4902
|
+
*/
|
|
4903
|
+
function caughtReadOf(error: unknown): { hook: string; digest: string } | null {
|
|
4904
|
+
const digest = (error as { digest?: string } | null)?.digest
|
|
4905
|
+
|
|
4906
|
+
if (digest === 'rsc-kit:search-params-fallback') return { hook: 'useSearchParams()', digest }
|
|
4907
|
+
|
|
4908
|
+
return null
|
|
4909
|
+
}
|
|
4910
|
+
|
|
4753
4911
|
function reportRenderError(phase: 'ssr' | 'resume') {
|
|
4754
4912
|
return (error: unknown, info?: { componentStack?: string }) => {
|
|
4755
4913
|
// The consumer cancelled - a browser that left mid-stream, a prefetch
|
|
@@ -4765,19 +4923,21 @@ function reportRenderError(phase: 'ssr' | 'resume') {
|
|
|
4765
4923
|
// times or once.
|
|
4766
4924
|
if (parseRedirectDigest(digest) || isNotFoundDigest(digest)) return digest
|
|
4767
4925
|
|
|
4768
|
-
|
|
4926
|
+
const caught = caughtReadOf(error)
|
|
4927
|
+
|
|
4928
|
+
if (caught) {
|
|
4769
4929
|
// The component is the first frame of React's stack. Noted on the
|
|
4770
4930
|
// request so the build attaches it to the route; printed as one line,
|
|
4771
4931
|
// not a stack, so the server says which boundary it wants.
|
|
4772
4932
|
const where = /at ([A-Z_$][\\w$]*)/.exec(info?.componentStack ?? '')?.[1]
|
|
4773
4933
|
|
|
4774
|
-
noteFallback(
|
|
4934
|
+
noteFallback(caught.hook + (where ? ' in ' + where : ''))
|
|
4775
4935
|
|
|
4776
4936
|
if (caughtByLoading(info?.componentStack)) {
|
|
4777
4937
|
console.error(
|
|
4778
4938
|
'[rsc-kit] ' + (where ? where + ': ' : '') +
|
|
4779
|
-
'
|
|
4780
|
-
'segment shows that fallback until the
|
|
4939
|
+
caught.hook + ' was read on the server with nothing closer than a loading.tsx, so the whole ' +
|
|
4940
|
+
'segment shows that fallback until the value arrives. A <Suspense> around the component that reads ' +
|
|
4781
4941
|
'keeps the rest of the page painted.',
|
|
4782
4942
|
)
|
|
4783
4943
|
}
|
|
@@ -5511,6 +5671,24 @@ export function rscKit(options = {}) {
|
|
|
5511
5671
|
setup(nitro) {
|
|
5512
5672
|
if (nitro.options.dev)
|
|
5513
5673
|
return;
|
|
5674
|
+
nitroPreset = nitro.options.preset ?? null;
|
|
5675
|
+
// On Workers the stored pages ride the static assets, not the bundle.
|
|
5676
|
+
// Copied once Nitro has listed its public files, so the copies are
|
|
5677
|
+
// not in that list: a path in it is served as an asset before the
|
|
5678
|
+
// Worker runs, and a stored page of a guarded route would then be
|
|
5679
|
+
// public. Off the list and behind run_worker_first, a request for
|
|
5680
|
+
// one reaches the app, which has no such route, and the pages are
|
|
5681
|
+
// read only through the binding. See ASSETS_PREFIX in files.ts.
|
|
5682
|
+
if (nitro.options.preset?.startsWith("cloudflare")) {
|
|
5683
|
+
nitro.hooks.hook("compiled", async () => {
|
|
5684
|
+
const { storedPagesToAssets } = await import("./files.js");
|
|
5685
|
+
const serverDir = nitro.options.output.serverDir;
|
|
5686
|
+
const publicDir = nitro.options.output.publicDir ?? join(dirname(serverDir), "public");
|
|
5687
|
+
const copied = await storedPagesToAssets(join(serverDir, NITRO_STATIC_DIR), publicDir, join(serverDir, "wrangler.json"));
|
|
5688
|
+
if (copied > 0)
|
|
5689
|
+
log(`stored pages: ${copied} files uploaded as assets, read through the ASSETS binding`);
|
|
5690
|
+
});
|
|
5691
|
+
}
|
|
5514
5692
|
// Said at the end of a bun build, only when true: which module keeps
|
|
5515
5693
|
// `bun build --compile --bytecode` from applying, and where. Bun
|
|
5516
5694
|
// itself names no file and exits 0. See bytecodeCheck.ts.
|
|
@@ -5816,6 +5994,17 @@ export function rscKit(options = {}) {
|
|
|
5816
5994
|
rollupOptions: {
|
|
5817
5995
|
input: { index: join(genDir, "entry.rsc.tsx") },
|
|
5818
5996
|
external: serverExternals,
|
|
5997
|
+
// The server-references map is `id: () => import(file)` for
|
|
5998
|
+
// every "use server" file, and the page that passes an action
|
|
5999
|
+
// to <Form> imports the same file statically. Rolldown says
|
|
6000
|
+
// so for each one: "dynamically imported by
|
|
6001
|
+
// virtual:vite-rsc/server-references but also statically
|
|
6002
|
+
// imported by page.tsx, dynamic import will not move module
|
|
6003
|
+
// into another chunk". Nothing was meant to move: a server
|
|
6004
|
+
// bundle has no chunk to lazy-load from a browser, and the
|
|
6005
|
+
// map is a lookup table, not a split point. One line per
|
|
6006
|
+
// action in every build was a paragraph nobody could act on.
|
|
6007
|
+
checks: { ineffectiveDynamicImport: false },
|
|
5819
6008
|
},
|
|
5820
6009
|
},
|
|
5821
6010
|
resolve: { noExternal: bundledClientPackages },
|
|
@@ -5826,6 +6015,8 @@ export function rscKit(options = {}) {
|
|
|
5826
6015
|
rollupOptions: {
|
|
5827
6016
|
input: { index: join(genDir, "entry.ssr.tsx") },
|
|
5828
6017
|
external: serverExternals,
|
|
6018
|
+
// Same table, same reason.
|
|
6019
|
+
checks: { ineffectiveDynamicImport: false },
|
|
5829
6020
|
},
|
|
5830
6021
|
},
|
|
5831
6022
|
resolve: { noExternal: bundledClientPackages },
|
|
@@ -6067,7 +6258,11 @@ export function rscKit(options = {}) {
|
|
|
6067
6258
|
// module is uploaded as a sibling of the bundle and imported at runtime
|
|
6068
6259
|
// when the directory is not there. Only under Nitro, whose presets are
|
|
6069
6260
|
// the ones without a disk; on its own the plugin serves from outDir.
|
|
6070
|
-
|
|
6261
|
+
// Not on Workers, where the pages ride the static assets instead - an
|
|
6262
|
+
// inline module counts against the script's size limit, and a port's
|
|
6263
|
+
// 571 stored pages were 69 MB of one. See the Nitro module's compiled
|
|
6264
|
+
// hook.
|
|
6265
|
+
if (clientOut && existsSync(staticDir) && !nitroPreset?.startsWith("cloudflare")) {
|
|
6071
6266
|
const { inlineModuleName, inlineModuleSource, compileEntrySource } = await import("./files.js");
|
|
6072
6267
|
writeFileSync(join(dirname(staticDir), inlineModuleName(NITRO_STATIC_DIR)), await inlineModuleSource(staticDir));
|
|
6073
6268
|
// And the entry a binary is compiled from, so the pages above end up
|