@rsc-kit/core 0.20.13 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/apiPrerender.js +1 -1
  2. package/dist/apiPrerender.js.map +1 -1
  3. package/dist/appAssets.d.ts +6 -0
  4. package/dist/appAssets.js +34 -0
  5. package/dist/appAssets.js.map +1 -1
  6. package/dist/appleSplash.d.ts +10 -0
  7. package/dist/appleSplash.js +74 -0
  8. package/dist/appleSplash.js.map +1 -0
  9. package/dist/earlyHints.js +7 -2
  10. package/dist/earlyHints.js.map +1 -1
  11. package/dist/export.js +3 -1
  12. package/dist/export.js.map +1 -1
  13. package/dist/files.d.ts +0 -8
  14. package/dist/files.js +6 -0
  15. package/dist/files.js.map +1 -1
  16. package/dist/headers.d.ts +8 -0
  17. package/dist/headers.js +8 -0
  18. package/dist/headers.js.map +1 -1
  19. package/dist/host.d.ts +9 -0
  20. package/dist/host.js +109 -29
  21. package/dist/host.js.map +1 -1
  22. package/dist/js/Form.d.ts +47 -54
  23. package/dist/js/Form.js +41 -48
  24. package/dist/js/Form.js.map +1 -1
  25. package/dist/js/chunkPreload.d.ts +26 -0
  26. package/dist/js/chunkPreload.js +66 -0
  27. package/dist/js/chunkPreload.js.map +1 -0
  28. package/dist/js/installCapture.d.ts +15 -0
  29. package/dist/js/installCapture.js +17 -0
  30. package/dist/js/installCapture.js.map +1 -0
  31. package/dist/js/navigate.js +4 -0
  32. package/dist/js/navigate.js.map +1 -1
  33. package/dist/js/useInstall.d.ts +15 -0
  34. package/dist/js/useInstall.js +102 -0
  35. package/dist/js/useInstall.js.map +1 -0
  36. package/dist/manifest.d.ts +2 -0
  37. package/dist/manifest.js.map +1 -1
  38. package/dist/metadata.d.ts +31 -0
  39. package/dist/metadata.js.map +1 -1
  40. package/dist/prerender.d.ts +43 -5
  41. package/dist/prerender.js +138 -19
  42. package/dist/prerender.js.map +1 -1
  43. package/dist/request.js +3 -2
  44. package/dist/request.js.map +1 -1
  45. package/dist/shellHead.js +4 -2
  46. package/dist/shellHead.js.map +1 -1
  47. package/dist/stableNames.d.ts +52 -0
  48. package/dist/stableNames.js +91 -0
  49. package/dist/stableNames.js.map +1 -0
  50. package/dist/vite.d.ts +5 -3
  51. package/dist/vite.js +200 -13
  52. package/dist/vite.js.map +1 -1
  53. package/dist/webManifest.d.ts +37 -0
  54. package/dist/webManifest.js +51 -6
  55. package/dist/webManifest.js.map +1 -1
  56. 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
  }
@@ -824,6 +841,14 @@ self.importScripts(${JSON.stringify(swExtra)})
824
841
  `
825
842
  : ""}
826
843
 
844
+ // How long one install-time fetch may take. An install is pending until every
845
+ // fetch in it settles, and a request that never answers kept the previous
846
+ // worker in charge for as long as it hung - so one that is late is treated
847
+ // like one that failed: skipped, and fetched again the first time it is used.
848
+ const INSTALL_FETCH_MS = 30000
849
+ const within = (ms, promise) =>
850
+ Promise.race([promise, new Promise((_, reject) => setTimeout(() => reject(new Error('no answer in ' + ms + ' ms')), ms))])
851
+
827
852
  self.addEventListener('install', (event) => {
828
853
  // The new worker takes over rather than waiting for every tab to close.
829
854
  // Safe here because assets are content-hashed: a page already open keeps
@@ -836,7 +861,26 @@ self.addEventListener('install', (event) => {
836
861
  // it is served by the host out of the prerendered directory, which this
837
862
  // worker cannot see. Added here so it is in the cache before it is
838
863
  // needed, which is the only moment it cannot be fetched.
839
- .then((cache) => cache.addAll(OFFLINE_URL ? [...PRECACHE, OFFLINE_URL] : PRECACHE).then(() => cache))
864
+ //
865
+ // One at a time rather than cache.addAll, which rejects the whole list
866
+ // for one failure - and a worker whose install rejects is never
867
+ // installed. The worker before it then stays in charge, serving its own
868
+ // build's pages, for every visit until one install happens to succeed:
869
+ // a network blip, a quota, a file a deploy replaced mid-install, and a
870
+ // phone kept a worker from builds ago whose bugs had long been fixed.
871
+ // A file that did not arrive costs a cache entry; the update still
872
+ // lands. The browser checks /sw.js for itself, without the page's
873
+ // javascript, so an install that cannot fail is also what rescues a
874
+ // visitor stuck on a worker far older than this one.
875
+ .then((cache) =>
876
+ Promise.all(
877
+ (OFFLINE_URL ? [...PRECACHE, OFFLINE_URL] : PRECACHE).map((url) =>
878
+ within(INSTALL_FETCH_MS, cache.add(url)).catch((error) => {
879
+ console.warn('[rsc-kit] offline: ' + url + ' was not precached (' + (error && error.message || error) + '); it will be cached the first time it is fetched')
880
+ }),
881
+ ),
882
+ ).then(() => cache),
883
+ )
840
884
  // And the payload each precached page boots from. A document alone is
841
885
  // markup that never hydrates: the client fetches its payload on boot,
842
886
  // and a precached / rendered offline and stayed inert because that
@@ -848,7 +892,7 @@ self.addEventListener('install', (event) => {
848
892
  .map((url) => {
849
893
  const warm = new Request(url, { headers: { 'X-RSC': '1' } })
850
894
 
851
- return fetch(warm)
895
+ return within(INSTALL_FETCH_MS, fetch(warm))
852
896
  .then((payload) => {
853
897
  if (mayStore(payload)) return cache.put(keyFor(warm), payload)
854
898
 
@@ -1222,6 +1266,15 @@ function copyAppAssets(clientDir) {
1222
1266
  copyFileSync(join(sourceDir, "app", asset.file), to);
1223
1267
  }
1224
1268
  log(`icons: ${all.length} copied from app/`);
1269
+ // A launch screen whose size no device has would be a media query that
1270
+ // matches nothing - served, linked from no page, shown to no one. Named
1271
+ // here, with the sizes that do exist a click away in the guide.
1272
+ const unmatched = foundAssets.appleSplash.filter((splash) => splash.media === null).map((splash) => splash.file);
1273
+ if (unmatched.length) {
1274
+ log(`apple-splash: ${unmatched.join(", ")} ${unmatched.length === 1 ? "matches" : "match"} no iPhone or iPad screen, ` +
1275
+ "so no page links to it. Name each file by the device's size in pixels, portrait or landscape - " +
1276
+ "apple-splash-1179x2556.png is an iPhone 15. The sizes are in the PWA guide.");
1277
+ }
1225
1278
  }
1226
1279
  function writeWebManifest(clientDir, options) {
1227
1280
  if (!existsSync(clientDir))
@@ -1230,6 +1283,9 @@ function writeWebManifest(clientDir, options) {
1230
1283
  const warning = manifestWarning(options);
1231
1284
  if (warning)
1232
1285
  log(warning);
1286
+ else if (!options.screenshots?.length)
1287
+ log(`manifest: ${options.name} is installable. Chrome shows its basic install sheet; ` +
1288
+ "a screenshot-wide-*.png and screenshot-narrow-*.png in app/ get the richer one.");
1233
1289
  else
1234
1290
  log(`manifest: ${options.name} is installable`);
1235
1291
  }
@@ -2265,6 +2321,18 @@ const FALLBACK_BODY = ` const answer = await devHandler(request)
2265
2321
  headers.set('x-forwarded-host', here.host)
2266
2322
  headers.set('x-forwarded-proto', here.protocol.replace(':', ''))
2267
2323
 
2324
+ // And never the visitor's own. The backend trusts this proxy, so an
2325
+ // X-Forwarded-For the browser wrote would be read as the client address -
2326
+ // a visitor choosing their own IP for a throttle on /login. Nothing here
2327
+ // knows the peer's address to put in its place, so the chain is dropped
2328
+ // unless this server sits behind a proxy of its own that wrote it, which
2329
+ // RSC_TRUST_FORWARDED=1 says.
2330
+ if (!(typeof process !== 'undefined' && process.env.RSC_TRUST_FORWARDED === '1')) {
2331
+ for (const name of ['x-forwarded-for', 'forwarded', 'x-real-ip', 'x-client-ip', 'x-forwarded-port', 'x-forwarded-prefix']) {
2332
+ headers.delete(name)
2333
+ }
2334
+ }
2335
+
2268
2336
  const hasBody = request.method !== 'GET' && request.method !== 'HEAD'
2269
2337
 
2270
2338
  try {
@@ -2333,10 +2401,7 @@ const NITRO_PRERENDERED = ` prerendered: import.meta.env.PROD
2333
2401
  ? prerenderedBeside(import.meta.url, ${JSON.stringify(NITRO_STATIC_DIR)})
2334
2402
  : undefined,
2335
2403
  `;
2336
- const NITRO_HANDLER_OPTIONS = ` props: (match, request) => ({
2337
- ...match.params,
2338
- ...Object.fromEntries(new URL(request.url).searchParams),
2339
- }),
2404
+ const NITRO_HANDLER_OPTIONS = ` props: queryAndParams,
2340
2405
  // A built server gzips what it answers, where the runtime can; the dev
2341
2406
  // server answers raw, which is what a person reading a response wants.
2342
2407
  compress: import.meta.env.PROD,
@@ -2475,7 +2540,7 @@ import { parseParams, parseSearchParams, parseBody, isSearchParamsError, isBodyE
2475
2540
  import { notFoundDigest, isNotFoundSignal } from ${JSON.stringify(join(packageDir, "notFound"))}
2476
2541
  import { noteRequestRead, urlOf } from ${JSON.stringify(join(packageDir, "request"))}
2477
2542
  import { redirectDigest } from ${JSON.stringify(join(packageDir, "redirectDigest"))}
2478
- import { createRscHandler } from ${JSON.stringify(join(packageDir, "host"))}
2543
+ import { createRscHandler, queryAndParams } from ${JSON.stringify(join(packageDir, "host"))}
2479
2544
  import { httpHostCalls } from ${JSON.stringify(join(packageDir, "hostCalls"))}
2480
2545
  import { prerenderedBeside } from ${JSON.stringify(join(packageDir, "files"))}
2481
2546
  import { renderToReadableStream, decodeReply, decodeAction, decodeFormState, loadServerAction } from '@vitejs/plugin-rsc/rsc'
@@ -3512,11 +3577,47 @@ async function renderTree(
3512
3577
  }
3513
3578
  }
3514
3579
 
3580
+ // How the app behaves added to an iPhone's home screen - Next's shape.
3581
+ // Both capable names: Safari reads the apple- one, Chrome warns about it
3582
+ // and reads the plain one, and Next switched between them in a minor.
3583
+ if (md.appleWebApp != null && md.appleWebApp !== false) {
3584
+ const web = (md.appleWebApp === true ? { capable: true } : md.appleWebApp) as Record<string, unknown>
3585
+
3586
+ if (web.capable) {
3587
+ head.push(tag('mobile-web-app-capable', 'yes'))
3588
+ head.push(tag('apple-mobile-web-app-capable', 'yes'))
3589
+ }
3590
+
3591
+ if (web.title != null) head.push(tag('apple-mobile-web-app-title', web.title))
3592
+ if (web.statusBarStyle != null) head.push(tag('apple-mobile-web-app-status-bar-style', web.statusBarStyle))
3593
+
3594
+ if (web.startupImage != null) {
3595
+ const startup = Array.isArray(web.startupImage) ? web.startupImage : [web.startupImage]
3596
+
3597
+ for (const item of startup) {
3598
+ if (item == null) continue
3599
+
3600
+ const image = typeof item === 'object' && !(item instanceof URL)
3601
+ ? (item as Record<string, unknown>)
3602
+ : { url: item }
3603
+
3604
+ head.push(
3605
+ createElement('link', {
3606
+ key: '__s_' + String(image.url).slice(0, 60) + '_' + String(image.media ?? ''),
3607
+ rel: 'apple-touch-startup-image',
3608
+ href: absolute(image.url),
3609
+ ...(image.media ? { media: image.media } : {}),
3610
+ }),
3611
+ )
3612
+ }
3613
+ }
3614
+ }
3615
+
3515
3616
  // other is flattened in beside the named keys, because it is a place to put
3516
3617
  // meta tags rather than a meta tag by that name. A key at the top level
3517
3618
  // still renders - the type no longer invites one, but an app written
3518
3619
  // against the old shape must not silently lose its tags.
3519
- const structured = new Set(['title', 'description', 'robots', 'metadataBase', 'openGraph', 'twitter', 'icons', 'other'])
3620
+ const structured = new Set(['title', 'description', 'robots', 'metadataBase', 'openGraph', 'twitter', 'icons', 'appleWebApp', 'other'])
3520
3621
  const named = Object.entries(md).filter(([k]) => !structured.has(k))
3521
3622
  const extra = Object.entries((md.other ?? {}) as Record<string, unknown>)
3522
3623
 
@@ -3606,6 +3707,11 @@ async function runHostMiddleware(component) {
3606
3707
  for (const route of manifest().routes) {
3607
3708
  if (route.hostMiddleware?.length) hostChains[route.component] = route.hostMiddleware
3608
3709
  }
3710
+
3711
+ // Api routes, keyed by module name - the key runRouteMiddleware is asked with.
3712
+ for (const api of manifest().apis ?? []) {
3713
+ if (api.hostMiddleware?.length) hostChains[api.name] = api.hostMiddleware
3714
+ }
3609
3715
  }
3610
3716
 
3611
3717
  const names = hostChains[component] ?? []
@@ -4471,6 +4577,8 @@ export async function handleRscPayload(
4471
4577
  // data, so only the shell is safe to cache
4472
4578
  // A page that sets neither is genuinely static and can be prerendered fully.
4473
4579
  const PPR_SHELL_TIMEOUT_MS = Number(process.env.RSC_PPR_TIMEOUT_MS || 2000)
4580
+ // How long a render goes without a Flight row before it counts as waiting.
4581
+ const PPR_QUIET_MS = 50
4474
4582
 
4475
4583
  export async function handleRscPprShell(
4476
4584
  component: string,
@@ -4507,6 +4615,11 @@ export async function handleRscPprShell(
4507
4615
  // installed, because those are different statements: a test that installed
4508
4616
  // one is not a build that can reach the host.
4509
4617
  canReachHost = false,
4618
+ // Told once, when the render has stopped producing rows and is only
4619
+ // waiting. The budget still runs - a slow query can still land and be
4620
+ // stored - but the caller no longer has to hold a slot for a render that
4621
+ // is doing nothing but watching a clock.
4622
+ onQuiet?: () => void,
4510
4623
  ): Promise<{ shellHtml: string; clientChunks: unknown; timedOut: boolean; usedDynamicApis: boolean; error?: string }> {
4511
4624
  // Deliberately no middleware here. The probe is asking whether the content is
4512
4625
  // the same for everyone, which is a question about the page. Whether a
@@ -4535,6 +4648,35 @@ export async function handleRscPprShell(
4535
4648
  const controller = new AbortController()
4536
4649
  const budget = setTimeout(() => controller.abort(), budgetMs)
4537
4650
 
4651
+ // Quiet is a stretch with no Flight row: what is left is awaiting something,
4652
+ // and whether that something ever answers is what the budget decides. A
4653
+ // guess in one direction only - a render mistaken for quiet just overlaps
4654
+ // with the next, and is still given its whole budget.
4655
+ let quietTimer: ReturnType<typeof setTimeout> | undefined
4656
+ const stir = () => {
4657
+ if (!onQuiet) return
4658
+ clearTimeout(quietTimer)
4659
+ quietTimer = setTimeout(() => {
4660
+ const told = onQuiet
4661
+ onQuiet = undefined
4662
+ told?.()
4663
+ }, PPR_QUIET_MS)
4664
+ }
4665
+ const watchForQuiet = (stream: ReadableStream<Uint8Array>): ReadableStream<Uint8Array> => {
4666
+ if (!onQuiet) return stream
4667
+
4668
+ stir()
4669
+
4670
+ return stream.pipeThrough(
4671
+ new TransformStream<Uint8Array, Uint8Array>({
4672
+ transform(chunk, out) {
4673
+ stir()
4674
+ out.enqueue(chunk)
4675
+ },
4676
+ }),
4677
+ )
4678
+ }
4679
+
4538
4680
  // Anything that failed while producing this shell.
4539
4681
  //
4540
4682
  // A rejection inside a Suspense boundary does NOT reach the caller: React
@@ -4616,7 +4758,7 @@ export async function handleRscPprShell(
4616
4758
  )
4617
4759
  // Quiet about a redirect: during the probe it is a classification, not
4618
4760
  // a failure, and React would otherwise print a stack for every one.
4619
- const flight = renderToReadableStream(tree, { onError: noteFailure })
4761
+ const flight = watchForQuiet(renderToReadableStream(tree, { onError: noteFailure }))
4620
4762
  const ssr = await (import.meta as any).viteRsc.loadModule('ssr', 'index')
4621
4763
  // The flight stream stays open across this. A Flight stream that has
4622
4764
  // closed tells the decoder the connection ended, so the boundary waiting
@@ -4647,6 +4789,7 @@ export async function handleRscPprShell(
4647
4789
 
4648
4790
  await produce
4649
4791
  clearTimeout(budget)
4792
+ clearTimeout(quietTimer)
4650
4793
 
4651
4794
  // timedOut used to mean the stopwatch ran out. It now means React has
4652
4795
  // boundaries it could not finish — the thing the caller was always asking
@@ -4815,6 +4958,7 @@ function generateEntrySsr() {
4815
4958
  const redirectDigestModule = join(packageDir, "redirectDigest");
4816
4959
  const notFoundModule = join(packageDir, "notFound");
4817
4960
  const earlyClicks = join(packageDir, "js/earlyClicks");
4961
+ const installCapture = join(packageDir, "js/installCapture");
4818
4962
  return `// GENERATED by rscKit() — do not edit.
4819
4963
  import { createFromReadableStream } from '@vitejs/plugin-rsc/ssr'
4820
4964
  import { renderToReadableStream, resume } from 'react-dom/server.edge'
@@ -4825,6 +4969,11 @@ import { cancelledByConsumer, caughtByLoading } from ${JSON.stringify(fallbackRe
4825
4969
  import { parseRedirectDigest } from ${JSON.stringify(redirectDigestModule)}
4826
4970
  import { isNotFoundDigest } from ${JSON.stringify(notFoundModule)}
4827
4971
  import { EARLY_CLICKS } from ${JSON.stringify(earlyClicks)}
4972
+ ${webManifestOptions ? `import { INSTALL_CAPTURE } from ${JSON.stringify(installCapture)}` : ""}
4973
+
4974
+ // What runs ahead of the runtime: held taps, and - in an app that can be
4975
+ // installed at all - the browser's offer to install it. See installCapture.ts.
4976
+ const EARLY = EARLY_CLICKS${webManifestOptions ? " + INSTALL_CAPTURE" : ""}
4828
4977
 
4829
4978
  // Set only by the dev server. @vitejs/plugin-rsc emits its bootstrap and CSS
4830
4979
  // links root-relative in dev, which would send the browser to the host for
@@ -4847,7 +4996,7 @@ export async function handleSsr(
4847
4996
  // seconds before the runtime hydrates is held for it rather than given
4848
4997
  // to the browser as a document load. See earlyClicks.ts.
4849
4998
  const bootstrapScriptContent = bootstrap
4850
- ? EARLY_CLICKS + (await (import.meta as any).viteRsc.loadBootstrapScriptContent('index'))
4999
+ ? EARLY + (await (import.meta as any).viteRsc.loadBootstrapScriptContent('index'))
4851
5000
  : undefined
4852
5001
 
4853
5002
  // Without an onError handler React rejects each abortable task on its own,
@@ -4889,7 +5038,7 @@ export async function handleSsrPrerender(
4889
5038
  const bootstrapScriptContent =
4890
5039
  options.bootstrap === false
4891
5040
  ? undefined
4892
- : EARLY_CLICKS + (await (import.meta as any).viteRsc.loadBootstrapScriptContent('index'))
5041
+ : EARLY + (await (import.meta as any).viteRsc.loadBootstrapScriptContent('index'))
4893
5042
 
4894
5043
  const { prelude, postponed } = await prerender(root as any, {
4895
5044
  bootstrapScriptContent,
@@ -5052,9 +5201,21 @@ function generateEntryBrowser() {
5052
5201
  }))
5053
5202
  : null;
5054
5203
  const refreshModule = join(packageDir, "js/navigate");
5204
+ const chunkPreload = join(packageDir, "js/chunkPreload");
5055
5205
  return `// GENERATED by rscKit() — do not edit.
5056
5206
  import { createViteRscApp } from ${JSON.stringify(clientBootstrap)}
5057
5207
  import { refresh } from ${JSON.stringify(refreshModule)}
5208
+ import { setChunkLoader } from ${JSON.stringify(chunkPreload)}
5209
+ import * as clientReferences from 'virtual:vite-rsc/client-references'
5210
+
5211
+ // How a client component's id becomes its chunk, for loading the ones a
5212
+ // prefetched page uses before the tap - see chunkPreload.ts. The same map the
5213
+ // runtime loads them through, so a chunk loaded here is the one it finds.
5214
+ // A production build only: in development the runtime imports by path.
5215
+ if (import.meta.env.PROD) {
5216
+ const references = (clientReferences as { default: Record<string, () => Promise<unknown>> }).default
5217
+ setChunkLoader((id) => references[id]?.())
5218
+ }
5058
5219
 
5059
5220
  createViteRscApp(document, ${JSON.stringify(interceptManifest())}, ${JSON.stringify({
5060
5221
  staticPayloads: staticPayloads || null,
@@ -5268,6 +5429,25 @@ const JSX_PLUGIN_PATTERN = /react|babel|oxc/i;
5268
5429
  * class is ever constructed on the server.
5269
5430
  */
5270
5431
  const CLIENT_STUB = /registerClientReference\(\s*\(\) => \{ throw new Error\("Unexpectedly client reference export '"/g;
5432
+ /**
5433
+ * The server's render of client components, with names a second bundler
5434
+ * cannot change. See stableNames.ts: a replay matches slots by component
5435
+ * name, and `bun build --compile` renames whatever collides when it merges
5436
+ * module scopes. Stamped on the ssr output as written, so the names are
5437
+ * the ones the prerender recorded - it renders from this same output - and
5438
+ * the ones anything serving it reads, compiled or not.
5439
+ */
5440
+ function stableComponentNames() {
5441
+ return {
5442
+ name: "rsc-kit:stable-component-names",
5443
+ apply: "build",
5444
+ applyToEnvironment: (environment) => environment.name === "ssr",
5445
+ renderChunk(code) {
5446
+ const stamped = stampDisplayNames(code, (source) => parseAst(source));
5447
+ return stamped === code ? null : { code: stamped, map: null };
5448
+ },
5449
+ };
5450
+ }
5271
5451
  function extendableClientReferences() {
5272
5452
  return {
5273
5453
  name: "rsc-kit:extendable-client-references",
@@ -6075,7 +6255,13 @@ export function rscKit(options = {}) {
6075
6255
  // dependency exists once, so there is nothing to collide.
6076
6256
  // The server does not pay for the bigger chunk the way a
6077
6257
  // browser would: it reads from disk, once, at startup.
6078
- advancedChunks: {
6258
+ //
6259
+ // `codeSplitting`, rolldown's name for what was
6260
+ // `advancedChunks`: the old name still works and prints a
6261
+ // deprecation warning in every build, twice - plugin-rsc
6262
+ // builds this environment once to scan and once for real.
6263
+ // Every rolldown a Vite 8 ships with knows the new name.
6264
+ codeSplitting: {
6079
6265
  groups: [{ name: "vendor", test: /[\\/]node_modules[\\/]/ }],
6080
6266
  },
6081
6267
  },
@@ -6417,6 +6603,7 @@ export function rscKit(options = {}) {
6417
6603
  metadataRoutesPlugin(),
6418
6604
  openApiPlugin(),
6419
6605
  extendableClientReferences(),
6606
+ stableComponentNames(),
6420
6607
  typecheckPlugin(),
6421
6608
  clientImportsAudit(),
6422
6609
  routesPlugin,