@rsc-kit/core 0.16.2 → 0.16.3

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.
@@ -23,8 +23,11 @@ export interface AppAssets {
23
23
  export declare const ASSET_BASE = "/_app";
24
24
  /** What the app declared by putting a file where it could be found. */
25
25
  export declare function appAssets(appDir: string): AppAssets;
26
+ export declare function typeOf(file: string): string;
26
27
  /** The head tags these produce, as plain data the generated entry turns into elements. */
28
+ /** Every asset found, favicon first: what the build copies and the dev server answers. */
29
+ export declare function allAppAssets(assets: AppAssets): AppAsset[];
27
30
  export declare function headTags(assets: AppAssets, origin?: string): {
28
- tag: 'link' | 'meta';
31
+ tag: "link" | "meta";
29
32
  props: Record<string, string>;
30
33
  }[];
package/dist/appAssets.js CHANGED
@@ -16,7 +16,7 @@
16
16
  // They live in `app/` rather than `public/` on purpose. `public/` is "serve
17
17
  // this verbatim"; these are read as well as served — an icon's size decides
18
18
  // what goes in the manifest, and its presence decides what goes in the head.
19
- import { existsSync, readdirSync } from 'node:fs';
19
+ import { existsSync, readdirSync } from "node:fs";
20
20
  const IMAGE = /\.(png|svg|jpg|jpeg|webp|gif|avif)$/i;
21
21
  /**
22
22
  * Where these are served from.
@@ -26,7 +26,7 @@ const IMAGE = /\.(png|svg|jpg|jpeg|webp|gif|avif)$/i;
26
26
  * these, in either direction. The exception is the favicon, which browsers ask
27
27
  * for at `/favicon.ico` whatever any markup says.
28
28
  */
29
- export const ASSET_BASE = '/_app';
29
+ export const ASSET_BASE = "/_app";
30
30
  function assetsIn(dir, match) {
31
31
  return readdirSync(dir)
32
32
  .filter(match)
@@ -36,7 +36,13 @@ function assetsIn(dir, match) {
36
36
  /** What the app declared by putting a file where it could be found. */
37
37
  export function appAssets(appDir) {
38
38
  if (!existsSync(appDir)) {
39
- return { favicon: null, icons: [], appleIcon: null, openGraph: null, twitter: null };
39
+ return {
40
+ favicon: null,
41
+ icons: [],
42
+ appleIcon: null,
43
+ openGraph: null,
44
+ twitter: null,
45
+ };
40
46
  }
41
47
  const names = readdirSync(appDir);
42
48
  const has = (name) => names.includes(name);
@@ -44,46 +50,76 @@ export function appAssets(appDir) {
44
50
  // Served where a browser looks for it regardless of markup: a request for
45
51
  // /favicon.ico goes out whether or not a link tag exists, and answering it
46
52
  // from somewhere else means answering a 404 to the request that matters.
47
- favicon: has('favicon.ico') ? { file: 'favicon.ico', href: '/favicon.ico' } : null,
53
+ favicon: has("favicon.ico")
54
+ ? { file: "favicon.ico", href: "/favicon.ico" }
55
+ : null,
48
56
  icons: assetsIn(appDir, (name) => /^icon[-\w]*/.test(name) && IMAGE.test(name)),
49
57
  appleIcon: assetsIn(appDir, (name) => /^apple-icon[-\w]*/.test(name) && IMAGE.test(name))[0] ?? null,
50
58
  openGraph: assetsIn(appDir, (name) => /^opengraph-image[-\w]*/.test(name) && IMAGE.test(name))[0] ?? null,
51
59
  twitter: assetsIn(appDir, (name) => /^twitter-image[-\w]*/.test(name) && IMAGE.test(name))[0] ?? null,
52
60
  };
53
61
  }
54
- function typeOf(file) {
55
- const extension = file.slice(file.lastIndexOf('.') + 1).toLowerCase();
56
- return extension === 'svg'
57
- ? 'image/svg+xml'
58
- : extension === 'jpg' || extension === 'jpeg'
59
- ? 'image/jpeg'
60
- : extension === 'ico'
61
- ? 'image/x-icon'
62
+ export function typeOf(file) {
63
+ const extension = file.slice(file.lastIndexOf(".") + 1).toLowerCase();
64
+ return extension === "svg"
65
+ ? "image/svg+xml"
66
+ : extension === "jpg" || extension === "jpeg"
67
+ ? "image/jpeg"
68
+ : extension === "ico"
69
+ ? "image/x-icon"
62
70
  : `image/${extension}`;
63
71
  }
64
72
  /** The head tags these produce, as plain data the generated entry turns into elements. */
73
+ /** Every asset found, favicon first: what the build copies and the dev server answers. */
74
+ export function allAppAssets(assets) {
75
+ return [
76
+ ...(assets.favicon ? [assets.favicon] : []),
77
+ ...assets.icons,
78
+ ...(assets.appleIcon ? [assets.appleIcon] : []),
79
+ ...(assets.openGraph ? [assets.openGraph] : []),
80
+ ...(assets.twitter ? [assets.twitter] : []),
81
+ ];
82
+ }
65
83
  export function headTags(assets, origin) {
66
84
  const tags = [];
67
85
  if (assets.favicon) {
68
- tags.push({ tag: 'link', props: { rel: 'icon', href: assets.favicon.href, type: 'image/x-icon' } });
86
+ tags.push({
87
+ tag: "link",
88
+ props: { rel: "icon", href: assets.favicon.href, type: "image/x-icon" },
89
+ });
69
90
  }
70
91
  for (const icon of assets.icons) {
71
- tags.push({ tag: 'link', props: { rel: 'icon', href: icon.href, type: typeOf(icon.file) } });
92
+ tags.push({
93
+ tag: "link",
94
+ props: { rel: "icon", href: icon.href, type: typeOf(icon.file) },
95
+ });
72
96
  }
73
97
  if (assets.appleIcon) {
74
- tags.push({ tag: 'link', props: { rel: 'apple-touch-icon', href: assets.appleIcon.href } });
98
+ tags.push({
99
+ tag: "link",
100
+ props: { rel: "apple-touch-icon", href: assets.appleIcon.href },
101
+ });
75
102
  }
76
103
  // Absolute when the app said where it lives. The og spec asks for an absolute
77
104
  // url and several crawlers still mean it — a relative one is read by some and
78
105
  // ignored by others, which is the worst of both. Relative is what is left
79
106
  // when nothing said, and is better than omitting the tag.
80
- const absolute = (href) => (origin ? new URL(href, origin).href : href);
107
+ const absolute = (href) => origin ? new URL(href, origin).href : href;
81
108
  if (assets.openGraph) {
82
- tags.push({ tag: 'meta', props: { property: 'og:image', content: absolute(assets.openGraph.href) } });
109
+ tags.push({
110
+ tag: "meta",
111
+ props: { property: "og:image", content: absolute(assets.openGraph.href) },
112
+ });
83
113
  }
84
114
  if (assets.twitter) {
85
- tags.push({ tag: 'meta', props: { name: 'twitter:image', content: absolute(assets.twitter.href) } });
86
- tags.push({ tag: 'meta', props: { name: 'twitter:card', content: 'summary_large_image' } });
115
+ tags.push({
116
+ tag: "meta",
117
+ props: { name: "twitter:image", content: absolute(assets.twitter.href) },
118
+ });
119
+ tags.push({
120
+ tag: "meta",
121
+ props: { name: "twitter:card", content: "summary_large_image" },
122
+ });
87
123
  }
88
124
  return tags;
89
125
  }
@@ -1 +1 @@
1
- {"version":3,"file":"appAssets.js","sourceRoot":"","sources":["../src/appAssets.ts"],"names":[],"mappings":"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","sourcesContent":["// Files that describe the app, found by name rather than configured.\n//\n// src/app/\n// favicon.ico <link rel=\"icon\">\n// icon.png <link rel=\"icon\">, and the manifest's icons\n// icon-192.png …as many as you like; sizes read from the name\n// apple-icon.png <link rel=\"apple-touch-icon\">\n// opengraph-image.png <meta property=\"og:image\">\n// twitter-image.png <meta name=\"twitter:image\">\n//\n// The same bargain as every other file in this directory: put it where it\n// belongs and it works, with nothing to register. Next established these names\n// and there is nothing to gain by inventing different ones — an app moving\n// between the two should not have to rename its favicon.\n//\n// They live in `app/` rather than `public/` on purpose. `public/` is \"serve\n// this verbatim\"; these are read as well as served — an icon's size decides\n// what goes in the manifest, and its presence decides what goes in the head.\n\nimport { existsSync, readdirSync } from 'node:fs'\n\n/** One found file, and where it will be served from. */\nexport interface AppAsset {\n /** The file's name in the app directory. */\n file: string\n /** The url it is served at. */\n href: string\n}\n\nexport interface AppAssets {\n favicon: AppAsset | null\n icons: AppAsset[]\n appleIcon: AppAsset | null\n openGraph: AppAsset | null\n twitter: AppAsset | null\n}\n\nconst IMAGE = /\\.(png|svg|jpg|jpeg|webp|gif|avif)$/i\n\n/**\n * Where these are served from.\n *\n * Their own directory rather than the output root, so an app that happens to\n * have a `public/icon.png` as well is not silently overwritten by one of\n * these, in either direction. The exception is the favicon, which browsers ask\n * for at `/favicon.ico` whatever any markup says.\n */\nexport const ASSET_BASE = '/_app'\n\nfunction assetsIn(dir: string, match: (name: string) => boolean): AppAsset[] {\n return readdirSync(dir)\n .filter(match)\n .sort()\n .map((file) => ({ file, href: `${ASSET_BASE}/${file}` }))\n}\n\n/** What the app declared by putting a file where it could be found. */\nexport function appAssets(appDir: string): AppAssets {\n if (!existsSync(appDir)) {\n return { favicon: null, icons: [], appleIcon: null, openGraph: null, twitter: null }\n }\n\n const names = readdirSync(appDir)\n const has = (name: string) => names.includes(name)\n\n return {\n // Served where a browser looks for it regardless of markup: a request for\n // /favicon.ico goes out whether or not a link tag exists, and answering it\n // from somewhere else means answering a 404 to the request that matters.\n favicon: has('favicon.ico') ? { file: 'favicon.ico', href: '/favicon.ico' } : null,\n icons: assetsIn(appDir, (name) => /^icon[-\\w]*/.test(name) && IMAGE.test(name)),\n appleIcon:\n assetsIn(appDir, (name) => /^apple-icon[-\\w]*/.test(name) && IMAGE.test(name))[0] ?? null,\n openGraph:\n assetsIn(appDir, (name) => /^opengraph-image[-\\w]*/.test(name) && IMAGE.test(name))[0] ?? null,\n twitter:\n assetsIn(appDir, (name) => /^twitter-image[-\\w]*/.test(name) && IMAGE.test(name))[0] ?? null,\n }\n}\n\nfunction typeOf(file: string): string {\n const extension = file.slice(file.lastIndexOf('.') + 1).toLowerCase()\n\n return extension === 'svg'\n ? 'image/svg+xml'\n : extension === 'jpg' || extension === 'jpeg'\n ? 'image/jpeg'\n : extension === 'ico'\n ? 'image/x-icon'\n : `image/${extension}`\n}\n\n/** The head tags these produce, as plain data the generated entry turns into elements. */\nexport function headTags(\n assets: AppAssets,\n origin?: string,\n): { tag: 'link' | 'meta'; props: Record<string, string> }[] {\n const tags: { tag: 'link' | 'meta'; props: Record<string, string> }[] = []\n\n if (assets.favicon) {\n tags.push({ tag: 'link', props: { rel: 'icon', href: assets.favicon.href, type: 'image/x-icon' } })\n }\n\n for (const icon of assets.icons) {\n tags.push({ tag: 'link', props: { rel: 'icon', href: icon.href, type: typeOf(icon.file) } })\n }\n\n if (assets.appleIcon) {\n tags.push({ tag: 'link', props: { rel: 'apple-touch-icon', href: assets.appleIcon.href } })\n }\n\n // Absolute when the app said where it lives. The og spec asks for an absolute\n // url and several crawlers still mean it — a relative one is read by some and\n // ignored by others, which is the worst of both. Relative is what is left\n // when nothing said, and is better than omitting the tag.\n const absolute = (href: string) => (origin ? new URL(href, origin).href : href)\n\n if (assets.openGraph) {\n tags.push({ tag: 'meta', props: { property: 'og:image', content: absolute(assets.openGraph.href) } })\n }\n\n if (assets.twitter) {\n tags.push({ tag: 'meta', props: { name: 'twitter:image', content: absolute(assets.twitter.href) } })\n tags.push({ tag: 'meta', props: { name: 'twitter:card', content: 'summary_large_image' } })\n }\n\n return tags\n}\n"]}
1
+ {"version":3,"file":"appAssets.js","sourceRoot":"","sources":["../src/appAssets.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,EAAE;AACF,aAAa;AACb,6CAA6C;AAC7C,uEAAuE;AACvE,0EAA0E;AAC1E,yDAAyD;AACzD,sDAAsD;AACtD,uDAAuD;AACvD,EAAE;AACF,0EAA0E;AAC1E,+EAA+E;AAC/E,2EAA2E;AAC3E,yDAAyD;AACzD,EAAE;AACF,4EAA4E;AAC5E,4EAA4E;AAC5E,6EAA6E;AAE7E,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAkBlD,MAAM,KAAK,GAAG,sCAAsC,CAAC;AAErD;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,OAAO,CAAC;AAElC,SAAS,QAAQ,CAAC,GAAW,EAAE,KAAgC;IAC7D,OAAO,WAAW,CAAC,GAAG,CAAC;SACpB,MAAM,CAAC,KAAK,CAAC;SACb,IAAI,EAAE;SACN,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,UAAU,IAAI,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC;AAC9D,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,SAAS,CAAC,MAAc;IACtC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;QACxB,OAAO;YACL,OAAO,EAAE,IAAI;YACb,KAAK,EAAE,EAAE;YACT,SAAS,EAAE,IAAI;YACf,SAAS,EAAE,IAAI;YACf,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;IAED,MAAM,KAAK,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;IAClC,MAAM,GAAG,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IAEnD,OAAO;QACL,0EAA0E;QAC1E,2EAA2E;QAC3E,yEAAyE;QACzE,OAAO,EAAE,GAAG,CAAC,aAAa,CAAC;YACzB,CAAC,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,cAAc,EAAE;YAC/C,CAAC,CAAC,IAAI;QACR,KAAK,EAAE,QAAQ,CACb,MAAM,EACN,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CACvD;QACD,SAAS,EACP,QAAQ,CACN,MAAM,EACN,CAAC,IAAI,EAAE,EAAE,CAAC,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAC7D,CAAC,CAAC,CAAC,IAAI,IAAI;QACd,SAAS,EACP,QAAQ,CACN,MAAM,EACN,CAAC,IAAI,EAAE,EAAE,CAAC,wBAAwB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAClE,CAAC,CAAC,CAAC,IAAI,IAAI;QACd,OAAO,EACL,QAAQ,CACN,MAAM,EACN,CAAC,IAAI,EAAE,EAAE,CAAC,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAChE,CAAC,CAAC,CAAC,IAAI,IAAI;KACf,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,IAAY;IACjC,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;IAEtE,OAAO,SAAS,KAAK,KAAK;QACxB,CAAC,CAAC,eAAe;QACjB,CAAC,CAAC,SAAS,KAAK,KAAK,IAAI,SAAS,KAAK,MAAM;YAC3C,CAAC,CAAC,YAAY;YACd,CAAC,CAAC,SAAS,KAAK,KAAK;gBACnB,CAAC,CAAC,cAAc;gBAChB,CAAC,CAAC,SAAS,SAAS,EAAE,CAAC;AAC/B,CAAC;AAED,0FAA0F;AAC1F,0FAA0F;AAC1F,MAAM,UAAU,YAAY,CAAC,MAAiB;IAC5C,OAAO;QACL,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3C,GAAG,MAAM,CAAC,KAAK;QACf,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/C,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/C,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;KAC5C,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,QAAQ,CACtB,MAAiB,EACjB,MAAe;IAEf,MAAM,IAAI,GAA8D,EAAE,CAAC;IAE3E,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QACnB,IAAI,CAAC,IAAI,CAAC;YACR,GAAG,EAAE,MAAM;YACX,KAAK,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,cAAc,EAAE;SACxE,CAAC,CAAC;IACL,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QAChC,IAAI,CAAC,IAAI,CAAC;YACR,GAAG,EAAE,MAAM;YACX,KAAK,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;SACjE,CAAC,CAAC;IACL,CAAC;IAED,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;QACrB,IAAI,CAAC,IAAI,CAAC;YACR,GAAG,EAAE,MAAM;YACX,KAAK,EAAE,EAAE,GAAG,EAAE,kBAAkB,EAAE,IAAI,EAAE,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE;SAChE,CAAC,CAAC;IACL,CAAC;IAED,8EAA8E;IAC9E,8EAA8E;IAC9E,0EAA0E;IAC1E,0DAA0D;IAC1D,MAAM,QAAQ,GAAG,CAAC,IAAY,EAAE,EAAE,CAChC,MAAM,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;IAE7C,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;QACrB,IAAI,CAAC,IAAI,CAAC;YACR,GAAG,EAAE,MAAM;YACX,KAAK,EAAE,EAAE,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE;SAC1E,CAAC,CAAC;IACL,CAAC;IAED,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QACnB,IAAI,CAAC,IAAI,CAAC;YACR,GAAG,EAAE,MAAM;YACX,KAAK,EAAE,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;SACzE,CAAC,CAAC;QACH,IAAI,CAAC,IAAI,CAAC;YACR,GAAG,EAAE,MAAM;YACX,KAAK,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,qBAAqB,EAAE;SAChE,CAAC,CAAC;IACL,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["// Files that describe the app, found by name rather than configured.\n//\n// src/app/\n// favicon.ico <link rel=\"icon\">\n// icon.png <link rel=\"icon\">, and the manifest's icons\n// icon-192.png …as many as you like; sizes read from the name\n// apple-icon.png <link rel=\"apple-touch-icon\">\n// opengraph-image.png <meta property=\"og:image\">\n// twitter-image.png <meta name=\"twitter:image\">\n//\n// The same bargain as every other file in this directory: put it where it\n// belongs and it works, with nothing to register. Next established these names\n// and there is nothing to gain by inventing different ones — an app moving\n// between the two should not have to rename its favicon.\n//\n// They live in `app/` rather than `public/` on purpose. `public/` is \"serve\n// this verbatim\"; these are read as well as served — an icon's size decides\n// what goes in the manifest, and its presence decides what goes in the head.\n\nimport { existsSync, readdirSync } from \"node:fs\";\n\n/** One found file, and where it will be served from. */\nexport interface AppAsset {\n /** The file's name in the app directory. */\n file: string;\n /** The url it is served at. */\n href: string;\n}\n\nexport interface AppAssets {\n favicon: AppAsset | null;\n icons: AppAsset[];\n appleIcon: AppAsset | null;\n openGraph: AppAsset | null;\n twitter: AppAsset | null;\n}\n\nconst IMAGE = /\\.(png|svg|jpg|jpeg|webp|gif|avif)$/i;\n\n/**\n * Where these are served from.\n *\n * Their own directory rather than the output root, so an app that happens to\n * have a `public/icon.png` as well is not silently overwritten by one of\n * these, in either direction. The exception is the favicon, which browsers ask\n * for at `/favicon.ico` whatever any markup says.\n */\nexport const ASSET_BASE = \"/_app\";\n\nfunction assetsIn(dir: string, match: (name: string) => boolean): AppAsset[] {\n return readdirSync(dir)\n .filter(match)\n .sort()\n .map((file) => ({ file, href: `${ASSET_BASE}/${file}` }));\n}\n\n/** What the app declared by putting a file where it could be found. */\nexport function appAssets(appDir: string): AppAssets {\n if (!existsSync(appDir)) {\n return {\n favicon: null,\n icons: [],\n appleIcon: null,\n openGraph: null,\n twitter: null,\n };\n }\n\n const names = readdirSync(appDir);\n const has = (name: string) => names.includes(name);\n\n return {\n // Served where a browser looks for it regardless of markup: a request for\n // /favicon.ico goes out whether or not a link tag exists, and answering it\n // from somewhere else means answering a 404 to the request that matters.\n favicon: has(\"favicon.ico\")\n ? { file: \"favicon.ico\", href: \"/favicon.ico\" }\n : null,\n icons: assetsIn(\n appDir,\n (name) => /^icon[-\\w]*/.test(name) && IMAGE.test(name),\n ),\n appleIcon:\n assetsIn(\n appDir,\n (name) => /^apple-icon[-\\w]*/.test(name) && IMAGE.test(name),\n )[0] ?? null,\n openGraph:\n assetsIn(\n appDir,\n (name) => /^opengraph-image[-\\w]*/.test(name) && IMAGE.test(name),\n )[0] ?? null,\n twitter:\n assetsIn(\n appDir,\n (name) => /^twitter-image[-\\w]*/.test(name) && IMAGE.test(name),\n )[0] ?? null,\n };\n}\n\nexport function typeOf(file: string): string {\n const extension = file.slice(file.lastIndexOf(\".\") + 1).toLowerCase();\n\n return extension === \"svg\"\n ? \"image/svg+xml\"\n : extension === \"jpg\" || extension === \"jpeg\"\n ? \"image/jpeg\"\n : extension === \"ico\"\n ? \"image/x-icon\"\n : `image/${extension}`;\n}\n\n/** The head tags these produce, as plain data the generated entry turns into elements. */\n/** Every asset found, favicon first: what the build copies and the dev server answers. */\nexport function allAppAssets(assets: AppAssets): AppAsset[] {\n return [\n ...(assets.favicon ? [assets.favicon] : []),\n ...assets.icons,\n ...(assets.appleIcon ? [assets.appleIcon] : []),\n ...(assets.openGraph ? [assets.openGraph] : []),\n ...(assets.twitter ? [assets.twitter] : []),\n ];\n}\n\nexport function headTags(\n assets: AppAssets,\n origin?: string,\n): { tag: \"link\" | \"meta\"; props: Record<string, string> }[] {\n const tags: { tag: \"link\" | \"meta\"; props: Record<string, string> }[] = [];\n\n if (assets.favicon) {\n tags.push({\n tag: \"link\",\n props: { rel: \"icon\", href: assets.favicon.href, type: \"image/x-icon\" },\n });\n }\n\n for (const icon of assets.icons) {\n tags.push({\n tag: \"link\",\n props: { rel: \"icon\", href: icon.href, type: typeOf(icon.file) },\n });\n }\n\n if (assets.appleIcon) {\n tags.push({\n tag: \"link\",\n props: { rel: \"apple-touch-icon\", href: assets.appleIcon.href },\n });\n }\n\n // Absolute when the app said where it lives. The og spec asks for an absolute\n // url and several crawlers still mean it — a relative one is read by some and\n // ignored by others, which is the worst of both. Relative is what is left\n // when nothing said, and is better than omitting the tag.\n const absolute = (href: string) =>\n origin ? new URL(href, origin).href : href;\n\n if (assets.openGraph) {\n tags.push({\n tag: \"meta\",\n props: { property: \"og:image\", content: absolute(assets.openGraph.href) },\n });\n }\n\n if (assets.twitter) {\n tags.push({\n tag: \"meta\",\n props: { name: \"twitter:image\", content: absolute(assets.twitter.href) },\n });\n tags.push({\n tag: \"meta\",\n props: { name: \"twitter:card\", content: \"summary_large_image\" },\n });\n }\n\n return tags;\n}\n"]}
@@ -1,5 +1,13 @@
1
+ // Vite-engine client bootstrap. Uses @vitejs/plugin-rsc's browser runtime as
2
+ // the Flight deserializer + action encoder, and drives the package's engine-agnostic
3
+ // navigate.ts SPA engine (Link, prefetch, popstate) through it. This replaces
4
+ // the bun engine's createRscApp + the hand-rolled webpack shim — the plugin
5
+ // resolves client references itself.
6
+ import { SEARCH_PARAMS_FALLBACK } from "./useSearchParams";
7
+ import { recoverFromStaleAssets } from "./staleAssets";
8
+ import { showDevNotice } from "./devNotice";
1
9
  import { isSafeRedirect } from "../safeUrl.js";
2
- import { createFromReadableStream, encodeReply, setServerCallback } from "@vitejs/plugin-rsc/browser";
10
+ import { createFromReadableStream, encodeReply, setServerCallback, } from "@vitejs/plugin-rsc/browser";
3
11
  import { createElement } from "react";
4
12
  import { hydrateRoot } from "react-dom/client";
5
13
  import { ActivityRoot } from "./ActivityRouter";
@@ -39,7 +47,8 @@ export async function createViteRscApp(container = document, interceptEntries =
39
47
  if (encoded instanceof FormData) {
40
48
  const serialized = new Response(encoded);
41
49
  body = await serialized.arrayBuffer();
42
- realContentType = serialized.headers.get("content-type") ?? "multipart/form-data";
50
+ realContentType =
51
+ serialized.headers.get("content-type") ?? "multipart/form-data";
43
52
  }
44
53
  else {
45
54
  body = encoded;
@@ -90,7 +99,9 @@ export async function createViteRscApp(container = document, interceptEntries =
90
99
  * was updated around it.
91
100
  */
92
101
  function unwrapRevalidated(answer) {
93
- if (answer === null || typeof answer !== "object" || !("__rscRevalidated" in answer)) {
102
+ if (answer === null ||
103
+ typeof answer !== "object" ||
104
+ !("__rscRevalidated" in answer)) {
94
105
  return answer;
95
106
  }
96
107
  const envelope = answer;
@@ -118,13 +129,14 @@ export async function createViteRscApp(container = document, interceptEntries =
118
129
  setServerCallback(callServer);
119
130
  // Link / Form / router live in a separate build graph and reach the SPA
120
131
  // engine through these globals.
121
- window.__rsc_navigate = navigate;
122
- window.__rsc_prefetch = prefetch;
123
- window.__rsc_cancel_prefetch =
124
- cancelPrefetch;
125
- window.__rsc_is_prefetched =
126
- isPrefetched;
127
- window.__rsc_refresh = refresh;
132
+ window.__rsc_navigate =
133
+ navigate;
134
+ window.__rsc_prefetch =
135
+ prefetch;
136
+ window.__rsc_cancel_prefetch = cancelPrefetch;
137
+ window.__rsc_is_prefetched = isPrefetched;
138
+ window.__rsc_refresh =
139
+ refresh;
128
140
  // Hydrate from the RSC endpoint (same url + X-RSC, no version header).
129
141
  //
130
142
  // Everything from here to the decode is failure handling, because this is
@@ -174,9 +186,43 @@ export async function createViteRscApp(container = document, interceptEntries =
174
186
  // React reports that as #419 and client-renders the boundary from the
175
187
  // Flight payload, which is the intended path, not a fault to report.
176
188
  const message = String(error?.message ?? error);
177
- if (message.includes("419") || message.includes("did not finish this Suspense boundary")) {
189
+ if (message.includes("419") ||
190
+ message.includes("did not finish this Suspense boundary")) {
178
191
  return;
179
192
  }
193
+ // A stored page whose query string was read under a boundary carries
194
+ // the fallback there, and React reports the recovery on hydration. It is
195
+ // reported here too - the boundary is the developer's to move - but in
196
+ // its own words: in production React sends only the digest, and the
197
+ // generic message says nothing about which read, or where.
198
+ if (error?.digest === SEARCH_PARAMS_FALLBACK &&
199
+ !message.includes("useSearchParams()")) {
200
+ const hint = "useSearchParams() was read while rendering on the server, where there is no query string to give. " +
201
+ "The fallback was stored and the real value rendered here. Wrap the component in <Suspense> " +
202
+ "- or add a loading.tsx beside the page - closer to the read, so less of the page waits for it.";
203
+ showDevNotice(hint, errorInfo?.componentStack);
204
+ console.error(new Error(hint), errorInfo);
205
+ return;
206
+ }
207
+ // In development React's message carries the server's own text; the
208
+ // page shows it too, for whoever is looking at the page and not the
209
+ // console.
210
+ if (message.includes("useSearchParams()")) {
211
+ showDevNotice(message.replace(/^.*?(?=useSearchParams\(\))/s, ""), errorInfo?.componentStack);
212
+ }
213
+ console.error(error, errorInfo);
214
+ },
215
+ // A chunk the deploy no longer serves is not a fault in the page; the
216
+ // document is loaded again, and the new names come with it. Anything
217
+ // else is reported the way React would have.
218
+ onUncaughtError(error, errorInfo) {
219
+ if (recoverFromStaleAssets(error))
220
+ return;
221
+ console.error(error, errorInfo);
222
+ },
223
+ onCaughtError(error, errorInfo) {
224
+ if (recoverFromStaleAssets(error))
225
+ return;
180
226
  console.error(error, errorInfo);
181
227
  },
182
228
  });
@@ -1 +1 @@
1
- {"version":3,"file":"createViteRscApp.js","sourceRoot":"","sources":["../../src/js/createViteRscApp.ts"],"names":[],"mappings":"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","sourcesContent":["// Vite-engine client bootstrap. Uses @vitejs/plugin-rsc's browser runtime as\n// the Flight deserializer + action encoder, and drives the package's engine-agnostic\n// navigate.ts SPA engine (Link, prefetch, popstate) through it. This replaces\n// the bun engine's createRscApp + the hand-rolled webpack shim — the plugin\n// resolves client references itself.\nimport type { Href } from \"../routes.js\";\nimport { isSafeRedirect } from \"../safeUrl.js\";\nimport { createFromReadableStream, encodeReply, setServerCallback } from \"@vitejs/plugin-rsc/browser\";\nimport { createElement } from \"react\";\nimport { hydrateRoot } from \"react-dom/client\";\nimport { ActivityRoot } from \"./ActivityRouter\";\nimport { ServerRedirectError, throwForFailedAction } from \"./errors\";\nimport { fetchPagePayload } from \"./pagePayload\";\nimport { claimRead, setQueryCodec } from \"./queryClient\";\nimport { clearSegments, restoreSegments, setSegment } from \"./segmentStore\";\nimport type { ReactNode } from \"react\";\nimport {\n cancelPrefetch,\n isPrefetched,\n navigate,\n refresh,\n applyRevalidated,\n prefetch,\n retentionKey,\n setCallServer,\n setHeldLayouts,\n setStaticPayloads,\n setStaticRoutes,\n seedStaticChain,\n payloadUrl,\n setDeserializer,\n setInterceptManifest,\n setNavigateHandler,\n setRestoreHandler,\n setVersion,\n} from \"./navigate\";\n\nexport async function createViteRscApp(\n container: Document | Element = document,\n interceptEntries: { urlPattern: string; slot: string }[] = [],\n options: { staticPayloads?: string | null; routes?: unknown[] | null } = {},\n): Promise<void> {\n // An exported build has no server to negotiate with, so payloads live at\n // their own urls rather than behind a header on the page's url.\n setStaticPayloads(options.staticPayloads ?? null);\n // Only an exported build ships this: with no server to negotiate with, the\n // client works out for itself how much of a page it still holds.\n if (options.routes) setStaticRoutes(options.routes as never);\n // The router has to recognise an intercepted link before it asks the server,\n // so the patterns are baked into the generated browser entry. Without them\n // every intercepted route falls through to a full-page navigation.\n setInterceptManifest(interceptEntries);\n\n async function callServer(id: string, args: unknown[]): Promise<unknown> {\n // A read opened a slot before calling the reference, and this is the first\n // point at which the id exists on the client — React keeps it private and\n // hands it over only here. Claiming the slot enqueues the read for the\n // batched GET instead of posting it.\n const read = claimRead(id, args);\n\n if (read) return await read;\n\n const encoded = await encodeReply(args);\n\n // encodeReply returns FormData as soon as an argument contains a File.\n // Sending that as multipart would make PHP consume php://input while\n // parsing it, leaving the action with an empty body — so serialize it to\n // raw bytes under an opaque content-type and send the real one in\n // X-RSC-Content-Type for the worker to rebuild FormData from.\n let body: BodyInit;\n let realContentType: string;\n\n if (encoded instanceof FormData) {\n const serialized = new Response(encoded);\n body = await serialized.arrayBuffer();\n realContentType = serialized.headers.get(\"content-type\") ?? \"multipart/form-data\";\n } else {\n body = encoded as BodyInit;\n realContentType = \"text/plain;charset=UTF-8\";\n }\n\n const res = await fetch(\"/_rsc/action\", {\n method: \"POST\",\n headers: {\n \"X-RSC-Action\": id,\n // Where the action was invoked from. The host resolves it to the\n // components that render the page, so anything the action says it\n // invalidated can come back with the answer instead of being fetched\n // afterwards.\n \"X-RSC-Referer\": window.location.pathname + window.location.search,\n \"X-RSC-Content-Type\": realContentType,\n \"Content-Type\": \"application/octet-stream\",\n \"X-XSRF-TOKEN\": decodeURIComponent(\n document.cookie.match(/XSRF-TOKEN=([^;]+)/)?.[1] ?? \"\",\n ),\n },\n body,\n });\n\n // A failed action does not answer with a Flight stream, and the decoder\n // cannot tell — it reports its own parse failure instead of what the\n // server said.\n try {\n await throwForFailedAction(res);\n } catch (err) {\n // A redirect is an instruction, not something for a form to display:\n // an expired session answers this way, and the destination is the login\n // page rather than a message about one.\n if (err instanceof ServerRedirectError) {\n // The destination came off a response header, so it is checked here\n // too — the engine refuses these at the source, but a proxy or a host\n // in front of it can write whatever it likes.\n if (isSafeRedirect(err.location)) window.location.href = err.location;\n }\n\n throw err;\n }\n\n const answer = await createFromReadableStream(res.body!, { callServer });\n\n return unwrapRevalidated(answer);\n }\n\n /**\n * Put anything the action re-rendered on screen, and hand back its result.\n *\n * The trees travel with the answer rather than being fetched afterwards, so\n * the caller sees only what its action returned and never knows the page\n * was updated around it.\n */\n function unwrapRevalidated(answer: unknown): unknown {\n if (answer === null || typeof answer !== \"object\" || !(\"__rscRevalidated\" in answer)) {\n return answer;\n }\n\n const envelope = answer as { __rscRevalidated: Record<string, unknown>; result: unknown };\n\n for (const [target, tree] of Object.entries(envelope.__rscRevalidated)) {\n applyRevalidated(target, tree);\n }\n\n return envelope.result;\n }\n\n setDeserializer(createFromReadableStream as never);\n // The query transport speaks Flight too, and reaches the runtime through here\n // rather than importing it: a second import of the browser runtime from the\n // client-component graph is a second client-reference registry, and the\n // symptom is components that render as undefined with nothing logged.\n setQueryCodec({\n deserialize: (stream) =>\n createFromReadableStream(stream, { callServer }) as Promise<unknown>,\n encode: (args) => encodeReply(args) as Promise<string | FormData>,\n // The way back when a read cannot ride in a url. Re-entering callServer is\n // safe: the slot was cleared the moment the reference returned, so this\n // takes the ordinary POST path rather than claiming a read again.\n asAction: (id, args) => callServer(id, args),\n });\n setCallServer(callServer);\n // The plugin's own \"use server\" client stubs route through its registered\n // server callback — register the same transport there too.\n setServerCallback(callServer as never);\n\n // Link / Form / router live in a separate build graph and reach the SPA\n // engine through these globals.\n (window as unknown as { __rsc_navigate: typeof navigate }).__rsc_navigate = navigate;\n (window as unknown as { __rsc_prefetch: typeof prefetch }).__rsc_prefetch = prefetch;\n (window as unknown as { __rsc_cancel_prefetch: typeof cancelPrefetch }).__rsc_cancel_prefetch =\n cancelPrefetch;\n (window as unknown as { __rsc_is_prefetched: typeof isPrefetched }).__rsc_is_prefetched =\n isPrefetched;\n (window as unknown as { __rsc_refresh: typeof refresh }).__rsc_refresh = refresh;\n\n // Hydrate from the RSC endpoint (same url + X-RSC, no version header).\n //\n // Everything from here to the decode is failure handling, because this is\n // the one request with nothing watching it. A page whose shell is already\n // rendered looks fine while this fails, and its fallbacks stay on screen\n // indefinitely with nothing reported anywhere.\n const res = await fetchPagePayload(payloadUrl(window.location.href));\n\n // Seed the SPA engine with the build this page was served from, so a\n // redeploy mid-session is caught on the next navigation. This matters most\n // behind a CDN, where the shell may be cached from an older build.\n const servedVersion = res.headers.get(\"X-RSC-Version\");\n\n if (servedVersion) {\n setVersion(servedVersion);\n }\n\n // The chain this page is built from, so the next navigation can say what is\n // already mounted and be sent only what changed.\n // A server says what this page is built from; a file server says nothing,\n // so an exported build works it out from the table it was given.\n const servedLayouts = res.headers.get(\"X-RSC-Layouts\");\n\n if (servedLayouts !== null) {\n setHeldLayouts(servedLayouts.split(\",\"));\n } else if (!seedStaticChain(window.location.href)) {\n setHeldLayouts([]);\n }\n const tree = await createFromReadableStream(res.body!, { callServer });\n\n // Retaining the previous page behind <Activity> needs a wrapper above the\n // page, and React will not hydrate a *document* container through one: the\n // root child of a document has to be <html>, and wrapping it hangs the\n // renderer outright (React 19.2.7). An app whose root layout owns <html>\n // therefore hydrates the tree directly and navigations replace it, as before.\n //\n // Apps that hydrate into an element get retention. Giving it to document-\n // rooted apps means SPA navigation returning only the changed segment rather\n // than a whole document, which is an engine change, not a client one.\n const retains = container !== document;\n\n const shell = retains\n ? createElement(ActivityRoot, {\n initialKey: retentionKey(window.location.href, null),\n initialTree: tree as ReactNode,\n })\n : (tree as ReactNode);\n\n const root = hydrateRoot(container, shell, {\n onRecoverableError(error: unknown, errorInfo: unknown) {\n // A PPR shell is served with its Suspense boundaries deliberately\n // unfinished — the build aborts the render once the static part is out.\n // React reports that as #419 and client-renders the boundary from the\n // Flight payload, which is the intended path, not a fault to report.\n const message = String((error as { message?: string })?.message ?? error);\n\n if (message.includes(\"419\") || message.includes(\"did not finish this Suspense boundary\")) {\n return;\n }\n\n console.error(error, errorInfo);\n },\n });\n\n // Depth 0 is a whole document and replaces the root. Anything deeper is one\n // segment: handing it to the boundary at that depth leaves the layouts above\n // it mounted, which is the point of asking for a partial render at all.\n setNavigateHandler((tree: unknown, key: string, segmentDepth: number) => {\n const newTree = tree as ReactNode;\n\n if (segmentDepth > 0) {\n setSegment(segmentDepth, key, newTree);\n\n return;\n }\n\n clearSegments();\n root.render(newTree);\n });\n\n // Back and forward reveal a page the boundaries are still holding, with the\n // form you were filling in still filled in, and without asking the server.\n setRestoreHandler((key: string, maxAge?: number) => restoreSegments(key, maxAge));\n\n window.addEventListener(\"popstate\", () => {\n // restore: back and forward reveal the page you were on, with its state.\n // Wherever the browser just went; not a literal this app wrote.\n navigate(window.location.href as Href, { replace: true, restore: true });\n });\n\n history.replaceState({ rscUrl: window.location.href }, \"\", window.location.href);\n}\n"]}
1
+ {"version":3,"file":"createViteRscApp.js","sourceRoot":"","sources":["../../src/js/createViteRscApp.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,qFAAqF;AACrF,8EAA8E;AAC9E,4EAA4E;AAC5E,qCAAqC;AACrC,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EACL,wBAAwB,EACxB,WAAW,EACX,iBAAiB,GAClB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AACrE,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE5E,OAAO,EACL,cAAc,EACd,YAAY,EACZ,QAAQ,EACR,OAAO,EACP,gBAAgB,EAChB,QAAQ,EACR,YAAY,EACZ,aAAa,EACb,cAAc,EACd,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,UAAU,EACV,eAAe,EACf,oBAAoB,EACpB,kBAAkB,EAClB,iBAAiB,EACjB,UAAU,GACX,MAAM,YAAY,CAAC;AAEpB,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,SAAS,GAAuB,QAAQ,EACxC,gBAAgB,GAA2C,EAAE,EAC7D,OAAO,GAAkE,EAAE;IAE3E,yEAAyE;IACzE,gEAAgE;IAChE,iBAAiB,CAAC,OAAO,CAAC,cAAc,IAAI,IAAI,CAAC,CAAC;IAClD,2EAA2E;IAC3E,iEAAiE;IACjE,IAAI,OAAO,CAAC,MAAM;QAAE,eAAe,CAAC,OAAO,CAAC,MAAe,CAAC,CAAC;IAC7D,6EAA6E;IAC7E,2EAA2E;IAC3E,mEAAmE;IACnE,oBAAoB,CAAC,gBAAgB,CAAC,CAAC;IAEvC,KAAK,UAAU,UAAU,CAAC,EAAU,EAAE,IAAe;QACnD,2EAA2E;QAC3E,0EAA0E;QAC1E,uEAAuE;QACvE,qCAAqC;QACrC,MAAM,IAAI,GAAG,SAAS,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;QAEjC,IAAI,IAAI;YAAE,OAAO,MAAM,IAAI,CAAC;QAE5B,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,IAAI,CAAC,CAAC;QAExC,uEAAuE;QACvE,qEAAqE;QACrE,yEAAyE;QACzE,kEAAkE;QAClE,8DAA8D;QAC9D,IAAI,IAAc,CAAC;QACnB,IAAI,eAAuB,CAAC;QAE5B,IAAI,OAAO,YAAY,QAAQ,EAAE,CAAC;YAChC,MAAM,UAAU,GAAG,IAAI,QAAQ,CAAC,OAAO,CAAC,CAAC;YACzC,IAAI,GAAG,MAAM,UAAU,CAAC,WAAW,EAAE,CAAC;YACtC,eAAe;gBACb,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,qBAAqB,CAAC;QACpE,CAAC;aAAM,CAAC;YACN,IAAI,GAAG,OAAmB,CAAC;YAC3B,eAAe,GAAG,0BAA0B,CAAC;QAC/C,CAAC;QAED,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,cAAc,EAAE;YACtC,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,EAAE;gBAClB,iEAAiE;gBACjE,kEAAkE;gBAClE,qEAAqE;gBACrE,cAAc;gBACd,eAAe,EAAE,MAAM,CAAC,QAAQ,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM;gBAClE,oBAAoB,EAAE,eAAe;gBACrC,cAAc,EAAE,0BAA0B;gBAC1C,cAAc,EAAE,kBAAkB,CAChC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,oBAAoB,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CACvD;aACF;YACD,IAAI;SACL,CAAC,CAAC;QAEH,wEAAwE;QACxE,qEAAqE;QACrE,eAAe;QACf,IAAI,CAAC;YACH,MAAM,oBAAoB,CAAC,GAAG,CAAC,CAAC;QAClC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,qEAAqE;YACrE,wEAAwE;YACxE,wCAAwC;YACxC,IAAI,GAAG,YAAY,mBAAmB,EAAE,CAAC;gBACvC,oEAAoE;gBACpE,sEAAsE;gBACtE,8CAA8C;gBAC9C,IAAI,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC;oBAAE,MAAM,CAAC,QAAQ,CAAC,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC;YACxE,CAAC;YAED,MAAM,GAAG,CAAC;QACZ,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,wBAAwB,CAAC,GAAG,CAAC,IAAK,EAAE,EAAE,UAAU,EAAE,CAAC,CAAC;QAEzE,OAAO,iBAAiB,CAAC,MAAM,CAAC,CAAC;IACnC,CAAC;IAED;;;;;;OAMG;IACH,SAAS,iBAAiB,CAAC,MAAe;QACxC,IACE,MAAM,KAAK,IAAI;YACf,OAAO,MAAM,KAAK,QAAQ;YAC1B,CAAC,CAAC,kBAAkB,IAAI,MAAM,CAAC,EAC/B,CAAC;YACD,OAAO,MAAM,CAAC;QAChB,CAAC;QAED,MAAM,QAAQ,GAAG,MAGhB,CAAC;QAEF,KAAK,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,CAAC;YACvE,gBAAgB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACjC,CAAC;QAED,OAAO,QAAQ,CAAC,MAAM,CAAC;IACzB,CAAC;IAED,eAAe,CAAC,wBAAiC,CAAC,CAAC;IACnD,8EAA8E;IAC9E,4EAA4E;IAC5E,wEAAwE;IACxE,sEAAsE;IACtE,aAAa,CAAC;QACZ,WAAW,EAAE,CAAC,MAAM,EAAE,EAAE,CACtB,wBAAwB,CAAC,MAAM,EAAE,EAAE,UAAU,EAAE,CAAqB;QACtE,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,CAA+B;QACjE,2EAA2E;QAC3E,wEAAwE;QACxE,kEAAkE;QAClE,QAAQ,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,IAAI,CAAC;KAC7C,CAAC,CAAC;IACH,aAAa,CAAC,UAAU,CAAC,CAAC;IAC1B,0EAA0E;IAC1E,2DAA2D;IAC3D,iBAAiB,CAAC,UAAmB,CAAC,CAAC;IAEvC,wEAAwE;IACxE,gCAAgC;IAC/B,MAAyD,CAAC,cAAc;QACvE,QAAQ,CAAC;IACV,MAAyD,CAAC,cAAc;QACvE,QAAQ,CAAC;IAET,MACD,CAAC,qBAAqB,GAAG,cAAc,CAAC;IAEvC,MACD,CAAC,mBAAmB,GAAG,YAAY,CAAC;IACpC,MAAuD,CAAC,aAAa;QACpE,OAAO,CAAC;IAEV,uEAAuE;IACvE,EAAE;IACF,0EAA0E;IAC1E,0EAA0E;IAC1E,yEAAyE;IACzE,+CAA+C;IAC/C,MAAM,GAAG,GAAG,MAAM,gBAAgB,CAAC,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;IAErE,qEAAqE;IACrE,2EAA2E;IAC3E,mEAAmE;IACnE,MAAM,aAAa,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IAEvD,IAAI,aAAa,EAAE,CAAC;QAClB,UAAU,CAAC,aAAa,CAAC,CAAC;IAC5B,CAAC;IAED,4EAA4E;IAC5E,iDAAiD;IACjD,0EAA0E;IAC1E,iEAAiE;IACjE,MAAM,aAAa,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IAEvD,IAAI,aAAa,KAAK,IAAI,EAAE,CAAC;QAC3B,cAAc,CAAC,aAAa,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3C,CAAC;SAAM,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QAClD,cAAc,CAAC,EAAE,CAAC,CAAC;IACrB,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,wBAAwB,CAAC,GAAG,CAAC,IAAK,EAAE,EAAE,UAAU,EAAE,CAAC,CAAC;IAEvE,0EAA0E;IAC1E,2EAA2E;IAC3E,uEAAuE;IACvE,yEAAyE;IACzE,8EAA8E;IAC9E,EAAE;IACF,0EAA0E;IAC1E,6EAA6E;IAC7E,sEAAsE;IACtE,MAAM,OAAO,GAAG,SAAS,KAAK,QAAQ,CAAC;IAEvC,MAAM,KAAK,GAAG,OAAO;QACnB,CAAC,CAAC,aAAa,CAAC,YAAY,EAAE;YAC1B,UAAU,EAAE,YAAY,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;YACpD,WAAW,EAAE,IAAiB;SAC/B,CAAC;QACJ,CAAC,CAAE,IAAkB,CAAC;IAExB,MAAM,IAAI,GAAG,WAAW,CAAC,SAAS,EAAE,KAAK,EAAE;QACzC,kBAAkB,CAAC,KAAc,EAAE,SAAkB;YACnD,kEAAkE;YAClE,wEAAwE;YACxE,sEAAsE;YACtE,qEAAqE;YACrE,MAAM,OAAO,GAAG,MAAM,CAAE,KAA8B,EAAE,OAAO,IAAI,KAAK,CAAC,CAAC;YAE1E,IACE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC;gBACvB,OAAO,CAAC,QAAQ,CAAC,uCAAuC,CAAC,EACzD,CAAC;gBACD,OAAO;YACT,CAAC;YACD,qEAAqE;YACrE,yEAAyE;YACzE,uEAAuE;YACvE,oEAAoE;YACpE,2DAA2D;YAC3D,IACG,KAA6B,EAAE,MAAM,KAAK,sBAAsB;gBACjE,CAAC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAC,EACtC,CAAC;gBACD,MAAM,IAAI,GACR,oGAAoG;oBACpG,6FAA6F;oBAC7F,gGAAgG,CAAC;gBAEnG,aAAa,CACX,IAAI,EACH,SAAgD,EAAE,cAAc,CAClE,CAAC;gBACF,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,EAAE,SAAS,CAAC,CAAC;gBAE1C,OAAO;YACT,CAAC;YAED,oEAAoE;YACpE,oEAAoE;YACpE,WAAW;YACX,IAAI,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAC,EAAE,CAAC;gBAC1C,aAAa,CACX,OAAO,CAAC,OAAO,CAAC,8BAA8B,EAAE,EAAE,CAAC,EAClD,SAAgD,EAAE,cAAc,CAClE,CAAC;YACJ,CAAC;YAED,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;QAClC,CAAC;QACD,sEAAsE;QACtE,qEAAqE;QACrE,6CAA6C;QAC7C,eAAe,CAAC,KAAc,EAAE,SAAkB;YAChD,IAAI,sBAAsB,CAAC,KAAK,CAAC;gBAAE,OAAO;YAE1C,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;QAClC,CAAC;QACD,aAAa,CAAC,KAAc,EAAE,SAAkB;YAC9C,IAAI,sBAAsB,CAAC,KAAK,CAAC;gBAAE,OAAO;YAE1C,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;QAClC,CAAC;KACF,CAAC,CAAC;IAEH,4EAA4E;IAC5E,6EAA6E;IAC7E,wEAAwE;IACxE,kBAAkB,CAAC,CAAC,IAAa,EAAE,GAAW,EAAE,YAAoB,EAAE,EAAE;QACtE,MAAM,OAAO,GAAG,IAAiB,CAAC;QAElC,IAAI,YAAY,GAAG,CAAC,EAAE,CAAC;YACrB,UAAU,CAAC,YAAY,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC;YAEvC,OAAO;QACT,CAAC;QAED,aAAa,EAAE,CAAC;QAChB,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACvB,CAAC,CAAC,CAAC;IAEH,4EAA4E;IAC5E,2EAA2E;IAC3E,iBAAiB,CAAC,CAAC,GAAW,EAAE,MAAe,EAAE,EAAE,CACjD,eAAe,CAAC,GAAG,EAAE,MAAM,CAAC,CAC7B,CAAC;IAEF,MAAM,CAAC,gBAAgB,CAAC,UAAU,EAAE,GAAG,EAAE;QACvC,yEAAyE;QACzE,gEAAgE;QAChE,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAY,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3E,CAAC,CAAC,CAAC;IAEH,OAAO,CAAC,YAAY,CAClB,EAAE,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,EAChC,EAAE,EACF,MAAM,CAAC,QAAQ,CAAC,IAAI,CACrB,CAAC;AACJ,CAAC","sourcesContent":["// Vite-engine client bootstrap. Uses @vitejs/plugin-rsc's browser runtime as\n// the Flight deserializer + action encoder, and drives the package's engine-agnostic\n// navigate.ts SPA engine (Link, prefetch, popstate) through it. This replaces\n// the bun engine's createRscApp + the hand-rolled webpack shim — the plugin\n// resolves client references itself.\nimport { SEARCH_PARAMS_FALLBACK } from \"./useSearchParams\";\nimport { recoverFromStaleAssets } from \"./staleAssets\";\nimport { showDevNotice } from \"./devNotice\";\nimport type { Href } from \"../routes.js\";\nimport { isSafeRedirect } from \"../safeUrl.js\";\nimport {\n createFromReadableStream,\n encodeReply,\n setServerCallback,\n} from \"@vitejs/plugin-rsc/browser\";\nimport { createElement } from \"react\";\nimport { hydrateRoot } from \"react-dom/client\";\nimport { ActivityRoot } from \"./ActivityRouter\";\nimport { ServerRedirectError, throwForFailedAction } from \"./errors\";\nimport { fetchPagePayload } from \"./pagePayload\";\nimport { claimRead, setQueryCodec } from \"./queryClient\";\nimport { clearSegments, restoreSegments, setSegment } from \"./segmentStore\";\nimport type { ReactNode } from \"react\";\nimport {\n cancelPrefetch,\n isPrefetched,\n navigate,\n refresh,\n applyRevalidated,\n prefetch,\n retentionKey,\n setCallServer,\n setHeldLayouts,\n setStaticPayloads,\n setStaticRoutes,\n seedStaticChain,\n payloadUrl,\n setDeserializer,\n setInterceptManifest,\n setNavigateHandler,\n setRestoreHandler,\n setVersion,\n} from \"./navigate\";\n\nexport async function createViteRscApp(\n container: Document | Element = document,\n interceptEntries: { urlPattern: string; slot: string }[] = [],\n options: { staticPayloads?: string | null; routes?: unknown[] | null } = {},\n): Promise<void> {\n // An exported build has no server to negotiate with, so payloads live at\n // their own urls rather than behind a header on the page's url.\n setStaticPayloads(options.staticPayloads ?? null);\n // Only an exported build ships this: with no server to negotiate with, the\n // client works out for itself how much of a page it still holds.\n if (options.routes) setStaticRoutes(options.routes as never);\n // The router has to recognise an intercepted link before it asks the server,\n // so the patterns are baked into the generated browser entry. Without them\n // every intercepted route falls through to a full-page navigation.\n setInterceptManifest(interceptEntries);\n\n async function callServer(id: string, args: unknown[]): Promise<unknown> {\n // A read opened a slot before calling the reference, and this is the first\n // point at which the id exists on the client — React keeps it private and\n // hands it over only here. Claiming the slot enqueues the read for the\n // batched GET instead of posting it.\n const read = claimRead(id, args);\n\n if (read) return await read;\n\n const encoded = await encodeReply(args);\n\n // encodeReply returns FormData as soon as an argument contains a File.\n // Sending that as multipart would make PHP consume php://input while\n // parsing it, leaving the action with an empty body — so serialize it to\n // raw bytes under an opaque content-type and send the real one in\n // X-RSC-Content-Type for the worker to rebuild FormData from.\n let body: BodyInit;\n let realContentType: string;\n\n if (encoded instanceof FormData) {\n const serialized = new Response(encoded);\n body = await serialized.arrayBuffer();\n realContentType =\n serialized.headers.get(\"content-type\") ?? \"multipart/form-data\";\n } else {\n body = encoded as BodyInit;\n realContentType = \"text/plain;charset=UTF-8\";\n }\n\n const res = await fetch(\"/_rsc/action\", {\n method: \"POST\",\n headers: {\n \"X-RSC-Action\": id,\n // Where the action was invoked from. The host resolves it to the\n // components that render the page, so anything the action says it\n // invalidated can come back with the answer instead of being fetched\n // afterwards.\n \"X-RSC-Referer\": window.location.pathname + window.location.search,\n \"X-RSC-Content-Type\": realContentType,\n \"Content-Type\": \"application/octet-stream\",\n \"X-XSRF-TOKEN\": decodeURIComponent(\n document.cookie.match(/XSRF-TOKEN=([^;]+)/)?.[1] ?? \"\",\n ),\n },\n body,\n });\n\n // A failed action does not answer with a Flight stream, and the decoder\n // cannot tell — it reports its own parse failure instead of what the\n // server said.\n try {\n await throwForFailedAction(res);\n } catch (err) {\n // A redirect is an instruction, not something for a form to display:\n // an expired session answers this way, and the destination is the login\n // page rather than a message about one.\n if (err instanceof ServerRedirectError) {\n // The destination came off a response header, so it is checked here\n // too — the engine refuses these at the source, but a proxy or a host\n // in front of it can write whatever it likes.\n if (isSafeRedirect(err.location)) window.location.href = err.location;\n }\n\n throw err;\n }\n\n const answer = await createFromReadableStream(res.body!, { callServer });\n\n return unwrapRevalidated(answer);\n }\n\n /**\n * Put anything the action re-rendered on screen, and hand back its result.\n *\n * The trees travel with the answer rather than being fetched afterwards, so\n * the caller sees only what its action returned and never knows the page\n * was updated around it.\n */\n function unwrapRevalidated(answer: unknown): unknown {\n if (\n answer === null ||\n typeof answer !== \"object\" ||\n !(\"__rscRevalidated\" in answer)\n ) {\n return answer;\n }\n\n const envelope = answer as {\n __rscRevalidated: Record<string, unknown>;\n result: unknown;\n };\n\n for (const [target, tree] of Object.entries(envelope.__rscRevalidated)) {\n applyRevalidated(target, tree);\n }\n\n return envelope.result;\n }\n\n setDeserializer(createFromReadableStream as never);\n // The query transport speaks Flight too, and reaches the runtime through here\n // rather than importing it: a second import of the browser runtime from the\n // client-component graph is a second client-reference registry, and the\n // symptom is components that render as undefined with nothing logged.\n setQueryCodec({\n deserialize: (stream) =>\n createFromReadableStream(stream, { callServer }) as Promise<unknown>,\n encode: (args) => encodeReply(args) as Promise<string | FormData>,\n // The way back when a read cannot ride in a url. Re-entering callServer is\n // safe: the slot was cleared the moment the reference returned, so this\n // takes the ordinary POST path rather than claiming a read again.\n asAction: (id, args) => callServer(id, args),\n });\n setCallServer(callServer);\n // The plugin's own \"use server\" client stubs route through its registered\n // server callback — register the same transport there too.\n setServerCallback(callServer as never);\n\n // Link / Form / router live in a separate build graph and reach the SPA\n // engine through these globals.\n (window as unknown as { __rsc_navigate: typeof navigate }).__rsc_navigate =\n navigate;\n (window as unknown as { __rsc_prefetch: typeof prefetch }).__rsc_prefetch =\n prefetch;\n (\n window as unknown as { __rsc_cancel_prefetch: typeof cancelPrefetch }\n ).__rsc_cancel_prefetch = cancelPrefetch;\n (\n window as unknown as { __rsc_is_prefetched: typeof isPrefetched }\n ).__rsc_is_prefetched = isPrefetched;\n (window as unknown as { __rsc_refresh: typeof refresh }).__rsc_refresh =\n refresh;\n\n // Hydrate from the RSC endpoint (same url + X-RSC, no version header).\n //\n // Everything from here to the decode is failure handling, because this is\n // the one request with nothing watching it. A page whose shell is already\n // rendered looks fine while this fails, and its fallbacks stay on screen\n // indefinitely with nothing reported anywhere.\n const res = await fetchPagePayload(payloadUrl(window.location.href));\n\n // Seed the SPA engine with the build this page was served from, so a\n // redeploy mid-session is caught on the next navigation. This matters most\n // behind a CDN, where the shell may be cached from an older build.\n const servedVersion = res.headers.get(\"X-RSC-Version\");\n\n if (servedVersion) {\n setVersion(servedVersion);\n }\n\n // The chain this page is built from, so the next navigation can say what is\n // already mounted and be sent only what changed.\n // A server says what this page is built from; a file server says nothing,\n // so an exported build works it out from the table it was given.\n const servedLayouts = res.headers.get(\"X-RSC-Layouts\");\n\n if (servedLayouts !== null) {\n setHeldLayouts(servedLayouts.split(\",\"));\n } else if (!seedStaticChain(window.location.href)) {\n setHeldLayouts([]);\n }\n const tree = await createFromReadableStream(res.body!, { callServer });\n\n // Retaining the previous page behind <Activity> needs a wrapper above the\n // page, and React will not hydrate a *document* container through one: the\n // root child of a document has to be <html>, and wrapping it hangs the\n // renderer outright (React 19.2.7). An app whose root layout owns <html>\n // therefore hydrates the tree directly and navigations replace it, as before.\n //\n // Apps that hydrate into an element get retention. Giving it to document-\n // rooted apps means SPA navigation returning only the changed segment rather\n // than a whole document, which is an engine change, not a client one.\n const retains = container !== document;\n\n const shell = retains\n ? createElement(ActivityRoot, {\n initialKey: retentionKey(window.location.href, null),\n initialTree: tree as ReactNode,\n })\n : (tree as ReactNode);\n\n const root = hydrateRoot(container, shell, {\n onRecoverableError(error: unknown, errorInfo: unknown) {\n // A PPR shell is served with its Suspense boundaries deliberately\n // unfinished — the build aborts the render once the static part is out.\n // React reports that as #419 and client-renders the boundary from the\n // Flight payload, which is the intended path, not a fault to report.\n const message = String((error as { message?: string })?.message ?? error);\n\n if (\n message.includes(\"419\") ||\n message.includes(\"did not finish this Suspense boundary\")\n ) {\n return;\n }\n // A stored page whose query string was read under a boundary carries\n // the fallback there, and React reports the recovery on hydration. It is\n // reported here too - the boundary is the developer's to move - but in\n // its own words: in production React sends only the digest, and the\n // generic message says nothing about which read, or where.\n if (\n (error as { digest?: string })?.digest === SEARCH_PARAMS_FALLBACK &&\n !message.includes(\"useSearchParams()\")\n ) {\n const hint =\n \"useSearchParams() was read while rendering on the server, where there is no query string to give. \" +\n \"The fallback was stored and the real value rendered here. Wrap the component in <Suspense> \" +\n \"- or add a loading.tsx beside the page - closer to the read, so less of the page waits for it.\";\n\n showDevNotice(\n hint,\n (errorInfo as { componentStack?: string } | null)?.componentStack,\n );\n console.error(new Error(hint), errorInfo);\n\n return;\n }\n\n // In development React's message carries the server's own text; the\n // page shows it too, for whoever is looking at the page and not the\n // console.\n if (message.includes(\"useSearchParams()\")) {\n showDevNotice(\n message.replace(/^.*?(?=useSearchParams\\(\\))/s, \"\"),\n (errorInfo as { componentStack?: string } | null)?.componentStack,\n );\n }\n\n console.error(error, errorInfo);\n },\n // A chunk the deploy no longer serves is not a fault in the page; the\n // document is loaded again, and the new names come with it. Anything\n // else is reported the way React would have.\n onUncaughtError(error: unknown, errorInfo: unknown) {\n if (recoverFromStaleAssets(error)) return;\n\n console.error(error, errorInfo);\n },\n onCaughtError(error: unknown, errorInfo: unknown) {\n if (recoverFromStaleAssets(error)) return;\n\n console.error(error, errorInfo);\n },\n });\n\n // Depth 0 is a whole document and replaces the root. Anything deeper is one\n // segment: handing it to the boundary at that depth leaves the layouts above\n // it mounted, which is the point of asking for a partial render at all.\n setNavigateHandler((tree: unknown, key: string, segmentDepth: number) => {\n const newTree = tree as ReactNode;\n\n if (segmentDepth > 0) {\n setSegment(segmentDepth, key, newTree);\n\n return;\n }\n\n clearSegments();\n root.render(newTree);\n });\n\n // Back and forward reveal a page the boundaries are still holding, with the\n // form you were filling in still filled in, and without asking the server.\n setRestoreHandler((key: string, maxAge?: number) =>\n restoreSegments(key, maxAge),\n );\n\n window.addEventListener(\"popstate\", () => {\n // restore: back and forward reveal the page you were on, with its state.\n // Wherever the browser just went; not a literal this app wrote.\n navigate(window.location.href as Href, { replace: true, restore: true });\n });\n\n history.replaceState(\n { rscUrl: window.location.href },\n \"\",\n window.location.href,\n );\n}\n"]}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * A line at the bottom of the page, in development only.
3
+ *
4
+ * The dev server prints the boundary the build will want, and the browser
5
+ * console reports it; the person looking at the page sees neither. Next.js
6
+ * shows a small badge for the same reason. This is that: fixed to the bottom
7
+ * of the viewport, the message and the component it came from, gone on a
8
+ * click. Nothing here reaches a production bundle - the DEV check is a
9
+ * constant at build time and the code behind it is dropped.
10
+ */
11
+ /** The first component in a React component stack: `at AuthTrigger (...)` → `AuthTrigger`. */
12
+ export declare function componentIn(componentStack: string | undefined | null): string | null;
13
+ export declare function showDevNotice(message: string, componentStack?: string | null): void;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * A line at the bottom of the page, in development only.
3
+ *
4
+ * The dev server prints the boundary the build will want, and the browser
5
+ * console reports it; the person looking at the page sees neither. Next.js
6
+ * shows a small badge for the same reason. This is that: fixed to the bottom
7
+ * of the viewport, the message and the component it came from, gone on a
8
+ * click. Nothing here reaches a production bundle - the DEV check is a
9
+ * constant at build time and the code behind it is dropped.
10
+ */
11
+ const ID = "rsc-kit-dev-notice";
12
+ /** The first component in a React component stack: `at AuthTrigger (...)` → `AuthTrigger`. */
13
+ export function componentIn(componentStack) {
14
+ const match = /at ([A-Z][\w$]*)/.exec(componentStack ?? "");
15
+ return match ? match[1] : null;
16
+ }
17
+ export function showDevNotice(message, componentStack) {
18
+ if (!import.meta.env?.DEV || typeof document === "undefined")
19
+ return;
20
+ const where = componentIn(componentStack);
21
+ const text = (where ? `${where}: ` : "") + message;
22
+ let box = document.getElementById(ID);
23
+ if (!box) {
24
+ box = document.createElement("div");
25
+ box.id = ID;
26
+ box.setAttribute("role", "status");
27
+ box.style.cssText =
28
+ "position:fixed;left:12px;right:12px;bottom:12px;z-index:2147483647;" +
29
+ "font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;color:#fff;background:#b91c1c;" +
30
+ "border-radius:6px;padding:10px 36px 10px 14px;box-shadow:0 4px 16px rgba(0,0,0,.35);" +
31
+ "max-height:40vh;overflow:auto;white-space:pre-wrap;cursor:pointer";
32
+ box.title = "rsc-kit · click to dismiss";
33
+ box.addEventListener("click", () => box?.remove());
34
+ document.body.appendChild(box);
35
+ }
36
+ // The same line twice is one problem, not two.
37
+ for (const line of Array.from(box.children)) {
38
+ if (line.textContent === text)
39
+ return;
40
+ }
41
+ const line = document.createElement("div");
42
+ line.textContent = text;
43
+ box.appendChild(line);
44
+ }
45
+ //# sourceMappingURL=devNotice.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devNotice.js","sourceRoot":"","sources":["../../src/js/devNotice.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,EAAE,GAAG,oBAAoB,CAAC;AAEhC,8FAA8F;AAC9F,MAAM,UAAU,WAAW,CACzB,cAAyC;IAEzC,MAAM,KAAK,GAAG,kBAAkB,CAAC,IAAI,CAAC,cAAc,IAAI,EAAE,CAAC,CAAC;IAE5D,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,aAAa,CAC3B,OAAe,EACf,cAA8B;IAE9B,IAAI,CAAC,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,OAAO,QAAQ,KAAK,WAAW;QAAE,OAAO;IAErE,MAAM,KAAK,GAAG,WAAW,CAAC,cAAc,CAAC,CAAC;IAC1C,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC;IAEnD,IAAI,GAAG,GAAG,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC;IAEtC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,GAAG,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QACpC,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC;QACZ,GAAG,CAAC,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QACnC,GAAG,CAAC,KAAK,CAAC,OAAO;YACf,qEAAqE;gBACrE,0FAA0F;gBAC1F,sFAAsF;gBACtF,mEAAmE,CAAC;QACtE,GAAG,CAAC,KAAK,GAAG,4BAA4B,CAAC;QACzC,GAAG,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,CAAC,CAAC;QACnD,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACjC,CAAC;IAED,+CAA+C;IAC/C,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC5C,IAAI,IAAI,CAAC,WAAW,KAAK,IAAI;YAAE,OAAO;IACxC,CAAC;IAED,MAAM,IAAI,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;IAE3C,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;IACxB,GAAG,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;AACxB,CAAC","sourcesContent":["/**\n * A line at the bottom of the page, in development only.\n *\n * The dev server prints the boundary the build will want, and the browser\n * console reports it; the person looking at the page sees neither. Next.js\n * shows a small badge for the same reason. This is that: fixed to the bottom\n * of the viewport, the message and the component it came from, gone on a\n * click. Nothing here reaches a production bundle - the DEV check is a\n * constant at build time and the code behind it is dropped.\n */\n\nconst ID = \"rsc-kit-dev-notice\";\n\n/** The first component in a React component stack: `at AuthTrigger (...)` → `AuthTrigger`. */\nexport function componentIn(\n componentStack: string | undefined | null,\n): string | null {\n const match = /at ([A-Z][\\w$]*)/.exec(componentStack ?? \"\");\n\n return match ? match[1]! : null;\n}\n\nexport function showDevNotice(\n message: string,\n componentStack?: string | null,\n): void {\n if (!import.meta.env?.DEV || typeof document === \"undefined\") return;\n\n const where = componentIn(componentStack);\n const text = (where ? `${where}: ` : \"\") + message;\n\n let box = document.getElementById(ID);\n\n if (!box) {\n box = document.createElement(\"div\");\n box.id = ID;\n box.setAttribute(\"role\", \"status\");\n box.style.cssText =\n \"position:fixed;left:12px;right:12px;bottom:12px;z-index:2147483647;\" +\n \"font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;color:#fff;background:#b91c1c;\" +\n \"border-radius:6px;padding:10px 36px 10px 14px;box-shadow:0 4px 16px rgba(0,0,0,.35);\" +\n \"max-height:40vh;overflow:auto;white-space:pre-wrap;cursor:pointer\";\n box.title = \"rsc-kit · click to dismiss\";\n box.addEventListener(\"click\", () => box?.remove());\n document.body.appendChild(box);\n }\n\n // The same line twice is one problem, not two.\n for (const line of Array.from(box.children)) {\n if (line.textContent === text) return;\n }\n\n const line = document.createElement(\"div\");\n\n line.textContent = text;\n box.appendChild(line);\n}\n"]}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * A navigation that lands on a chunk that is no longer there.
3
+ *
4
+ * After a deploy the open tab still holds the old page, and its next
5
+ * navigation asks for client components by the old hashed names, which the
6
+ * new deploy does not serve. The dev server does the same when it
7
+ * re-optimises dependencies and answers the old names with 504. Either way
8
+ * the payload arrives and React fails to load the module it names, on a
9
+ * page that was working a click ago.
10
+ *
11
+ * The fix is the one the browser would have applied: load the document again.
12
+ * Once — a second failure on the same url within a few seconds means the
13
+ * deploy is broken, not stale, and reloading forever would hide that.
14
+ */
15
+ export declare function isStaleAssetError(error: unknown): boolean;
16
+ /** True when the page is being reloaded for it; false when the error is something else, or reloading already failed. */
17
+ export declare function recoverFromStaleAssets(error: unknown): boolean;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * A navigation that lands on a chunk that is no longer there.
3
+ *
4
+ * After a deploy the open tab still holds the old page, and its next
5
+ * navigation asks for client components by the old hashed names, which the
6
+ * new deploy does not serve. The dev server does the same when it
7
+ * re-optimises dependencies and answers the old names with 504. Either way
8
+ * the payload arrives and React fails to load the module it names, on a
9
+ * page that was working a click ago.
10
+ *
11
+ * The fix is the one the browser would have applied: load the document again.
12
+ * Once — a second failure on the same url within a few seconds means the
13
+ * deploy is broken, not stale, and reloading forever would hide that.
14
+ */
15
+ const STALE_MODULE = /Failed to fetch dynamically imported module|Importing a module script failed|error loading dynamically imported module|Outdated Optimize Dep|Loading (?:CSS )?chunk/i;
16
+ const RELOADED = "rsc-kit:reloaded";
17
+ const WINDOW_MS = 10_000;
18
+ export function isStaleAssetError(error) {
19
+ const message = String(error?.message ?? error);
20
+ return STALE_MODULE.test(message);
21
+ }
22
+ /** True when the page is being reloaded for it; false when the error is something else, or reloading already failed. */
23
+ export function recoverFromStaleAssets(error) {
24
+ if (!isStaleAssetError(error) || typeof window === "undefined")
25
+ return false;
26
+ const mark = `${RELOADED}:${window.location.href}`;
27
+ try {
28
+ const last = Number(sessionStorage.getItem(mark) ?? 0);
29
+ if (Date.now() - last < WINDOW_MS)
30
+ return false;
31
+ sessionStorage.setItem(mark, String(Date.now()));
32
+ }
33
+ catch {
34
+ // No storage: reload anyway, once is the best that can be promised.
35
+ }
36
+ window.location.reload();
37
+ return true;
38
+ }
39
+ //# sourceMappingURL=staleAssets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"staleAssets.js","sourceRoot":"","sources":["../../src/js/staleAssets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,YAAY,GAChB,sKAAsK,CAAC;AAEzK,MAAM,QAAQ,GAAG,kBAAkB,CAAC;AACpC,MAAM,SAAS,GAAG,MAAM,CAAC;AAEzB,MAAM,UAAU,iBAAiB,CAAC,KAAc;IAC9C,MAAM,OAAO,GAAG,MAAM,CACnB,KAAqC,EAAE,OAAO,IAAI,KAAK,CACzD,CAAC;IAEF,OAAO,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AACpC,CAAC;AAED,wHAAwH;AACxH,MAAM,UAAU,sBAAsB,CAAC,KAAc;IACnD,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,IAAI,OAAO,MAAM,KAAK,WAAW;QAAE,OAAO,KAAK,CAAC;IAE7E,MAAM,IAAI,GAAG,GAAG,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;IAEnD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAEvD,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,SAAS;YAAE,OAAO,KAAK,CAAC;QAEhD,cAAc,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IACnD,CAAC;IAAC,MAAM,CAAC;QACP,oEAAoE;IACtE,CAAC;IAED,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;IAEzB,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["/**\n * A navigation that lands on a chunk that is no longer there.\n *\n * After a deploy the open tab still holds the old page, and its next\n * navigation asks for client components by the old hashed names, which the\n * new deploy does not serve. The dev server does the same when it\n * re-optimises dependencies and answers the old names with 504. Either way\n * the payload arrives and React fails to load the module it names, on a\n * page that was working a click ago.\n *\n * The fix is the one the browser would have applied: load the document again.\n * Once — a second failure on the same url within a few seconds means the\n * deploy is broken, not stale, and reloading forever would hide that.\n */\n\nconst STALE_MODULE =\n /Failed to fetch dynamically imported module|Importing a module script failed|error loading dynamically imported module|Outdated Optimize Dep|Loading (?:CSS )?chunk/i;\n\nconst RELOADED = \"rsc-kit:reloaded\";\nconst WINDOW_MS = 10_000;\n\nexport function isStaleAssetError(error: unknown): boolean {\n const message = String(\n (error as { message?: string } | null)?.message ?? error,\n );\n\n return STALE_MODULE.test(message);\n}\n\n/** True when the page is being reloaded for it; false when the error is something else, or reloading already failed. */\nexport function recoverFromStaleAssets(error: unknown): boolean {\n if (!isStaleAssetError(error) || typeof window === \"undefined\") return false;\n\n const mark = `${RELOADED}:${window.location.href}`;\n\n try {\n const last = Number(sessionStorage.getItem(mark) ?? 0);\n\n if (Date.now() - last < WINDOW_MS) return false;\n\n sessionStorage.setItem(mark, String(Date.now()));\n } catch {\n // No storage: reload anyway, once is the best that can be promised.\n }\n\n window.location.reload();\n\n return true;\n}\n"]}
@@ -1 +1,7 @@
1
+ /**
2
+ * The digest a stored page carries where the query was read under the
3
+ * boundary that caught it. The client recognises it on hydration and does
4
+ * not report the fallback as an error, because it is the designed path.
5
+ */
6
+ export declare const SEARCH_PARAMS_FALLBACK = "rsc-kit:search-params-fallback";
1
7
  export declare function useSearchParams(): URLSearchParams;
@@ -14,6 +14,11 @@
14
14
  * root, nothing paints, and the build refuses the route and says why — which
15
15
  * is the same answer it gives for reading the request too early on the server.
16
16
  *
17
+ * The dev server renders per request and could answer with that request's
18
+ * query string; it does not, on purpose. A page has one shape — a stored
19
+ * shell with the query read in the browser — and dev shows that shape, so a
20
+ * boundary missing in dev is the boundary the build will refuse.
21
+ *
17
22
  * Returning an empty URLSearchParams instead would be worse than either: the
18
23
  * page would be stored showing results for no query at all, and nothing would
19
24
  * say so.
@@ -56,10 +61,18 @@ function getSnapshot() {
56
61
  }
57
62
  return cached.params;
58
63
  }
64
+ /**
65
+ * The digest a stored page carries where the query was read under the
66
+ * boundary that caught it. The client recognises it on hydration and does
67
+ * not report the fallback as an error, because it is the designed path.
68
+ */
69
+ export const SEARCH_PARAMS_FALLBACK = "rsc-kit:search-params-fallback";
59
70
  function getServerSnapshot() {
60
- throw new Error("useSearchParams() was read while rendering on the server, where there is no query string. " +
61
- "Wrap the component in <Suspense> — or add a loading.tsx beside the page — so the fallback " +
62
- "is stored and the real value arrives in the browser.");
71
+ const error = new Error("useSearchParams() was read while rendering on the server, where there is no query string to give: " +
72
+ "a stored page would serve one visitor's query to everyone. Wrap the component in <Suspense> — or add a " +
73
+ "loading.tsx beside the page — so the fallback is stored and the real value arrives in the browser.");
74
+ error.digest = SEARCH_PARAMS_FALLBACK;
75
+ throw error;
63
76
  }
64
77
  export function useSearchParams() {
65
78
  return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
@@ -1 +1 @@
1
- {"version":3,"file":"useSearchParams.js","sourceRoot":"","sources":["../../src/js/useSearchParams.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAE7C,MAAM,SAAS,GAAG,IAAI,GAAG,EAAc,CAAC;AAExC;;;;;;GAMG;AACH,IAAI,MAAM,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,eAAe,EAAE,EAAE,CAAC;AAE3D,SAAS,aAAa;IACpB,OAAO,OAAO,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;AACrE,CAAC;AAED,SAAS,MAAM;IACb,SAAS,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;AAClC,CAAC;AAED,IAAI,SAAS,GAAG,KAAK,CAAC;AAEtB,SAAS,SAAS,CAAC,QAAoB;IACrC,0EAA0E;IAC1E,0EAA0E;IAC1E,yEAAyE;IACzE,yEAAyE;IACzE,yEAAyE;IACzE,IAAI,CAAC,SAAS,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;QAChD,MAAM,CAAC,gBAAgB,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC;QAChD,MAAM,CAAC,gBAAgB,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QAC5C,SAAS,GAAG,IAAI,CAAC;IACnB,CAAC;IAED,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAExB,OAAO,GAAG,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;AAC1C,CAAC;AAED,SAAS,WAAW;IAClB,MAAM,MAAM,GAAG,aAAa,EAAE,CAAC;IAE/B,IAAI,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,CAAC;QAC7B,MAAM,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,eAAe,CAAC,MAAM,CAAC,EAAE,CAAC;IAC3D,CAAC;IAED,OAAO,MAAM,CAAC,MAAM,CAAC;AACvB,CAAC;AAED,SAAS,iBAAiB;IACxB,MAAM,IAAI,KAAK,CACb,4FAA4F;QAC1F,4FAA4F;QAC5F,sDAAsD,CACzD,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,OAAO,oBAAoB,CAAC,SAAS,EAAE,WAAW,EAAE,iBAAiB,CAAC,CAAC;AACzE,CAAC","sourcesContent":["\"use client\";\n\n/**\n * The query string, live across navigations.\n *\n * Deliberately unlike usePathname, which answers with the url being rendered.\n * A pathname is fixed for a given stored page; a query string is not — the\n * same route is asked for with `?q=shoes` and `?q=hats`, and a page stored\n * holding one of them would serve it to everyone.\n *\n * So there is no server snapshot to give, and this throws rather than\n * inventing one. React treats an error thrown during SSR as recoverable at the\n * nearest Suspense boundary: the fallback is what gets stored, and the client\n * renders the real thing on hydration. Without a boundary the error reaches the\n * root, nothing paints, and the build refuses the route and says why — which\n * is the same answer it gives for reading the request too early on the server.\n *\n * Returning an empty URLSearchParams instead would be worse than either: the\n * page would be stored showing results for no query at all, and nothing would\n * say so.\n */\n\nimport { useSyncExternalStore } from \"react\";\n\nconst listeners = new Set<() => void>();\n\n/**\n * Cached by the string it was parsed from.\n *\n * useSyncExternalStore compares snapshots by identity, so handing back a fresh\n * URLSearchParams on every read reads as \"changed every time\" and loops until\n * React gives up.\n */\nlet cached = { search: \"\", params: new URLSearchParams() };\n\nfunction currentSearch(): string {\n return typeof window === \"undefined\" ? \"\" : window.location.search;\n}\n\nfunction notify(): void {\n listeners.forEach((fn) => fn());\n}\n\nlet listening = false;\n\nfunction subscribe(callback: () => void): () => void {\n // On the first subscriber rather than at module load. A listener attached\n // when the module is evaluated is attached to whatever `window` exists at\n // that moment — which in a test that installs a DOM after its imports is\n // none, and the hook silently never updates. Attaching here is the shape\n // useSyncExternalStore expects and is correct wherever the module loads.\n if (!listening && typeof window !== \"undefined\") {\n window.addEventListener(\"rsc-navigate\", notify);\n window.addEventListener(\"popstate\", notify);\n listening = true;\n }\n\n listeners.add(callback);\n\n return () => listeners.delete(callback);\n}\n\nfunction getSnapshot(): URLSearchParams {\n const search = currentSearch();\n\n if (search !== cached.search) {\n cached = { search, params: new URLSearchParams(search) };\n }\n\n return cached.params;\n}\n\nfunction getServerSnapshot(): URLSearchParams {\n throw new Error(\n \"useSearchParams() was read while rendering on the server, where there is no query string. \" +\n \"Wrap the component in <Suspense> — or add a loading.tsx beside the page — so the fallback \" +\n \"is stored and the real value arrives in the browser.\",\n );\n}\n\nexport function useSearchParams(): URLSearchParams {\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n}\n"]}
1
+ {"version":3,"file":"useSearchParams.js","sourceRoot":"","sources":["../../src/js/useSearchParams.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAE7C,MAAM,SAAS,GAAG,IAAI,GAAG,EAAc,CAAC;AAExC;;;;;;GAMG;AACH,IAAI,MAAM,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,eAAe,EAAE,EAAE,CAAC;AAE3D,SAAS,aAAa;IACpB,OAAO,OAAO,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;AACrE,CAAC;AAED,SAAS,MAAM;IACb,SAAS,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;AAClC,CAAC;AAED,IAAI,SAAS,GAAG,KAAK,CAAC;AAEtB,SAAS,SAAS,CAAC,QAAoB;IACrC,0EAA0E;IAC1E,0EAA0E;IAC1E,yEAAyE;IACzE,yEAAyE;IACzE,yEAAyE;IACzE,IAAI,CAAC,SAAS,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;QAChD,MAAM,CAAC,gBAAgB,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC;QAChD,MAAM,CAAC,gBAAgB,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QAC5C,SAAS,GAAG,IAAI,CAAC;IACnB,CAAC;IAED,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAExB,OAAO,GAAG,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;AAC1C,CAAC;AAED,SAAS,WAAW;IAClB,MAAM,MAAM,GAAG,aAAa,EAAE,CAAC;IAE/B,IAAI,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,CAAC;QAC7B,MAAM,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,eAAe,CAAC,MAAM,CAAC,EAAE,CAAC;IAC3D,CAAC;IAED,OAAO,MAAM,CAAC,MAAM,CAAC;AACvB,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,gCAAgC,CAAC;AAEvE,SAAS,iBAAiB;IACxB,MAAM,KAAK,GAAG,IAAI,KAAK,CACrB,oGAAoG;QAClG,yGAAyG;QACzG,oGAAoG,CACxE,CAAC;IAEjC,KAAK,CAAC,MAAM,GAAG,sBAAsB,CAAC;IAEtC,MAAM,KAAK,CAAC;AACd,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,OAAO,oBAAoB,CAAC,SAAS,EAAE,WAAW,EAAE,iBAAiB,CAAC,CAAC;AACzE,CAAC","sourcesContent":["\"use client\";\n\n/**\n * The query string, live across navigations.\n *\n * Deliberately unlike usePathname, which answers with the url being rendered.\n * A pathname is fixed for a given stored page; a query string is not — the\n * same route is asked for with `?q=shoes` and `?q=hats`, and a page stored\n * holding one of them would serve it to everyone.\n *\n * So there is no server snapshot to give, and this throws rather than\n * inventing one. React treats an error thrown during SSR as recoverable at the\n * nearest Suspense boundary: the fallback is what gets stored, and the client\n * renders the real thing on hydration. Without a boundary the error reaches the\n * root, nothing paints, and the build refuses the route and says why — which\n * is the same answer it gives for reading the request too early on the server.\n *\n * The dev server renders per request and could answer with that request's\n * query string; it does not, on purpose. A page has one shape — a stored\n * shell with the query read in the browser — and dev shows that shape, so a\n * boundary missing in dev is the boundary the build will refuse.\n *\n * Returning an empty URLSearchParams instead would be worse than either: the\n * page would be stored showing results for no query at all, and nothing would\n * say so.\n */\n\nimport { useSyncExternalStore } from \"react\";\n\nconst listeners = new Set<() => void>();\n\n/**\n * Cached by the string it was parsed from.\n *\n * useSyncExternalStore compares snapshots by identity, so handing back a fresh\n * URLSearchParams on every read reads as \"changed every time\" and loops until\n * React gives up.\n */\nlet cached = { search: \"\", params: new URLSearchParams() };\n\nfunction currentSearch(): string {\n return typeof window === \"undefined\" ? \"\" : window.location.search;\n}\n\nfunction notify(): void {\n listeners.forEach((fn) => fn());\n}\n\nlet listening = false;\n\nfunction subscribe(callback: () => void): () => void {\n // On the first subscriber rather than at module load. A listener attached\n // when the module is evaluated is attached to whatever `window` exists at\n // that moment — which in a test that installs a DOM after its imports is\n // none, and the hook silently never updates. Attaching here is the shape\n // useSyncExternalStore expects and is correct wherever the module loads.\n if (!listening && typeof window !== \"undefined\") {\n window.addEventListener(\"rsc-navigate\", notify);\n window.addEventListener(\"popstate\", notify);\n listening = true;\n }\n\n listeners.add(callback);\n\n return () => listeners.delete(callback);\n}\n\nfunction getSnapshot(): URLSearchParams {\n const search = currentSearch();\n\n if (search !== cached.search) {\n cached = { search, params: new URLSearchParams(search) };\n }\n\n return cached.params;\n}\n\n/**\n * The digest a stored page carries where the query was read under the\n * boundary that caught it. The client recognises it on hydration and does\n * not report the fallback as an error, because it is the designed path.\n */\nexport const SEARCH_PARAMS_FALLBACK = \"rsc-kit:search-params-fallback\";\n\nfunction getServerSnapshot(): URLSearchParams {\n const error = new Error(\n \"useSearchParams() was read while rendering on the server, where there is no query string to give: \" +\n \"a stored page would serve one visitor's query to everyone. Wrap the component in <Suspense> — or add a \" +\n \"loading.tsx beside the page — so the fallback is stored and the real value arrives in the browser.\",\n ) as Error & { digest?: string };\n\n error.digest = SEARCH_PARAMS_FALLBACK;\n\n throw error;\n}\n\nexport function useSearchParams(): URLSearchParams {\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n}\n"]}