@rsc-kit/core 0.20.12 → 0.21.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/apiPrerender.js +1 -1
- package/dist/apiPrerender.js.map +1 -1
- package/dist/appAssets.d.ts +6 -0
- package/dist/appAssets.js +34 -0
- package/dist/appAssets.js.map +1 -1
- package/dist/appleSplash.d.ts +10 -0
- package/dist/appleSplash.js +74 -0
- package/dist/appleSplash.js.map +1 -0
- package/dist/earlyHints.js +7 -2
- package/dist/earlyHints.js.map +1 -1
- package/dist/export.js +3 -1
- package/dist/export.js.map +1 -1
- package/dist/files.d.ts +0 -8
- package/dist/files.js +7 -18
- package/dist/files.js.map +1 -1
- package/dist/headers.d.ts +8 -0
- package/dist/headers.js +8 -0
- package/dist/headers.js.map +1 -1
- package/dist/host.d.ts +15 -0
- package/dist/host.js +133 -27
- package/dist/host.js.map +1 -1
- package/dist/js/Form.d.ts +47 -54
- package/dist/js/Form.js +41 -48
- package/dist/js/Form.js.map +1 -1
- package/dist/js/chunkPreload.d.ts +26 -0
- package/dist/js/chunkPreload.js +66 -0
- package/dist/js/chunkPreload.js.map +1 -0
- package/dist/js/installCapture.d.ts +15 -0
- package/dist/js/installCapture.js +17 -0
- package/dist/js/installCapture.js.map +1 -0
- package/dist/js/navigate.js +4 -0
- package/dist/js/navigate.js.map +1 -1
- package/dist/js/useInstall.d.ts +15 -0
- package/dist/js/useInstall.js +102 -0
- package/dist/js/useInstall.js.map +1 -0
- package/dist/manifest.d.ts +2 -0
- package/dist/manifest.js.map +1 -1
- package/dist/metadata.d.ts +31 -0
- package/dist/metadata.js.map +1 -1
- package/dist/prerender.d.ts +24 -5
- package/dist/prerender.js +107 -19
- package/dist/prerender.js.map +1 -1
- package/dist/request.js +3 -2
- package/dist/request.js.map +1 -1
- package/dist/shellHead.js +4 -2
- package/dist/shellHead.js.map +1 -1
- package/dist/stableNames.d.ts +52 -0
- package/dist/stableNames.js +91 -0
- package/dist/stableNames.js.map +1 -0
- package/dist/vite.d.ts +5 -3
- package/dist/vite.js +215 -13
- package/dist/vite.js.map +1 -1
- package/dist/webManifest.d.ts +37 -0
- package/dist/webManifest.js +51 -6
- package/dist/webManifest.js.map +1 -1
- package/package.json +8 -4
package/dist/vite.js
CHANGED
|
@@ -20,7 +20,8 @@ import { spawnSync } from "node:child_process";
|
|
|
20
20
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
21
21
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
22
22
|
import rsc, { getPluginApi } from "@vitejs/plugin-rsc";
|
|
23
|
-
import { loadEnv } from "vite";
|
|
23
|
+
import { loadEnv, parseAst } from "vite";
|
|
24
|
+
import { stampDisplayNames } from "./stableNames.js";
|
|
24
25
|
import { REPORT_FILE, buildReport } from "./buildReport.js";
|
|
25
26
|
import { MANIFEST_PATH, manifestWarning, webManifest } from "./webManifest.js";
|
|
26
27
|
import { ASSET_BASE, allAppAssets, appAssets, headTags, typeOf, } from "./appAssets.js";
|
|
@@ -192,6 +193,8 @@ let foundAssets = {
|
|
|
192
193
|
favicon: null,
|
|
193
194
|
icons: [],
|
|
194
195
|
appleIcon: null,
|
|
196
|
+
appleSplash: [],
|
|
197
|
+
screenshots: [],
|
|
195
198
|
openGraph: null,
|
|
196
199
|
twitter: null,
|
|
197
200
|
};
|
|
@@ -390,6 +393,16 @@ function resolvePaths(options) {
|
|
|
390
393
|
icons: foundAssets.icons.map((icon) => icon.href),
|
|
391
394
|
};
|
|
392
395
|
}
|
|
396
|
+
// Screenshots the same way: found by name, their size and form factor read
|
|
397
|
+
// from it when the manifest is written.
|
|
398
|
+
if (webManifestOptions &&
|
|
399
|
+
!webManifestOptions.screenshots?.length &&
|
|
400
|
+
foundAssets.screenshots.length) {
|
|
401
|
+
webManifestOptions = {
|
|
402
|
+
...webManifestOptions,
|
|
403
|
+
screenshots: foundAssets.screenshots.map((shot) => shot.href),
|
|
404
|
+
};
|
|
405
|
+
}
|
|
393
406
|
hostActions = options.hostActions ?? fileHostActions(projectRoot);
|
|
394
407
|
}
|
|
395
408
|
function log(...args) {
|
|
@@ -620,6 +633,10 @@ function routeManifest() {
|
|
|
620
633
|
// A synthesised robots.txt or sitemap.xml runs no guard: it exists to be
|
|
621
634
|
// read by anyone, and a guard on the root would 401 the crawler.
|
|
622
635
|
middleware: generated ? [] : ancestors(name, "middleware"),
|
|
636
|
+
// And the host's, for the same reason: `export const middleware =
|
|
637
|
+
// ['auth']` on a directory guards the route.ts files in it, or adding
|
|
638
|
+
// an endpoint under a guarded path opened a hole in it.
|
|
639
|
+
hostMiddleware: generated ? [] : hostMiddleware(dirOf(name)),
|
|
623
640
|
})),
|
|
624
641
|
};
|
|
625
642
|
}
|
|
@@ -1222,6 +1239,15 @@ function copyAppAssets(clientDir) {
|
|
|
1222
1239
|
copyFileSync(join(sourceDir, "app", asset.file), to);
|
|
1223
1240
|
}
|
|
1224
1241
|
log(`icons: ${all.length} copied from app/`);
|
|
1242
|
+
// A launch screen whose size no device has would be a media query that
|
|
1243
|
+
// matches nothing - served, linked from no page, shown to no one. Named
|
|
1244
|
+
// here, with the sizes that do exist a click away in the guide.
|
|
1245
|
+
const unmatched = foundAssets.appleSplash.filter((splash) => splash.media === null).map((splash) => splash.file);
|
|
1246
|
+
if (unmatched.length) {
|
|
1247
|
+
log(`apple-splash: ${unmatched.join(", ")} ${unmatched.length === 1 ? "matches" : "match"} no iPhone or iPad screen, ` +
|
|
1248
|
+
"so no page links to it. Name each file by the device's size in pixels, portrait or landscape - " +
|
|
1249
|
+
"apple-splash-1179x2556.png is an iPhone 15. The sizes are in the PWA guide.");
|
|
1250
|
+
}
|
|
1225
1251
|
}
|
|
1226
1252
|
function writeWebManifest(clientDir, options) {
|
|
1227
1253
|
if (!existsSync(clientDir))
|
|
@@ -1230,6 +1256,9 @@ function writeWebManifest(clientDir, options) {
|
|
|
1230
1256
|
const warning = manifestWarning(options);
|
|
1231
1257
|
if (warning)
|
|
1232
1258
|
log(warning);
|
|
1259
|
+
else if (!options.screenshots?.length)
|
|
1260
|
+
log(`manifest: ${options.name} is installable. Chrome shows its basic install sheet; ` +
|
|
1261
|
+
"a screenshot-wide-*.png and screenshot-narrow-*.png in app/ get the richer one.");
|
|
1233
1262
|
else
|
|
1234
1263
|
log(`manifest: ${options.name} is installable`);
|
|
1235
1264
|
}
|
|
@@ -2265,6 +2294,18 @@ const FALLBACK_BODY = ` const answer = await devHandler(request)
|
|
|
2265
2294
|
headers.set('x-forwarded-host', here.host)
|
|
2266
2295
|
headers.set('x-forwarded-proto', here.protocol.replace(':', ''))
|
|
2267
2296
|
|
|
2297
|
+
// And never the visitor's own. The backend trusts this proxy, so an
|
|
2298
|
+
// X-Forwarded-For the browser wrote would be read as the client address -
|
|
2299
|
+
// a visitor choosing their own IP for a throttle on /login. Nothing here
|
|
2300
|
+
// knows the peer's address to put in its place, so the chain is dropped
|
|
2301
|
+
// unless this server sits behind a proxy of its own that wrote it, which
|
|
2302
|
+
// RSC_TRUST_FORWARDED=1 says.
|
|
2303
|
+
if (!(typeof process !== 'undefined' && process.env.RSC_TRUST_FORWARDED === '1')) {
|
|
2304
|
+
for (const name of ['x-forwarded-for', 'forwarded', 'x-real-ip', 'x-client-ip', 'x-forwarded-port', 'x-forwarded-prefix']) {
|
|
2305
|
+
headers.delete(name)
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2268
2309
|
const hasBody = request.method !== 'GET' && request.method !== 'HEAD'
|
|
2269
2310
|
|
|
2270
2311
|
try {
|
|
@@ -2333,10 +2374,7 @@ const NITRO_PRERENDERED = ` prerendered: import.meta.env.PROD
|
|
|
2333
2374
|
? prerenderedBeside(import.meta.url, ${JSON.stringify(NITRO_STATIC_DIR)})
|
|
2334
2375
|
: undefined,
|
|
2335
2376
|
`;
|
|
2336
|
-
const NITRO_HANDLER_OPTIONS = ` props:
|
|
2337
|
-
...match.params,
|
|
2338
|
-
...Object.fromEntries(new URL(request.url).searchParams),
|
|
2339
|
-
}),
|
|
2377
|
+
const NITRO_HANDLER_OPTIONS = ` props: queryAndParams,
|
|
2340
2378
|
// A built server gzips what it answers, where the runtime can; the dev
|
|
2341
2379
|
// server answers raw, which is what a person reading a response wants.
|
|
2342
2380
|
compress: import.meta.env.PROD,
|
|
@@ -2475,7 +2513,7 @@ import { parseParams, parseSearchParams, parseBody, isSearchParamsError, isBodyE
|
|
|
2475
2513
|
import { notFoundDigest, isNotFoundSignal } from ${JSON.stringify(join(packageDir, "notFound"))}
|
|
2476
2514
|
import { noteRequestRead, urlOf } from ${JSON.stringify(join(packageDir, "request"))}
|
|
2477
2515
|
import { redirectDigest } from ${JSON.stringify(join(packageDir, "redirectDigest"))}
|
|
2478
|
-
import { createRscHandler } from ${JSON.stringify(join(packageDir, "host"))}
|
|
2516
|
+
import { createRscHandler, queryAndParams } from ${JSON.stringify(join(packageDir, "host"))}
|
|
2479
2517
|
import { httpHostCalls } from ${JSON.stringify(join(packageDir, "hostCalls"))}
|
|
2480
2518
|
import { prerenderedBeside } from ${JSON.stringify(join(packageDir, "files"))}
|
|
2481
2519
|
import { renderToReadableStream, decodeReply, decodeAction, decodeFormState, loadServerAction } from '@vitejs/plugin-rsc/rsc'
|
|
@@ -3512,11 +3550,47 @@ async function renderTree(
|
|
|
3512
3550
|
}
|
|
3513
3551
|
}
|
|
3514
3552
|
|
|
3553
|
+
// How the app behaves added to an iPhone's home screen - Next's shape.
|
|
3554
|
+
// Both capable names: Safari reads the apple- one, Chrome warns about it
|
|
3555
|
+
// and reads the plain one, and Next switched between them in a minor.
|
|
3556
|
+
if (md.appleWebApp != null && md.appleWebApp !== false) {
|
|
3557
|
+
const web = (md.appleWebApp === true ? { capable: true } : md.appleWebApp) as Record<string, unknown>
|
|
3558
|
+
|
|
3559
|
+
if (web.capable) {
|
|
3560
|
+
head.push(tag('mobile-web-app-capable', 'yes'))
|
|
3561
|
+
head.push(tag('apple-mobile-web-app-capable', 'yes'))
|
|
3562
|
+
}
|
|
3563
|
+
|
|
3564
|
+
if (web.title != null) head.push(tag('apple-mobile-web-app-title', web.title))
|
|
3565
|
+
if (web.statusBarStyle != null) head.push(tag('apple-mobile-web-app-status-bar-style', web.statusBarStyle))
|
|
3566
|
+
|
|
3567
|
+
if (web.startupImage != null) {
|
|
3568
|
+
const startup = Array.isArray(web.startupImage) ? web.startupImage : [web.startupImage]
|
|
3569
|
+
|
|
3570
|
+
for (const item of startup) {
|
|
3571
|
+
if (item == null) continue
|
|
3572
|
+
|
|
3573
|
+
const image = typeof item === 'object' && !(item instanceof URL)
|
|
3574
|
+
? (item as Record<string, unknown>)
|
|
3575
|
+
: { url: item }
|
|
3576
|
+
|
|
3577
|
+
head.push(
|
|
3578
|
+
createElement('link', {
|
|
3579
|
+
key: '__s_' + String(image.url).slice(0, 60) + '_' + String(image.media ?? ''),
|
|
3580
|
+
rel: 'apple-touch-startup-image',
|
|
3581
|
+
href: absolute(image.url),
|
|
3582
|
+
...(image.media ? { media: image.media } : {}),
|
|
3583
|
+
}),
|
|
3584
|
+
)
|
|
3585
|
+
}
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
3588
|
+
|
|
3515
3589
|
// other is flattened in beside the named keys, because it is a place to put
|
|
3516
3590
|
// meta tags rather than a meta tag by that name. A key at the top level
|
|
3517
3591
|
// still renders - the type no longer invites one, but an app written
|
|
3518
3592
|
// against the old shape must not silently lose its tags.
|
|
3519
|
-
const structured = new Set(['title', 'description', 'robots', 'metadataBase', 'openGraph', 'twitter', 'icons', 'other'])
|
|
3593
|
+
const structured = new Set(['title', 'description', 'robots', 'metadataBase', 'openGraph', 'twitter', 'icons', 'appleWebApp', 'other'])
|
|
3520
3594
|
const named = Object.entries(md).filter(([k]) => !structured.has(k))
|
|
3521
3595
|
const extra = Object.entries((md.other ?? {}) as Record<string, unknown>)
|
|
3522
3596
|
|
|
@@ -3606,6 +3680,11 @@ async function runHostMiddleware(component) {
|
|
|
3606
3680
|
for (const route of manifest().routes) {
|
|
3607
3681
|
if (route.hostMiddleware?.length) hostChains[route.component] = route.hostMiddleware
|
|
3608
3682
|
}
|
|
3683
|
+
|
|
3684
|
+
// Api routes, keyed by module name - the key runRouteMiddleware is asked with.
|
|
3685
|
+
for (const api of manifest().apis ?? []) {
|
|
3686
|
+
if (api.hostMiddleware?.length) hostChains[api.name] = api.hostMiddleware
|
|
3687
|
+
}
|
|
3609
3688
|
}
|
|
3610
3689
|
|
|
3611
3690
|
const names = hostChains[component] ?? []
|
|
@@ -3903,9 +3982,20 @@ export async function handleRscResume(
|
|
|
3903
3982
|
)
|
|
3904
3983
|
|
|
3905
3984
|
const ssr = await (import.meta as any).viteRsc.loadModule('ssr', 'index')
|
|
3906
|
-
|
|
3985
|
+
// Whether React could replay the recorded tree at all. A bundler that
|
|
3986
|
+
// merges scopes renames components, and a name is how React matches a
|
|
3987
|
+
// slot: the same page, built once and bundled again - which is what
|
|
3988
|
+
// bun build --compile does - has different names at runtime than the
|
|
3989
|
+
// shell recorded. Reported so the host can stop resuming this shell and
|
|
3990
|
+
// render the page whole, rather than serving one whose holes are all
|
|
3991
|
+
// client-rendered. Set before the stream ends, because that is when
|
|
3992
|
+
// React reports it.
|
|
3993
|
+
let replayFailed = false
|
|
3994
|
+
const htmlStream = await ssr.handleSsrResume(flight, postponed, nonce, () => {
|
|
3995
|
+
replayFailed = true
|
|
3996
|
+
})
|
|
3907
3997
|
|
|
3908
|
-
return { htmlStream }
|
|
3998
|
+
return { htmlStream, replayed: () => !replayFailed }
|
|
3909
3999
|
}
|
|
3910
4000
|
|
|
3911
4001
|
// Server action (worker: rsc-action).
|
|
@@ -4460,6 +4550,8 @@ export async function handleRscPayload(
|
|
|
4460
4550
|
// data, so only the shell is safe to cache
|
|
4461
4551
|
// A page that sets neither is genuinely static and can be prerendered fully.
|
|
4462
4552
|
const PPR_SHELL_TIMEOUT_MS = Number(process.env.RSC_PPR_TIMEOUT_MS || 2000)
|
|
4553
|
+
// How long a render goes without a Flight row before it counts as waiting.
|
|
4554
|
+
const PPR_QUIET_MS = 50
|
|
4463
4555
|
|
|
4464
4556
|
export async function handleRscPprShell(
|
|
4465
4557
|
component: string,
|
|
@@ -4496,6 +4588,11 @@ export async function handleRscPprShell(
|
|
|
4496
4588
|
// installed, because those are different statements: a test that installed
|
|
4497
4589
|
// one is not a build that can reach the host.
|
|
4498
4590
|
canReachHost = false,
|
|
4591
|
+
// Told once, when the render has stopped producing rows and is only
|
|
4592
|
+
// waiting. The budget still runs - a slow query can still land and be
|
|
4593
|
+
// stored - but the caller no longer has to hold a slot for a render that
|
|
4594
|
+
// is doing nothing but watching a clock.
|
|
4595
|
+
onQuiet?: () => void,
|
|
4499
4596
|
): Promise<{ shellHtml: string; clientChunks: unknown; timedOut: boolean; usedDynamicApis: boolean; error?: string }> {
|
|
4500
4597
|
// Deliberately no middleware here. The probe is asking whether the content is
|
|
4501
4598
|
// the same for everyone, which is a question about the page. Whether a
|
|
@@ -4524,6 +4621,35 @@ export async function handleRscPprShell(
|
|
|
4524
4621
|
const controller = new AbortController()
|
|
4525
4622
|
const budget = setTimeout(() => controller.abort(), budgetMs)
|
|
4526
4623
|
|
|
4624
|
+
// Quiet is a stretch with no Flight row: what is left is awaiting something,
|
|
4625
|
+
// and whether that something ever answers is what the budget decides. A
|
|
4626
|
+
// guess in one direction only - a render mistaken for quiet just overlaps
|
|
4627
|
+
// with the next, and is still given its whole budget.
|
|
4628
|
+
let quietTimer: ReturnType<typeof setTimeout> | undefined
|
|
4629
|
+
const stir = () => {
|
|
4630
|
+
if (!onQuiet) return
|
|
4631
|
+
clearTimeout(quietTimer)
|
|
4632
|
+
quietTimer = setTimeout(() => {
|
|
4633
|
+
const told = onQuiet
|
|
4634
|
+
onQuiet = undefined
|
|
4635
|
+
told?.()
|
|
4636
|
+
}, PPR_QUIET_MS)
|
|
4637
|
+
}
|
|
4638
|
+
const watchForQuiet = (stream: ReadableStream<Uint8Array>): ReadableStream<Uint8Array> => {
|
|
4639
|
+
if (!onQuiet) return stream
|
|
4640
|
+
|
|
4641
|
+
stir()
|
|
4642
|
+
|
|
4643
|
+
return stream.pipeThrough(
|
|
4644
|
+
new TransformStream<Uint8Array, Uint8Array>({
|
|
4645
|
+
transform(chunk, out) {
|
|
4646
|
+
stir()
|
|
4647
|
+
out.enqueue(chunk)
|
|
4648
|
+
},
|
|
4649
|
+
}),
|
|
4650
|
+
)
|
|
4651
|
+
}
|
|
4652
|
+
|
|
4527
4653
|
// Anything that failed while producing this shell.
|
|
4528
4654
|
//
|
|
4529
4655
|
// A rejection inside a Suspense boundary does NOT reach the caller: React
|
|
@@ -4605,7 +4731,7 @@ export async function handleRscPprShell(
|
|
|
4605
4731
|
)
|
|
4606
4732
|
// Quiet about a redirect: during the probe it is a classification, not
|
|
4607
4733
|
// a failure, and React would otherwise print a stack for every one.
|
|
4608
|
-
const flight = renderToReadableStream(tree, { onError: noteFailure })
|
|
4734
|
+
const flight = watchForQuiet(renderToReadableStream(tree, { onError: noteFailure }))
|
|
4609
4735
|
const ssr = await (import.meta as any).viteRsc.loadModule('ssr', 'index')
|
|
4610
4736
|
// The flight stream stays open across this. A Flight stream that has
|
|
4611
4737
|
// closed tells the decoder the connection ended, so the boundary waiting
|
|
@@ -4636,6 +4762,7 @@ export async function handleRscPprShell(
|
|
|
4636
4762
|
|
|
4637
4763
|
await produce
|
|
4638
4764
|
clearTimeout(budget)
|
|
4765
|
+
clearTimeout(quietTimer)
|
|
4639
4766
|
|
|
4640
4767
|
// timedOut used to mean the stopwatch ran out. It now means React has
|
|
4641
4768
|
// boundaries it could not finish — the thing the caller was always asking
|
|
@@ -4804,6 +4931,7 @@ function generateEntrySsr() {
|
|
|
4804
4931
|
const redirectDigestModule = join(packageDir, "redirectDigest");
|
|
4805
4932
|
const notFoundModule = join(packageDir, "notFound");
|
|
4806
4933
|
const earlyClicks = join(packageDir, "js/earlyClicks");
|
|
4934
|
+
const installCapture = join(packageDir, "js/installCapture");
|
|
4807
4935
|
return `// GENERATED by rscKit() — do not edit.
|
|
4808
4936
|
import { createFromReadableStream } from '@vitejs/plugin-rsc/ssr'
|
|
4809
4937
|
import { renderToReadableStream, resume } from 'react-dom/server.edge'
|
|
@@ -4814,6 +4942,11 @@ import { cancelledByConsumer, caughtByLoading } from ${JSON.stringify(fallbackRe
|
|
|
4814
4942
|
import { parseRedirectDigest } from ${JSON.stringify(redirectDigestModule)}
|
|
4815
4943
|
import { isNotFoundDigest } from ${JSON.stringify(notFoundModule)}
|
|
4816
4944
|
import { EARLY_CLICKS } from ${JSON.stringify(earlyClicks)}
|
|
4945
|
+
${webManifestOptions ? `import { INSTALL_CAPTURE } from ${JSON.stringify(installCapture)}` : ""}
|
|
4946
|
+
|
|
4947
|
+
// What runs ahead of the runtime: held taps, and - in an app that can be
|
|
4948
|
+
// installed at all - the browser's offer to install it. See installCapture.ts.
|
|
4949
|
+
const EARLY = EARLY_CLICKS${webManifestOptions ? " + INSTALL_CAPTURE" : ""}
|
|
4817
4950
|
|
|
4818
4951
|
// Set only by the dev server. @vitejs/plugin-rsc emits its bootstrap and CSS
|
|
4819
4952
|
// links root-relative in dev, which would send the browser to the host for
|
|
@@ -4836,7 +4969,7 @@ export async function handleSsr(
|
|
|
4836
4969
|
// seconds before the runtime hydrates is held for it rather than given
|
|
4837
4970
|
// to the browser as a document load. See earlyClicks.ts.
|
|
4838
4971
|
const bootstrapScriptContent = bootstrap
|
|
4839
|
-
?
|
|
4972
|
+
? EARLY + (await (import.meta as any).viteRsc.loadBootstrapScriptContent('index'))
|
|
4840
4973
|
: undefined
|
|
4841
4974
|
|
|
4842
4975
|
// Without an onError handler React rejects each abortable task on its own,
|
|
@@ -4878,7 +5011,7 @@ export async function handleSsrPrerender(
|
|
|
4878
5011
|
const bootstrapScriptContent =
|
|
4879
5012
|
options.bootstrap === false
|
|
4880
5013
|
? undefined
|
|
4881
|
-
:
|
|
5014
|
+
: EARLY + (await (import.meta as any).viteRsc.loadBootstrapScriptContent('index'))
|
|
4882
5015
|
|
|
4883
5016
|
const { prelude, postponed } = await prerender(root as any, {
|
|
4884
5017
|
bootstrapScriptContent,
|
|
@@ -4977,6 +5110,11 @@ export async function handleSsrResume(
|
|
|
4977
5110
|
rscStream: ReadableStream,
|
|
4978
5111
|
postponed: unknown,
|
|
4979
5112
|
nonce?: string,
|
|
5113
|
+
// Told when React refuses to replay: the tree it recorded and the tree
|
|
5114
|
+
// rendering now put different components in a slot, so every hole below
|
|
5115
|
+
// is client-rendered instead of filled here. The caller decides what to
|
|
5116
|
+
// do about it; see the host.
|
|
5117
|
+
onReplayMismatch?: () => void,
|
|
4980
5118
|
): Promise<ReadableStream> {
|
|
4981
5119
|
const root = await createFromReadableStream(rscStream)
|
|
4982
5120
|
|
|
@@ -4987,7 +5125,13 @@ export async function handleSsrResume(
|
|
|
4987
5125
|
// the designed path, on every resume of a shell whose hole holds one -
|
|
4988
5126
|
// printed as "[rsc-kit:resume] Error: useSearchParams() was read..." on
|
|
4989
5127
|
// every request, with advice the app had already followed.
|
|
4990
|
-
onError:
|
|
5128
|
+
onError: (error: unknown, info: unknown) => {
|
|
5129
|
+
const message = String((error as { message?: string } | null)?.message ?? error)
|
|
5130
|
+
|
|
5131
|
+
if (message.includes('instead it rendered') || message.includes('resumable slots')) onReplayMismatch?.()
|
|
5132
|
+
|
|
5133
|
+
return reportRenderError('resume')(error, info)
|
|
5134
|
+
},
|
|
4991
5135
|
})
|
|
4992
5136
|
|
|
4993
5137
|
return DEV_ORIGIN ? rewriteViteDevUrlStream(html, DEV_ORIGIN) : html
|
|
@@ -5030,9 +5174,21 @@ function generateEntryBrowser() {
|
|
|
5030
5174
|
}))
|
|
5031
5175
|
: null;
|
|
5032
5176
|
const refreshModule = join(packageDir, "js/navigate");
|
|
5177
|
+
const chunkPreload = join(packageDir, "js/chunkPreload");
|
|
5033
5178
|
return `// GENERATED by rscKit() — do not edit.
|
|
5034
5179
|
import { createViteRscApp } from ${JSON.stringify(clientBootstrap)}
|
|
5035
5180
|
import { refresh } from ${JSON.stringify(refreshModule)}
|
|
5181
|
+
import { setChunkLoader } from ${JSON.stringify(chunkPreload)}
|
|
5182
|
+
import * as clientReferences from 'virtual:vite-rsc/client-references'
|
|
5183
|
+
|
|
5184
|
+
// How a client component's id becomes its chunk, for loading the ones a
|
|
5185
|
+
// prefetched page uses before the tap - see chunkPreload.ts. The same map the
|
|
5186
|
+
// runtime loads them through, so a chunk loaded here is the one it finds.
|
|
5187
|
+
// A production build only: in development the runtime imports by path.
|
|
5188
|
+
if (import.meta.env.PROD) {
|
|
5189
|
+
const references = (clientReferences as { default: Record<string, () => Promise<unknown>> }).default
|
|
5190
|
+
setChunkLoader((id) => references[id]?.())
|
|
5191
|
+
}
|
|
5036
5192
|
|
|
5037
5193
|
createViteRscApp(document, ${JSON.stringify(interceptManifest())}, ${JSON.stringify({
|
|
5038
5194
|
staticPayloads: staticPayloads || null,
|
|
@@ -5246,6 +5402,25 @@ const JSX_PLUGIN_PATTERN = /react|babel|oxc/i;
|
|
|
5246
5402
|
* class is ever constructed on the server.
|
|
5247
5403
|
*/
|
|
5248
5404
|
const CLIENT_STUB = /registerClientReference\(\s*\(\) => \{ throw new Error\("Unexpectedly client reference export '"/g;
|
|
5405
|
+
/**
|
|
5406
|
+
* The server's render of client components, with names a second bundler
|
|
5407
|
+
* cannot change. See stableNames.ts: a replay matches slots by component
|
|
5408
|
+
* name, and `bun build --compile` renames whatever collides when it merges
|
|
5409
|
+
* module scopes. Stamped on the ssr output as written, so the names are
|
|
5410
|
+
* the ones the prerender recorded - it renders from this same output - and
|
|
5411
|
+
* the ones anything serving it reads, compiled or not.
|
|
5412
|
+
*/
|
|
5413
|
+
function stableComponentNames() {
|
|
5414
|
+
return {
|
|
5415
|
+
name: "rsc-kit:stable-component-names",
|
|
5416
|
+
apply: "build",
|
|
5417
|
+
applyToEnvironment: (environment) => environment.name === "ssr",
|
|
5418
|
+
renderChunk(code) {
|
|
5419
|
+
const stamped = stampDisplayNames(code, (source) => parseAst(source));
|
|
5420
|
+
return stamped === code ? null : { code: stamped, map: null };
|
|
5421
|
+
},
|
|
5422
|
+
};
|
|
5423
|
+
}
|
|
5249
5424
|
function extendableClientReferences() {
|
|
5250
5425
|
return {
|
|
5251
5426
|
name: "rsc-kit:extendable-client-references",
|
|
@@ -6031,6 +6206,32 @@ export function rscKit(options = {}) {
|
|
|
6031
6206
|
external: serverExternals,
|
|
6032
6207
|
// Same table, same reason.
|
|
6033
6208
|
checks: { ineffectiveDynamicImport: false },
|
|
6209
|
+
output: {
|
|
6210
|
+
// One copy of a dependency, however many client components
|
|
6211
|
+
// reach for it.
|
|
6212
|
+
//
|
|
6213
|
+
// Every client component is its own entry here - the SSR
|
|
6214
|
+
// side of a client reference, imported by id - and a
|
|
6215
|
+
// dependency two of them share is copied into each one's
|
|
6216
|
+
// chunk group. Two copies of one component are two
|
|
6217
|
+
// different functions, and that is fine until something
|
|
6218
|
+
// merges the module scopes: `bun build --compile` does,
|
|
6219
|
+
// renames the second copy, and a partially prerendered
|
|
6220
|
+
// page records its tree BY COMPONENT NAME. The shell then
|
|
6221
|
+
// says <J>, the binary renders <J2>, React refuses the
|
|
6222
|
+
// replay and every hole falls to the browser - a page
|
|
6223
|
+
// that arrives looking finished with nothing wired up. A
|
|
6224
|
+
// port found it on one page, in production, on cold
|
|
6225
|
+
// loads only.
|
|
6226
|
+
//
|
|
6227
|
+
// Grouping node_modules into one chunk means a shared
|
|
6228
|
+
// dependency exists once, so there is nothing to collide.
|
|
6229
|
+
// The server does not pay for the bigger chunk the way a
|
|
6230
|
+
// browser would: it reads from disk, once, at startup.
|
|
6231
|
+
advancedChunks: {
|
|
6232
|
+
groups: [{ name: "vendor", test: /[\\/]node_modules[\\/]/ }],
|
|
6233
|
+
},
|
|
6234
|
+
},
|
|
6034
6235
|
},
|
|
6035
6236
|
},
|
|
6036
6237
|
resolve: { noExternal: bundledClientPackages },
|
|
@@ -6369,6 +6570,7 @@ export function rscKit(options = {}) {
|
|
|
6369
6570
|
metadataRoutesPlugin(),
|
|
6370
6571
|
openApiPlugin(),
|
|
6371
6572
|
extendableClientReferences(),
|
|
6573
|
+
stableComponentNames(),
|
|
6372
6574
|
typecheckPlugin(),
|
|
6373
6575
|
clientImportsAudit(),
|
|
6374
6576
|
routesPlugin,
|