@rsc-kit/core 0.20.3 → 0.20.5

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
@@ -756,6 +756,14 @@ function reportAllDynamic() {
756
756
  export const SERVICE_WORKER = (version, precache, frozen = [], offlineUrl = null, swExtra = null) => `// GENERATED by rscKit() — do not edit.
757
757
  const VERSION = ${JSON.stringify(version)}
758
758
  const CACHE = 'rsc-kit-' + VERSION
759
+
760
+ // This worker's own cache, and only that. \`caches.match()\` searches every
761
+ // cache on the origin - the previous worker's included, until this one's
762
+ // activation sweeps it - and answered a payload request for the page with
763
+ // the previous build's payload: client references this build does not
764
+ // have, and the page reloaded to recover. A document, a payload, a page
765
+ // this worker did not cache is not served from a cache it did not fill.
766
+ const own = (key, options) => caches.open(CACHE).then((cache) => cache.match(key, options))
759
767
  const PRECACHE = ${JSON.stringify(precache, null, 2)}
760
768
 
761
769
  // The urls the build stored whole. Their answer cannot change until the next
@@ -869,6 +877,39 @@ async function tellTheOpenPages() {
869
877
 
870
878
  const immutable = (url) => url.pathname.startsWith('/assets/') || /-[A-Za-z0-9_-]{8,}\\.[a-z]+$/.test(url.pathname)
871
879
 
880
+ // The build the server is on, as a page reported it: a page told 409 - its
881
+ // build is not the server's - says so here before it loads the document,
882
+ // and answers with the same to say it has been heard. Until this worker is
883
+ // replaced, a stored document from any other build is served from the
884
+ // network rather than the cache. Without this a phone lived in a loop for
885
+ // as long as the new worker took to install: the old worker served the old
886
+ // document from cache, the old client was told 409 on its first tap, loaded
887
+ // the document - the old one again - and so on, once per url, a document
888
+ // load per tap, for the tens of seconds a phone spends precaching.
889
+ let serverBuild = null
890
+
891
+ self.addEventListener('message', (event) => {
892
+ const data = event.data
893
+
894
+ if (data && data.type === 'rsc-kit:server-build' && typeof data.version === 'string') {
895
+ serverBuild = data.version
896
+
897
+ if (event.ports && event.ports[0]) event.ports[0].postMessage({ type: 'rsc-kit:server-build', version: serverBuild })
898
+ }
899
+ })
900
+
901
+ // Whether a cached document may be the previous build's: the server has
902
+ // been reported on another build than the copy carries, or a newer worker
903
+ // is installing or waiting behind this one.
904
+ const maybeStale = (hit) => {
905
+ if (!hit) return false
906
+ if (self.registration && (self.registration.installing || self.registration.waiting)) return true
907
+
908
+ const built = hit.headers.get('X-RSC-Version')
909
+
910
+ return serverBuild !== null && built !== null && built !== serverBuild
911
+ }
912
+
872
913
  // Whether a response may be kept at all.
873
914
  //
874
915
  // \`no-store\` is not advice here, it is the answer. A query defaults to
@@ -905,11 +946,11 @@ const keyFor = (request) => {
905
946
  const standIn = async (request) => {
906
947
  if (!OFFLINE_URL) return null
907
948
 
908
- if (request.mode === 'navigate') return (await caches.match(OFFLINE_URL)) ?? null
949
+ if (request.mode === 'navigate') return (await own(OFFLINE_URL)) ?? null
909
950
 
910
951
  if (request.headers.get('X-RSC') && !request.headers.get('X-RSC-Segments')) {
911
952
  return (
912
- (await caches.match(
953
+ (await own(
913
954
  keyFor(new Request(new URL(OFFLINE_URL, self.location.origin), { headers: { 'X-RSC': '1' } })),
914
955
  MATCH,
915
956
  )) ?? null
@@ -960,7 +1001,7 @@ self.addEventListener('fetch', (event) => {
960
1001
  // differently for every value of it.
961
1002
  if (FROZEN.has(url.pathname.replace(/\\/+$/, '') || '/') && !url.search) {
962
1003
  event.respondWith(
963
- caches.match(keyFor(request), MATCH).then((hit) => {
1004
+ own(keyFor(request), MATCH).then((hit) => {
964
1005
  const fresh = fetch(request)
965
1006
  .then((response) => {
966
1007
  if (mayStore(response)) {
@@ -984,7 +1025,11 @@ self.addEventListener('fetch', (event) => {
984
1025
  })
985
1026
 
986
1027
  // Not awaited when there is a hit: the point is that the visitor does
987
- // not wait for the network for a page that cannot have changed.
1028
+ // not wait for the network for a page that cannot have changed - and
1029
+ // a deploy is how it changes, so a copy that may be the previous
1030
+ // build's waits for the network, with the copy as the fallback.
1031
+ if (maybeStale(hit)) return fresh.catch(() => hit)
1032
+
988
1033
  return hit ?? fresh
989
1034
  }),
990
1035
  )
@@ -1044,7 +1089,7 @@ self.addEventListener('fetch', (event) => {
1044
1089
  return response
1045
1090
  })
1046
1091
  .catch(async () => {
1047
- const hit = await caches.match(keyFor(request), MATCH)
1092
+ const hit = await own(keyFor(request), MATCH)
1048
1093
 
1049
1094
  if (hit) return hit
1050
1095
 
@@ -2393,6 +2438,7 @@ import { LoadingBoundary } from ${JSON.stringify(join(packageDir, "js/LoadingBou
2393
2438
  import { DocumentTitle } from ${JSON.stringify(join(packageDir, "js/DocumentTitle"))}
2394
2439
  import { SlotBoundary } from ${JSON.stringify(join(packageDir, "js/SlotBoundary"))}
2395
2440
  import { RouteErrorBoundary } from ${JSON.stringify(join(packageDir, "js/RouteErrorBoundary"))}
2441
+ import __rsc_assets from 'virtual:vite-rsc/assets-manifest'
2396
2442
  import { sectionComponent } from ${JSON.stringify(join(packageDir, "js/section"))}
2397
2443
  import { PathnameProvider } from ${JSON.stringify(join(packageDir, "js/PathnameProvider"))}
2398
2444
  import { DefaultRouteError } from ${JSON.stringify(join(packageDir, "js/DefaultRouteError"))}
@@ -2659,14 +2705,48 @@ export function manifest(): any {
2659
2705
  return ${JSON.stringify(routeManifest())}
2660
2706
  }
2661
2707
 
2708
+ /**
2709
+ * The stylesheet and the client entry every document of this build links,
2710
+ * from the build's own manifest, for the host's Link header - a CDN sends
2711
+ * it ahead as 103 Early Hints. The root layout's stylesheet is the one the
2712
+ * document carries; the entry's own is usually empty, and either way both
2713
+ * are named. See earlyHints.ts.
2714
+ */
2715
+ export function criticalAssets(): { styles: string[]; modules: string[]; fonts: string[] } {
2716
+ // Nothing to send ahead in development: the entry is Vite's own url, and
2717
+ // there is no CDN in front to hint.
2718
+ if (import.meta.env.DEV) return { styles: [], modules: [], fonts: [] }
2719
+
2720
+ const manifest = __rsc_assets as {
2721
+ clientEntryUrl?: string
2722
+ clientEntryDeps?: { css?: string[] }
2723
+ serverResources?: Record<string, { css?: string[] }>
2724
+ }
2725
+ const styles = new Set<string>(manifest.clientEntryDeps?.css ?? [])
2726
+
2727
+ const rootLayouts = ['app/layout.tsx', 'app/layout.jsx', 'app/layout.ts', 'app/layout.js']
2728
+
2729
+ for (const [file, resources] of Object.entries(manifest.serverResources ?? {})) {
2730
+ if (rootLayouts.some((name) => file === name || file.endsWith('/' + name))) {
2731
+ for (const href of resources.css ?? []) styles.add(href)
2732
+ }
2733
+ }
2734
+
2735
+ return {
2736
+ styles: [...styles],
2737
+ modules: manifest.clientEntryUrl ? [manifest.clientEntryUrl] : [],
2738
+ fonts: [],
2739
+ }
2740
+ }
2741
+
2662
2742
  let cachedBuildId: string | null = null
2663
2743
 
2664
2744
  /**
2665
2745
  * What this build's client is, as a short id.
2666
2746
  *
2667
- * Derived from the bootstrap script the document ships - the hashed name of
2668
- * the browser entry, which changes with the client references and chunks
2669
- * it maps - so two builds whose browsers would disagree never share one.
2747
+ * Derived from the bootstrap script the document ships and the assets
2748
+ * manifest behind it - the entry, every client reference and the chunks
2749
+ * each loads - so two builds whose browsers would disagree never share one.
2670
2750
  * The host puts it on every answer and compares it against what a client
2671
2751
  * says it is running; a mismatch on a payload request is a 409, and the
2672
2752
  * client loads the document instead. Read once, on demand, because the
@@ -2675,7 +2755,13 @@ let cachedBuildId: string | null = null
2675
2755
  export async function buildId(): Promise<string> {
2676
2756
  if (cachedBuildId !== null) return cachedBuildId
2677
2757
 
2678
- const content: string = await (import.meta as any).viteRsc.loadBootstrapScriptContent('index')
2758
+ // The bootstrap script names the entry; the assets manifest names every
2759
+ // client reference and the chunks each loads. A client component added
2760
+ // to a layout changes the manifest and not always the entry - the
2761
+ // reference map is its own chunk - and that is exactly the change an old
2762
+ // client cannot survive, so the id covers both.
2763
+ const content: string =
2764
+ (await (import.meta as any).viteRsc.loadBootstrapScriptContent('index')) + JSON.stringify(__rsc_assets)
2679
2765
  let hash = 2166136261
2680
2766
 
2681
2767
  for (let i = 0; i < content.length; i++) {
@@ -4475,6 +4561,7 @@ async function serve(request: Request): Promise<Response> {
4475
4561
  resolveMetadata,
4476
4562
  runRouteMiddleware,
4477
4563
  buildId,
4564
+ criticalAssets,
4478
4565
  } as never,
4479
4566
  ${NITRO_HANDLER_OPTIONS}${NITRO_PRERENDERED} maxActionBody: ${maxActionBody === undefined ? "undefined" : String(maxActionBody)},
4480
4567
  })
@@ -4558,6 +4645,7 @@ function generateEntrySsr() {
4558
4645
  const fallbackReport = join(packageDir, "js/fallbackReport");
4559
4646
  const redirectDigestModule = join(packageDir, "redirectDigest");
4560
4647
  const notFoundModule = join(packageDir, "notFound");
4648
+ const earlyClicks = join(packageDir, "js/earlyClicks");
4561
4649
  return `// GENERATED by rscKit() — do not edit.
4562
4650
  import { createFromReadableStream } from '@vitejs/plugin-rsc/ssr'
4563
4651
  import { renderToReadableStream, resume } from 'react-dom/server.edge'
@@ -4567,6 +4655,7 @@ import { noteFallback } from ${JSON.stringify(request)}
4567
4655
  import { cancelledByConsumer, caughtByLoading } from ${JSON.stringify(fallbackReport)}
4568
4656
  import { parseRedirectDigest } from ${JSON.stringify(redirectDigestModule)}
4569
4657
  import { isNotFoundDigest } from ${JSON.stringify(notFoundModule)}
4658
+ import { EARLY_CLICKS } from ${JSON.stringify(earlyClicks)}
4570
4659
 
4571
4660
  // Set only by the dev server. @vitejs/plugin-rsc emits its bootstrap and CSS
4572
4661
  // links root-relative in dev, which would send the browser to the host for
@@ -4585,8 +4674,11 @@ export async function handleSsr(
4585
4674
  // Without the bootstrap the page ships no client runtime at all: no React,
4586
4675
  // no Flight client, no router. HTML only. A page with nothing interactive on
4587
4676
  // it has no use for 70kB of hydration.
4677
+ // The click listener first, then the runtime's import: a tap in the
4678
+ // seconds before the runtime hydrates is held for it rather than given
4679
+ // to the browser as a document load. See earlyClicks.ts.
4588
4680
  const bootstrapScriptContent = bootstrap
4589
- ? await (import.meta as any).viteRsc.loadBootstrapScriptContent('index')
4681
+ ? EARLY_CLICKS + (await (import.meta as any).viteRsc.loadBootstrapScriptContent('index'))
4590
4682
  : undefined
4591
4683
 
4592
4684
  // Without an onError handler React rejects each abortable task on its own,
@@ -4628,7 +4720,7 @@ export async function handleSsrPrerender(
4628
4720
  const bootstrapScriptContent =
4629
4721
  options.bootstrap === false
4630
4722
  ? undefined
4631
- : await (import.meta as any).viteRsc.loadBootstrapScriptContent('index')
4723
+ : EARLY_CLICKS + (await (import.meta as any).viteRsc.loadBootstrapScriptContent('index'))
4632
4724
 
4633
4725
  const { prelude, postponed } = await prerender(root as any, {
4634
4726
  bootstrapScriptContent,
@@ -5458,6 +5550,16 @@ export function rscKit(options = {}) {
5458
5550
  if (typeof nitro.options.compressPublicAssets !== "object")
5459
5551
  nitro.options.compressPublicAssets = true;
5460
5552
  nitro.options.virtual ??= {};
5553
+ // The worker script is never cached by heuristics. It was served
5554
+ // with no Cache-Control at all, and a browser keeps such a file for a
5555
+ // tenth of its age: a deploy's new worker then installs the previous
5556
+ // deploy's list against the new server, every file 404s, and the
5557
+ // install fails - leaving the old worker in charge, serving the old
5558
+ // documents, for as long as the browser holds the script.
5559
+ nitro.options.routeRules = {
5560
+ ...(nitro.options.routeRules ?? {}),
5561
+ "/sw.js": { ...(nitro.options.routeRules?.["/sw.js"] ?? {}), headers: { "cache-control": "no-cache", ...(nitro.options.routeRules?.["/sw.js"]?.headers ?? {}) } },
5562
+ };
5461
5563
  // The names of functions survive Nitro's bundle. The pages were
5462
5564
  // prerendered against Vite's own server bundles, where a dependency
5463
5565
  // is loaded from node_modules under the names it was written with;