@rsc-kit/core 0.16.3 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/dist/barrelImports.d.ts +6 -0
  2. package/dist/barrelImports.js +93 -0
  3. package/dist/barrelImports.js.map +1 -0
  4. package/dist/buildReport.d.ts +6 -1
  5. package/dist/buildReport.js +16 -18
  6. package/dist/buildReport.js.map +1 -1
  7. package/dist/cache.js +8 -4
  8. package/dist/cache.js.map +1 -1
  9. package/dist/clientEntries.d.ts +32 -0
  10. package/dist/clientEntries.js +107 -0
  11. package/dist/clientEntries.js.map +1 -0
  12. package/dist/clientImports.d.ts +36 -0
  13. package/dist/clientImports.js +36 -0
  14. package/dist/clientImports.js.map +1 -0
  15. package/dist/devReload.d.ts +19 -0
  16. package/dist/devReload.js +32 -0
  17. package/dist/devReload.js.map +1 -0
  18. package/dist/events.d.ts +57 -0
  19. package/dist/events.js +113 -0
  20. package/dist/events.js.map +1 -0
  21. package/dist/host.d.ts +16 -6
  22. package/dist/host.js +219 -124
  23. package/dist/host.js.map +1 -1
  24. package/dist/hostCalls.d.ts +27 -0
  25. package/dist/hostCalls.js +127 -12
  26. package/dist/hostCalls.js.map +1 -1
  27. package/dist/hostRouting.d.ts +53 -0
  28. package/dist/hostRouting.js +100 -0
  29. package/dist/hostRouting.js.map +1 -0
  30. package/dist/js/DefaultRouteError.d.ts +3 -0
  31. package/dist/js/DefaultRouteError.js +61 -0
  32. package/dist/js/DefaultRouteError.js.map +1 -0
  33. package/dist/js/Form.js +54 -6
  34. package/dist/js/Form.js.map +1 -1
  35. package/dist/js/Link.d.ts +0 -3
  36. package/dist/js/Link.js +3 -6
  37. package/dist/js/Link.js.map +1 -1
  38. package/dist/js/SegmentBoundary.d.ts +38 -2
  39. package/dist/js/SegmentBoundary.js +51 -13
  40. package/dist/js/SegmentBoundary.js.map +1 -1
  41. package/dist/js/createViteRscApp.js +32 -17
  42. package/dist/js/createViteRscApp.js.map +1 -1
  43. package/dist/js/devNotice.d.ts +2 -2
  44. package/dist/js/devNotice.js +17 -7
  45. package/dist/js/devNotice.js.map +1 -1
  46. package/dist/js/fallbackReport.d.ts +2 -0
  47. package/dist/js/fallbackReport.js +51 -0
  48. package/dist/js/fallbackReport.js.map +1 -0
  49. package/dist/js/navigate.d.ts +1 -1
  50. package/dist/js/navigate.js +55 -23
  51. package/dist/js/navigate.js.map +1 -1
  52. package/dist/js/queryClient.d.ts +2 -1
  53. package/dist/js/queryClient.js +0 -12
  54. package/dist/js/queryClient.js.map +1 -1
  55. package/dist/js/router.d.ts +2 -2
  56. package/dist/js/router.js.map +1 -1
  57. package/dist/js/segmentStore.d.ts +2 -0
  58. package/dist/js/segmentStore.js +21 -1
  59. package/dist/js/segmentStore.js.map +1 -1
  60. package/dist/js/staleAssets.js +10 -1
  61. package/dist/js/staleAssets.js.map +1 -1
  62. package/dist/js/standardSchema.d.ts +6 -0
  63. package/dist/js/standardSchema.js +19 -1
  64. package/dist/js/standardSchema.js.map +1 -1
  65. package/dist/js/useEvents.d.ts +25 -0
  66. package/dist/js/useEvents.js +78 -0
  67. package/dist/js/useEvents.js.map +1 -0
  68. package/dist/js/useLinkStatus.d.ts +14 -1
  69. package/dist/js/useLinkStatus.js +15 -1
  70. package/dist/js/useLinkStatus.js.map +1 -1
  71. package/dist/js/usePolling.d.ts +29 -0
  72. package/dist/js/usePolling.js +141 -0
  73. package/dist/js/usePolling.js.map +1 -0
  74. package/dist/manifest.d.ts +18 -1
  75. package/dist/manifest.js.map +1 -1
  76. package/dist/metadata.d.ts +94 -15
  77. package/dist/metadata.js.map +1 -1
  78. package/dist/metadataRoutes.d.ts +61 -0
  79. package/dist/metadataRoutes.js +173 -0
  80. package/dist/metadataRoutes.js.map +1 -0
  81. package/dist/prerender.js +44 -10
  82. package/dist/prerender.js.map +1 -1
  83. package/dist/reactCache.d.ts +1 -0
  84. package/dist/reactCache.js +52 -0
  85. package/dist/reactCache.js.map +1 -0
  86. package/dist/redirect.d.ts +16 -2
  87. package/dist/redirect.js +10 -19
  88. package/dist/redirect.js.map +1 -1
  89. package/dist/request.d.ts +18 -1
  90. package/dist/request.js +91 -5
  91. package/dist/request.js.map +1 -1
  92. package/dist/revalidate.d.ts +6 -4
  93. package/dist/revalidate.js +6 -5
  94. package/dist/revalidate.js.map +1 -1
  95. package/dist/routes.d.ts +16 -3
  96. package/dist/routes.js +5 -5
  97. package/dist/routes.js.map +1 -1
  98. package/dist/routing.d.ts +2 -2
  99. package/dist/routing.js +47 -25
  100. package/dist/routing.js.map +1 -1
  101. package/dist/useSsr.d.ts +19 -0
  102. package/dist/useSsr.js +104 -0
  103. package/dist/useSsr.js.map +1 -0
  104. package/dist/vite.d.ts +86 -9
  105. package/dist/vite.js +827 -24
  106. package/dist/vite.js.map +1 -1
  107. package/package.json +13 -1
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Rewrite the named imports in `code` whose package resolves to a barrel.
3
+ * `resolveEntry` answers a bare specifier with the file Vite would load for
4
+ * it, or null. Returns null when nothing changed.
5
+ */
6
+ export declare function unrollBarrelImports(code: string, resolveEntry: (specifier: string) => string | null): string | null;
@@ -0,0 +1,93 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ /**
4
+ * A named import from a barrel, rewritten to the module that owns the name.
5
+ *
6
+ * `import { ArrowRight } from 'lucide-react'` reaches a file that re-exports
7
+ * fifteen hundred icons, one module each. The browser gets the package
8
+ * pre-bundled; the server environments do not, on purpose - a package with
9
+ * a "use client" file stays as files so a client reference resolves to a
10
+ * real one - so Vite loaded and transformed every icon module on the first
11
+ * request. Measured: 3,700 transforms and twenty-five seconds before the
12
+ * first document, for thirteen icons. Next rewrites these imports for the
13
+ * same reason (optimizePackageImports); this does it for the packages it
14
+ * finds barrels in, by reading the barrel.
15
+ *
16
+ * The map from export name to file is read from the barrel's own
17
+ * `export { default as Name } from './x.mjs'` lines, once per barrel, so no
18
+ * package needs a table here. A name the barrel does not map that way - a
19
+ * component exported from the package's own source, say - is left on the
20
+ * original import and resolves as before.
21
+ */
22
+ const REEXPORT = /export\s*\{([^}]*)\}\s*from\s*['"]([^'"]+)['"]/g;
23
+ const NAMED_IMPORT = /^([ \t]*)import\s*(type\s+)?\{([^}]*)\}\s*from\s*(['"])([^'"]+)\4\s*;?[ \t]*$/gm;
24
+ /** Which packages to unroll: a barrel is any entry whose exports are mostly re-exports of default exports. */
25
+ const barrels = new Map();
26
+ function readBarrel(entry) {
27
+ const cached = barrels.get(entry);
28
+ if (cached !== undefined)
29
+ return cached;
30
+ let map = null;
31
+ try {
32
+ const source = readFileSync(entry, "utf-8");
33
+ const found = new Map();
34
+ for (const m of source.matchAll(REEXPORT)) {
35
+ const file = resolve(dirname(entry), m[2]);
36
+ for (const spec of m[1].split(",")) {
37
+ const part = spec.trim().match(/^default\s+as\s+([A-Za-z_$][\w$]*)$/);
38
+ if (part)
39
+ found.set(part[1], file);
40
+ }
41
+ }
42
+ // Under a hundred names is a module with a few re-exports, not a barrel
43
+ // worth unrolling; the cost this pays for is the long tail.
44
+ map = found.size >= 100 ? found : null;
45
+ }
46
+ catch {
47
+ map = null;
48
+ }
49
+ barrels.set(entry, map);
50
+ return map;
51
+ }
52
+ /**
53
+ * Rewrite the named imports in `code` whose package resolves to a barrel.
54
+ * `resolveEntry` answers a bare specifier with the file Vite would load for
55
+ * it, or null. Returns null when nothing changed.
56
+ */
57
+ export function unrollBarrelImports(code, resolveEntry) {
58
+ if (!code.includes("import"))
59
+ return null;
60
+ let changed = false;
61
+ const out = code.replace(NAMED_IMPORT, (whole, indent, typeOnly, names, _q, specifier) => {
62
+ if (typeOnly || specifier.startsWith(".") || specifier.startsWith("/"))
63
+ return whole;
64
+ const entry = resolveEntry(specifier);
65
+ const map = entry && existsSync(entry) ? readBarrel(entry) : null;
66
+ if (!map)
67
+ return whole;
68
+ const kept = [];
69
+ const direct = [];
70
+ for (const spec of names.split(",")) {
71
+ const s = spec.trim();
72
+ if (!s)
73
+ continue;
74
+ const m = s.match(/^(?:type\s+)?([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/);
75
+ const exported = m?.[1];
76
+ const local = m?.[2] ?? exported;
77
+ const file = exported && !s.startsWith("type ") ? map.get(exported) : undefined;
78
+ if (file && local)
79
+ direct.push(`${indent}import ${local} from ${JSON.stringify(file)};`);
80
+ else
81
+ kept.push(s);
82
+ }
83
+ if (direct.length === 0)
84
+ return whole;
85
+ changed = true;
86
+ const rest = kept.length
87
+ ? `${indent}import { ${kept.join(", ")} } from ${JSON.stringify(specifier)};\n`
88
+ : "";
89
+ return rest + direct.join("\n");
90
+ });
91
+ return changed ? out : null;
92
+ }
93
+ //# sourceMappingURL=barrelImports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"barrelImports.js","sourceRoot":"","sources":["../src/barrelImports.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, resolve } from \"node:path\";\n\n/**\n * A named import from a barrel, rewritten to the module that owns the name.\n *\n * `import { ArrowRight } from 'lucide-react'` reaches a file that re-exports\n * fifteen hundred icons, one module each. The browser gets the package\n * pre-bundled; the server environments do not, on purpose - a package with\n * a \"use client\" file stays as files so a client reference resolves to a\n * real one - so Vite loaded and transformed every icon module on the first\n * request. Measured: 3,700 transforms and twenty-five seconds before the\n * first document, for thirteen icons. Next rewrites these imports for the\n * same reason (optimizePackageImports); this does it for the packages it\n * finds barrels in, by reading the barrel.\n *\n * The map from export name to file is read from the barrel's own\n * `export { default as Name } from './x.mjs'` lines, once per barrel, so no\n * package needs a table here. A name the barrel does not map that way - a\n * component exported from the package's own source, say - is left on the\n * original import and resolves as before.\n */\n\nconst REEXPORT = /export\\s*\\{([^}]*)\\}\\s*from\\s*['\"]([^'\"]+)['\"]/g;\nconst NAMED_IMPORT =\n /^([ \\t]*)import\\s*(type\\s+)?\\{([^}]*)\\}\\s*from\\s*(['\"])([^'\"]+)\\4\\s*;?[ \\t]*$/gm;\n\n/** Which packages to unroll: a barrel is any entry whose exports are mostly re-exports of default exports. */\nconst barrels = new Map<string, Map<string, string> | null>();\n\nfunction readBarrel(entry: string): Map<string, string> | null {\n const cached = barrels.get(entry);\n\n if (cached !== undefined) return cached;\n\n let map: Map<string, string> | null = null;\n\n try {\n const source = readFileSync(entry, \"utf-8\");\n const found = new Map<string, string>();\n\n for (const m of source.matchAll(REEXPORT)) {\n const file = resolve(dirname(entry), m[2]);\n\n for (const spec of m[1].split(\",\")) {\n const part = spec.trim().match(/^default\\s+as\\s+([A-Za-z_$][\\w$]*)$/);\n\n if (part) found.set(part[1], file);\n }\n }\n\n // Under a hundred names is a module with a few re-exports, not a barrel\n // worth unrolling; the cost this pays for is the long tail.\n map = found.size >= 100 ? found : null;\n } catch {\n map = null;\n }\n\n barrels.set(entry, map);\n\n return map;\n}\n\n/**\n * Rewrite the named imports in `code` whose package resolves to a barrel.\n * `resolveEntry` answers a bare specifier with the file Vite would load for\n * it, or null. Returns null when nothing changed.\n */\nexport function unrollBarrelImports(\n code: string,\n resolveEntry: (specifier: string) => string | null,\n): string | null {\n if (!code.includes(\"import\")) return null;\n\n let changed = false;\n\n const out = code.replace(\n NAMED_IMPORT,\n (whole, indent: string, typeOnly, names: string, _q, specifier: string) => {\n if (typeOnly || specifier.startsWith(\".\") || specifier.startsWith(\"/\"))\n return whole;\n\n const entry = resolveEntry(specifier);\n const map = entry && existsSync(entry) ? readBarrel(entry) : null;\n\n if (!map) return whole;\n\n const kept: string[] = [];\n const direct: string[] = [];\n\n for (const spec of names.split(\",\")) {\n const s = spec.trim();\n\n if (!s) continue;\n\n const m = s.match(\n /^(?:type\\s+)?([A-Za-z_$][\\w$]*)(?:\\s+as\\s+([A-Za-z_$][\\w$]*))?$/,\n );\n const exported = m?.[1];\n const local = m?.[2] ?? exported;\n const file =\n exported && !s.startsWith(\"type \") ? map.get(exported) : undefined;\n\n if (file && local)\n direct.push(`${indent}import ${local} from ${JSON.stringify(file)};`);\n else kept.push(s);\n }\n\n if (direct.length === 0) return whole;\n\n changed = true;\n\n const rest = kept.length\n ? `${indent}import { ${kept.join(\", \")} } from ${JSON.stringify(specifier)};\\n`\n : \"\";\n\n return rest + direct.join(\"\\n\");\n },\n );\n\n return changed ? out : null;\n}\n"]}
@@ -1,3 +1,4 @@
1
+ import type { ClientLibraryImport } from "./clientImports.js";
1
2
  /** One route, as the build left it. */
2
3
  export interface ReportedRoute {
3
4
  url: string;
@@ -39,6 +40,10 @@ export interface BuildReport {
39
40
  apis: ReportedApiRoute[];
40
41
  /** Every "use server" export the app registered. Absent from reports older than this field. */
41
42
  actions?: ReportedAction[];
43
+ /** Server files importing cache from React, relative to the source dir; absent from older reports. */
44
+ reactCache?: string[];
45
+ /** Server files importing a client library, with the packages and the importer; absent from older reports. */
46
+ clientImports?: ClientLibraryImport[];
42
47
  totals: {
43
48
  static: number;
44
49
  partial: number;
@@ -48,7 +53,7 @@ export interface BuildReport {
48
53
  }
49
54
  /** The name the report is written under, inside the build's own directory. */
50
55
  export declare const REPORT_FILE = "build-report.json";
51
- export declare function buildReport(routes: ReportedRoute[], apis: ReportedApiRoute[], actions?: ReportedAction[]): string;
56
+ export declare function buildReport(routes: ReportedRoute[], apis: ReportedApiRoute[], actions?: ReportedAction[], reactCache?: string[], clientImports?: ClientLibraryImport[]): string;
52
57
  /**
53
58
  * The routes worth asking about, most interesting first.
54
59
  *
@@ -1,16 +1,6 @@
1
- // What the build decided, written down.
2
- //
3
- // The classification exists already — it is printed as the build runs, and
4
- // then it is gone. That is fine for a person watching a terminal and useless
5
- // for anything that wants to ask afterwards: a CI step asserting nothing
6
- // regressed, an editor, an agent being asked why a page is slow.
7
- //
8
- // So the same facts go to a file. Not a new computation and not a second
9
- // source of truth — the report is written from the results the build already
10
- // produced, in the same pass that prints them.
11
1
  /** The name the report is written under, inside the build's own directory. */
12
- export const REPORT_FILE = 'build-report.json';
13
- export function buildReport(routes, apis, actions = []) {
2
+ export const REPORT_FILE = "build-report.json";
3
+ export function buildReport(routes, apis, actions = [], reactCache = [], clientImports = []) {
14
4
  const count = (...types) => routes.filter((r) => types.includes(r.type)).length +
15
5
  apis.filter((a) => types.includes(a.type)).length;
16
6
  const report = {
@@ -18,16 +8,18 @@ export function buildReport(routes, apis, actions = []) {
18
8
  routes,
19
9
  apis,
20
10
  actions,
11
+ reactCache,
12
+ clientImports,
21
13
  totals: {
22
- static: count('frozen'),
23
- partial: count('shell'),
24
- dynamic: count('dynamic'),
14
+ static: count("frozen"),
15
+ partial: count("shell"),
16
+ dynamic: count("dynamic"),
25
17
  // Blocked is refused, not dynamic: a page that painted nothing before it
26
18
  // read the request has no shell to store and the build did not finish.
27
- failed: count('error', 'blocked'),
19
+ failed: count("error", "blocked"),
28
20
  },
29
21
  };
30
- return JSON.stringify(report, null, 2) + '\n';
22
+ return JSON.stringify(report, null, 2) + "\n";
31
23
  }
32
24
  /**
33
25
  * The routes worth asking about, most interesting first.
@@ -37,7 +29,13 @@ export function buildReport(routes, apis, actions = []) {
37
29
  * be stored at all, then one that ships a shell, then the ones that are fine.
38
30
  */
39
31
  export function byInterest(routes) {
40
- const rank = { error: 0, blocked: 1, shell: 2, dynamic: 3, frozen: 4 };
32
+ const rank = {
33
+ error: 0,
34
+ blocked: 1,
35
+ shell: 2,
36
+ dynamic: 3,
37
+ frozen: 4,
38
+ };
41
39
  return [...routes].sort((a, b) => (rank[a.type] ?? 9) - (rank[b.type] ?? 9) || a.url.localeCompare(b.url));
42
40
  }
43
41
  //# sourceMappingURL=buildReport.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"buildReport.js","sourceRoot":"","sources":["../src/buildReport.ts"],"names":[],"mappings":"AAAA,wCAAwC;AACxC,EAAE;AACF,2EAA2E;AAC3E,6EAA6E;AAC7E,yEAAyE;AACzE,iEAAiE;AACjE,EAAE;AACF,yEAAyE;AACzE,6EAA6E;AAC7E,+CAA+C;AAsD/C,8EAA8E;AAC9E,MAAM,CAAC,MAAM,WAAW,GAAG,mBAAmB,CAAA;AAE9C,MAAM,UAAU,WAAW,CACzB,MAAuB,EACvB,IAAwB,EACxB,OAAO,GAAqB,EAAE;IAE9B,MAAM,KAAK,GAAG,CAAC,GAAG,KAAe,EAAE,EAAE,CACnC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM;QACnD,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAA;IAEnD,MAAM,MAAM,GAAgB;QAC1B,OAAO,EAAE,CAAC;QACV,MAAM;QACN,IAAI;QACJ,OAAO;QACP,MAAM,EAAE;YACN,MAAM,EAAE,KAAK,CAAC,QAAQ,CAAC;YACvB,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC;YACvB,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC;YACzB,yEAAyE;YACzE,uEAAuE;YACvE,MAAM,EAAE,KAAK,CAAC,OAAO,EAAE,SAAS,CAAC;SAClC;KACF,CAAA;IAED,OAAO,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAA;AAC/C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,MAAuB;IAChD,MAAM,IAAI,GAA2B,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAA;IAE9F,OAAO,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,CACrB,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,CAClF,CAAA;AACH,CAAC","sourcesContent":["// What the build decided, written down.\n//\n// The classification exists already — it is printed as the build runs, and\n// then it is gone. That is fine for a person watching a terminal and useless\n// for anything that wants to ask afterwards: a CI step asserting nothing\n// regressed, an editor, an agent being asked why a page is slow.\n//\n// So the same facts go to a file. Not a new computation and not a second\n// source of truth — the report is written from the results the build already\n// produced, in the same pass that prints them.\n\n/** One route, as the build left it. */\nexport interface ReportedRoute {\n url: string\n component: string\n /** frozen | shell | blocked | dynamic | error — see PrerenderResult. blocked and error failed the build. */\n type: string\n /** Why it is not frozen, in the words the build printed. */\n reason: string | null\n /** Something true and worth knowing that is not a failure. */\n warning: string | null\n /** A fact about how it was stored - \"no client components, so ships no javascript\". Absent from older reports. */\n note?: string | null\n /** Gzipped bytes of javascript this url makes the browser download. */\n clientJs: number | null\n}\n\n/** A server action, and whether anything checks who calls it. */\nexport interface ReportedAction {\n id: string\n name: string\n file: string\n /** Built by createActionClient, so its middleware ran. */\n client: boolean\n /** A read (GET) rather than a mutation. */\n query: boolean\n}\n\nexport interface ReportedApiRoute {\n url: string\n name: string\n type: string\n reason: string | null\n /** Stored, and froze a value that will not be the same tomorrow. Absent from older reports. */\n warning?: string | null\n}\n\nexport interface BuildReport {\n version: 1\n /** Routes that render, in the order the build reported them. */\n routes: ReportedRoute[]\n /** route.ts endpoints. */\n apis: ReportedApiRoute[]\n /** Every \"use server\" export the app registered. Absent from reports older than this field. */\n actions?: ReportedAction[]\n totals: {\n static: number\n partial: number\n dynamic: number\n failed: number\n }\n}\n\n/** The name the report is written under, inside the build's own directory. */\nexport const REPORT_FILE = 'build-report.json'\n\nexport function buildReport(\n routes: ReportedRoute[],\n apis: ReportedApiRoute[],\n actions: ReportedAction[] = [],\n): string {\n const count = (...types: string[]) =>\n routes.filter((r) => types.includes(r.type)).length +\n apis.filter((a) => types.includes(a.type)).length\n\n const report: BuildReport = {\n version: 1,\n routes,\n apis,\n actions,\n totals: {\n static: count('frozen'),\n partial: count('shell'),\n dynamic: count('dynamic'),\n // Blocked is refused, not dynamic: a page that painted nothing before it\n // read the request has no shell to store and the build did not finish.\n failed: count('error', 'blocked'),\n },\n }\n\n return JSON.stringify(report, null, 2) + '\\n'\n}\n\n/**\n * The routes worth asking about, most interesting first.\n *\n * \"Interesting\" is not a judgement about the app — it is the order someone\n * looking for a problem reads in. A failure first, then a page that could not\n * be stored at all, then one that ships a shell, then the ones that are fine.\n */\nexport function byInterest(routes: ReportedRoute[]): ReportedRoute[] {\n const rank: Record<string, number> = { error: 0, blocked: 1, shell: 2, dynamic: 3, frozen: 4 }\n\n return [...routes].sort(\n (a, b) => (rank[a.type] ?? 9) - (rank[b.type] ?? 9) || a.url.localeCompare(b.url),\n )\n}\n"]}
1
+ {"version":3,"file":"buildReport.js","sourceRoot":"","sources":["../src/buildReport.ts"],"names":[],"mappings":"AAqEA,8EAA8E;AAC9E,MAAM,CAAC,MAAM,WAAW,GAAG,mBAAmB,CAAC;AAE/C,MAAM,UAAU,WAAW,CACzB,MAAuB,EACvB,IAAwB,EACxB,OAAO,GAAqB,EAAE,EAC9B,UAAU,GAAa,EAAE,EACzB,aAAa,GAA0B,EAAE;IAEzC,MAAM,KAAK,GAAG,CAAC,GAAG,KAAe,EAAE,EAAE,CACnC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM;QACnD,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC;IAEpD,MAAM,MAAM,GAAgB;QAC1B,OAAO,EAAE,CAAC;QACV,MAAM;QACN,IAAI;QACJ,OAAO;QACP,UAAU;QACV,aAAa;QACb,MAAM,EAAE;YACN,MAAM,EAAE,KAAK,CAAC,QAAQ,CAAC;YACvB,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC;YACvB,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC;YACzB,yEAAyE;YACzE,uEAAuE;YACvE,MAAM,EAAE,KAAK,CAAC,OAAO,EAAE,SAAS,CAAC;SAClC;KACF,CAAC;IAEF,OAAO,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC;AAChD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,MAAuB;IAChD,MAAM,IAAI,GAA2B;QACnC,KAAK,EAAE,CAAC;QACR,OAAO,EAAE,CAAC;QACV,KAAK,EAAE,CAAC;QACR,OAAO,EAAE,CAAC;QACV,MAAM,EAAE,CAAC;KACV,CAAC;IAEF,OAAO,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,CACrB,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CACP,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,CAC1E,CAAC;AACJ,CAAC","sourcesContent":["import type { ClientLibraryImport } from \"./clientImports.js\";\n\n// What the build decided, written down.\n//\n// The classification exists already — it is printed as the build runs, and\n// then it is gone. That is fine for a person watching a terminal and useless\n// for anything that wants to ask afterwards: a CI step asserting nothing\n// regressed, an editor, an agent being asked why a page is slow.\n//\n// So the same facts go to a file. Not a new computation and not a second\n// source of truth — the report is written from the results the build already\n// produced, in the same pass that prints them.\n\n/** One route, as the build left it. */\nexport interface ReportedRoute {\n url: string;\n component: string;\n /** frozen | shell | blocked | dynamic | error — see PrerenderResult. blocked and error failed the build. */\n type: string;\n /** Why it is not frozen, in the words the build printed. */\n reason: string | null;\n /** Something true and worth knowing that is not a failure. */\n warning: string | null;\n /** A fact about how it was stored - \"no client components, so ships no javascript\". Absent from older reports. */\n note?: string | null;\n /** Gzipped bytes of javascript this url makes the browser download. */\n clientJs: number | null;\n}\n\n/** A server action, and whether anything checks who calls it. */\nexport interface ReportedAction {\n id: string;\n name: string;\n file: string;\n /** Built by createActionClient, so its middleware ran. */\n client: boolean;\n /** A read (GET) rather than a mutation. */\n query: boolean;\n}\n\nexport interface ReportedApiRoute {\n url: string;\n name: string;\n type: string;\n reason: string | null;\n /** Stored, and froze a value that will not be the same tomorrow. Absent from older reports. */\n warning?: string | null;\n}\n\nexport interface BuildReport {\n version: 1;\n /** Routes that render, in the order the build reported them. */\n routes: ReportedRoute[];\n /** route.ts endpoints. */\n apis: ReportedApiRoute[];\n /** Every \"use server\" export the app registered. Absent from reports older than this field. */\n actions?: ReportedAction[];\n /** Server files importing cache from React, relative to the source dir; absent from older reports. */\n reactCache?: string[];\n /** Server files importing a client library, with the packages and the importer; absent from older reports. */\n clientImports?: ClientLibraryImport[];\n totals: {\n static: number;\n partial: number;\n dynamic: number;\n failed: number;\n };\n}\n\n/** The name the report is written under, inside the build's own directory. */\nexport const REPORT_FILE = \"build-report.json\";\n\nexport function buildReport(\n routes: ReportedRoute[],\n apis: ReportedApiRoute[],\n actions: ReportedAction[] = [],\n reactCache: string[] = [],\n clientImports: ClientLibraryImport[] = [],\n): string {\n const count = (...types: string[]) =>\n routes.filter((r) => types.includes(r.type)).length +\n apis.filter((a) => types.includes(a.type)).length;\n\n const report: BuildReport = {\n version: 1,\n routes,\n apis,\n actions,\n reactCache,\n clientImports,\n totals: {\n static: count(\"frozen\"),\n partial: count(\"shell\"),\n dynamic: count(\"dynamic\"),\n // Blocked is refused, not dynamic: a page that painted nothing before it\n // read the request has no shell to store and the build did not finish.\n failed: count(\"error\", \"blocked\"),\n },\n };\n\n return JSON.stringify(report, null, 2) + \"\\n\";\n}\n\n/**\n * The routes worth asking about, most interesting first.\n *\n * \"Interesting\" is not a judgement about the app — it is the order someone\n * looking for a problem reads in. A failure first, then a page that could not\n * be stored at all, then one that ships a shell, then the ones that are fine.\n */\nexport function byInterest(routes: ReportedRoute[]): ReportedRoute[] {\n const rank: Record<string, number> = {\n error: 0,\n blocked: 1,\n shell: 2,\n dynamic: 3,\n frozen: 4,\n };\n\n return [...routes].sort(\n (a, b) =>\n (rank[a.type] ?? 9) - (rank[b.type] ?? 9) || a.url.localeCompare(b.url),\n );\n}\n"]}
package/dist/cache.js CHANGED
@@ -16,15 +16,16 @@
16
16
  // Scoped to the request, so two in flight cannot see each other's answers. The
17
17
  // scope is opened by whoever owns the request — the host, the worker, or the
18
18
  // prerenderer — and everything below it shares one store.
19
- import { resolveScope } from './revalidate.js';
20
- const SCOPE = Symbol.for('@rsc-kit/core.cache-scope');
19
+ import { noteAwaiter, withHelper } from "./request.js";
20
+ import { resolveScope } from "./revalidate.js";
21
+ const SCOPE = Symbol.for("@rsc-kit/core.cache-scope");
21
22
  const globals = globalThis;
22
23
  let ready = null;
23
24
  function scope() {
24
25
  return globals[SCOPE] ?? null;
25
26
  }
26
27
  function isObject(value) {
27
- return (typeof value === 'object' && value !== null) || typeof value === 'function';
28
+ return ((typeof value === "object" && value !== null) || typeof value === "function");
28
29
  }
29
30
  /** Walk to the node for this argument list, creating it as we go. */
30
31
  function nodeFor(root, args) {
@@ -75,9 +76,12 @@ export function cache(fn) {
75
76
  store.set(fn, root);
76
77
  }
77
78
  const node = nodeFor(root, args);
79
+ // Who asked, every time: the body runs once, and a read it makes can
80
+ // then name every component that waited on it, not only the first.
81
+ noteAwaiter(fn);
78
82
  if (node.result)
79
83
  return node.result.value;
80
- const value = fn(...args);
84
+ const value = withHelper(fn, () => fn(...args));
81
85
  node.result = { value };
82
86
  return value;
83
87
  };
package/dist/cache.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"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","sourcesContent":["// Asking the same question twice in one request.\n//\n// import { cache } from '@rsc-kit/core/cache'\n//\n// export const currentUser = cache(async () => db.user(await sessionId()))\n//\n// A guard checks who you are; the layout wants their name; the page wants their\n// permissions. Three calls, one request, one answer — without threading the\n// result through props that only exist to carry it.\n//\n// This is the same shape as React's cache(), and deliberately not React's: the\n// engine's own boundaries are not React's. A guard runs before any component\n// does, which is exactly where the duplication starts, and React has no scope\n// open there.\n//\n// Scoped to the request, so two in flight cannot see each other's answers. The\n// scope is opened by whoever owns the request — the host, the worker, or the\n// prerenderer — and everything below it shares one store.\n\nimport { resolveScope } from './revalidate.js'\n\n/** One request's memo table. */\ntype Store = Map<unknown, ArgNode>\n\n/**\n * Arguments keyed the way React keys them: a tree, one level per argument.\n *\n * Primitives compare by value and objects by identity, which falls out of\n * using a Map for the first and a WeakMap for the second. Serialising the\n * arguments instead would make `f({ a: 1 })` and `f({ a: 1 })` the same call —\n * which they are not, if the object is mutable.\n */\ninterface ArgNode {\n /** The answer for the argument list that ends here. */\n result?: { value: unknown }\n byValue?: Map<unknown, ArgNode>\n byIdentity?: WeakMap<object, ArgNode>\n}\n\nconst SCOPE = Symbol.for('@rsc-kit/core.cache-scope')\n\nconst globals = globalThis as Record<symbol | string, unknown>\n\nlet ready: Promise<void> | null = null\n\ninterface Scope {\n getStore(): Store | undefined\n run<T>(store: Store, fn: () => T): T\n}\n\nfunction scope(): Scope | null {\n return (globals[SCOPE] as Scope | undefined) ?? null\n}\n\nfunction isObject(value: unknown): value is object {\n return (typeof value === 'object' && value !== null) || typeof value === 'function'\n}\n\n/** Walk to the node for this argument list, creating it as we go. */\nfunction nodeFor(root: ArgNode, args: unknown[]): ArgNode {\n let node = root\n\n for (const arg of args) {\n if (isObject(arg)) {\n node.byIdentity ??= new WeakMap()\n\n let next = node.byIdentity.get(arg)\n\n if (!next) {\n next = {}\n node.byIdentity.set(arg, next)\n }\n\n node = next\n } else {\n node.byValue ??= new Map()\n\n let next = node.byValue.get(arg)\n\n if (!next) {\n next = {}\n node.byValue.set(arg, next)\n }\n\n node = next\n }\n }\n\n return node\n}\n\n/**\n * Memoise a function for the length of one request.\n *\n * The *promise* is cached, not the value, so callers that arrive while the\n * first is still in flight wait on it rather than starting a second. A\n * rejection is cached too: within one request the answer to a question does\n * not change because you asked again.\n *\n * Outside a request this calls straight through. Nothing is stored, nothing\n * leaks between requests, and shared code does not have to know which side of\n * the boundary it is on — the same reason revalidate() is a no-op outside an\n * action.\n */\nexport function cache<A extends unknown[], R>(fn: (...args: A) => R): (...args: A) => R {\n return (...args: A): R => {\n const store = scope()?.getStore()\n\n if (!store) return fn(...args)\n\n let root = store.get(fn)\n\n if (!root) {\n root = {}\n store.set(fn, root)\n }\n\n const node = nodeFor(root, args)\n\n if (node.result) return node.result.value as R\n\n const value = fn(...args)\n\n node.result = { value }\n\n return value\n }\n}\n\n/** Whether anything is listening, for code that wants to know. */\nexport function isCaching(): boolean {\n return scope()?.getStore() !== undefined\n}\n\n/**\n * Run one request with a memo table of its own.\n *\n * Opened by whoever owns the request. Nesting is harmless and does not create a\n * second table — an inner call reuses the one already open, so a host that\n * wraps and an engine that also wraps do not end up with two.\n */\nexport async function withCache<T>(run: () => Promise<T>): Promise<T> {\n if (!globals[SCOPE]) {\n ready ??= resolveScope().then((resolved) => {\n globals[SCOPE] ??= resolved as unknown as Scope\n })\n\n await ready\n }\n\n const existing = scope()!.getStore()\n\n if (existing) return await run()\n\n return await scope()!.run(new Map(), run)\n}\n"]}
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"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","sourcesContent":["// Asking the same question twice in one request.\n//\n// import { cache } from '@rsc-kit/core/cache'\n//\n// export const currentUser = cache(async () => db.user(await sessionId()))\n//\n// A guard checks who you are; the layout wants their name; the page wants their\n// permissions. Three calls, one request, one answer — without threading the\n// result through props that only exist to carry it.\n//\n// This is the same shape as React's cache(), and deliberately not React's: the\n// engine's own boundaries are not React's. A guard runs before any component\n// does, which is exactly where the duplication starts, and React has no scope\n// open there.\n//\n// Scoped to the request, so two in flight cannot see each other's answers. The\n// scope is opened by whoever owns the request — the host, the worker, or the\n// prerenderer — and everything below it shares one store.\n\nimport { noteAwaiter, withHelper } from \"./request.js\";\nimport { resolveScope } from \"./revalidate.js\";\n\n/** One request's memo table. */\ntype Store = Map<unknown, ArgNode>;\n\n/**\n * Arguments keyed the way React keys them: a tree, one level per argument.\n *\n * Primitives compare by value and objects by identity, which falls out of\n * using a Map for the first and a WeakMap for the second. Serialising the\n * arguments instead would make `f({ a: 1 })` and `f({ a: 1 })` the same call —\n * which they are not, if the object is mutable.\n */\ninterface ArgNode {\n /** The answer for the argument list that ends here. */\n result?: { value: unknown };\n byValue?: Map<unknown, ArgNode>;\n byIdentity?: WeakMap<object, ArgNode>;\n}\n\nconst SCOPE = Symbol.for(\"@rsc-kit/core.cache-scope\");\n\nconst globals = globalThis as Record<symbol | string, unknown>;\n\nlet ready: Promise<void> | null = null;\n\ninterface Scope {\n getStore(): Store | undefined;\n run<T>(store: Store, fn: () => T): T;\n}\n\nfunction scope(): Scope | null {\n return (globals[SCOPE] as Scope | undefined) ?? null;\n}\n\nfunction isObject(value: unknown): value is object {\n return (\n (typeof value === \"object\" && value !== null) || typeof value === \"function\"\n );\n}\n\n/** Walk to the node for this argument list, creating it as we go. */\nfunction nodeFor(root: ArgNode, args: unknown[]): ArgNode {\n let node = root;\n\n for (const arg of args) {\n if (isObject(arg)) {\n node.byIdentity ??= new WeakMap();\n\n let next = node.byIdentity.get(arg);\n\n if (!next) {\n next = {};\n node.byIdentity.set(arg, next);\n }\n\n node = next;\n } else {\n node.byValue ??= new Map();\n\n let next = node.byValue.get(arg);\n\n if (!next) {\n next = {};\n node.byValue.set(arg, next);\n }\n\n node = next;\n }\n }\n\n return node;\n}\n\n/**\n * Memoise a function for the length of one request.\n *\n * The *promise* is cached, not the value, so callers that arrive while the\n * first is still in flight wait on it rather than starting a second. A\n * rejection is cached too: within one request the answer to a question does\n * not change because you asked again.\n *\n * Outside a request this calls straight through. Nothing is stored, nothing\n * leaks between requests, and shared code does not have to know which side of\n * the boundary it is on — the same reason revalidate() is a no-op outside an\n * action.\n */\nexport function cache<A extends unknown[], R>(\n fn: (...args: A) => R,\n): (...args: A) => R {\n return (...args: A): R => {\n const store = scope()?.getStore();\n\n if (!store) return fn(...args);\n\n let root = store.get(fn);\n\n if (!root) {\n root = {};\n store.set(fn, root);\n }\n\n const node = nodeFor(root, args);\n\n // Who asked, every time: the body runs once, and a read it makes can\n // then name every component that waited on it, not only the first.\n noteAwaiter(fn);\n\n if (node.result) return node.result.value as R;\n\n const value = withHelper(fn, () => fn(...args));\n\n node.result = { value };\n\n return value;\n };\n}\n\n/** Whether anything is listening, for code that wants to know. */\nexport function isCaching(): boolean {\n return scope()?.getStore() !== undefined;\n}\n\n/**\n * Run one request with a memo table of its own.\n *\n * Opened by whoever owns the request. Nesting is harmless and does not create a\n * second table — an inner call reuses the one already open, so a host that\n * wraps and an engine that also wraps do not end up with two.\n */\nexport async function withCache<T>(run: () => Promise<T>): Promise<T> {\n if (!globals[SCOPE]) {\n ready ??= resolveScope().then((resolved) => {\n globals[SCOPE] ??= resolved as unknown as Scope;\n });\n\n await ready;\n }\n\n const existing = scope()!.getStore();\n\n if (existing) return await run();\n\n return await scope()!.run(new Map(), run);\n}\n"]}
@@ -0,0 +1,32 @@
1
+ export declare function clientEntries(sourceDir: string): string[];
2
+ /**
3
+ * The engine's own "use client" modules: Link, Form, the nuqs adapter, the
4
+ * boundaries. The package is excluded from pre-bundling so that a payload's
5
+ * reference to one of these files is the file itself, and an excluded
6
+ * package is one the scanner does not enter - so the dependencies these
7
+ * modules carry (the nuqs adapter's, for one) were found only when a page
8
+ * first loaded the module, and that page re-optimised under itself.
9
+ */
10
+ export declare function engineClientEntries(): string[];
11
+ /**
12
+ * What the scanner may follow from those files: the browser's graph, not
13
+ * the server's.
14
+ *
15
+ * A client component imports a "use server" module to call its actions. In
16
+ * the browser build that module is replaced by a proxy of its exports, and
17
+ * nothing below it - the database, the runtime's own `bun` modules - is
18
+ * ever bundled. The scanner runs before that replacement exists, follows
19
+ * the raw file, reaches `import { SQL } from "bun"`, and gives up on the
20
+ * whole scan: "Failed to run dependency scan", nothing pre-bundled, and the
21
+ * re-optimisation the entries were meant to prevent. So an action module
22
+ * scans as empty, and a runtime builtin is external here as it is in the
23
+ * server bundles.
24
+ */
25
+ export declare function clientScanPlugin(): {
26
+ name: string;
27
+ resolveId(id: string): {
28
+ id: string;
29
+ external: true;
30
+ } | null;
31
+ load(id: string): string | null;
32
+ };
@@ -0,0 +1,107 @@
1
+ import { readdirSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ /**
5
+ * The app's "use client" files, for Vite's dependency scanner.
6
+ *
7
+ * Vite pre-bundles the browser's dependencies once, at startup, from what it
8
+ * can reach by crawling the client entry. Here that entry imports the engine
9
+ * and React and nothing else: an app's client components are reached through
10
+ * RSC payloads, lazily, page by page. So the first visit to a page that uses
11
+ * base-ui, or lucide, or a form library, discovered a dependency the scanner
12
+ * never saw, and Vite re-optimised everything mid-session: new hashes for
13
+ * every pre-bundle, modules already in memory still pointing at the old
14
+ * React, the new page's modules at the new one - two Reacts, a null hook
15
+ * dispatcher, React unmounting the document. Blank, then Vite's own reload.
16
+ *
17
+ * Handing the scanner every "use client" file up front is what Next's
18
+ * bundler gets for free by walking one graph. The scanner follows imports,
19
+ * so a component without the directive that a client file imports is
20
+ * covered too; a server file is never scanned, so a server-only dependency
21
+ * is never pre-bundled for the browser by mistake.
22
+ */
23
+ const SOURCE = /\.(?:[cm]?[jt]sx?)$/;
24
+ const CLIENT = directive("use client");
25
+ const SERVER = directive("use server");
26
+ function directive(name) {
27
+ return new RegExp(`^\\s*(?:\\/\\/[^\\n]*\\n|\\/\\*[\\s\\S]*?\\*\\/\\s*)*["']${name}["']`);
28
+ }
29
+ function head(file) {
30
+ try {
31
+ return readFileSync(file, "utf-8").slice(0, 2048);
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ export function clientEntries(sourceDir) {
38
+ const found = [];
39
+ const walk = (dir) => {
40
+ let entries;
41
+ try {
42
+ entries = readdirSync(dir, { withFileTypes: true });
43
+ }
44
+ catch {
45
+ return;
46
+ }
47
+ for (const entry of entries) {
48
+ if (entry.name === "node_modules" || entry.name.startsWith("."))
49
+ continue;
50
+ const path = join(dir, entry.name);
51
+ if (entry.isDirectory()) {
52
+ walk(path);
53
+ continue;
54
+ }
55
+ if (!SOURCE.test(entry.name) || entry.name.endsWith(".d.ts"))
56
+ continue;
57
+ const source = head(path);
58
+ if (source !== null && CLIENT.test(source))
59
+ found.push(path);
60
+ }
61
+ };
62
+ walk(sourceDir);
63
+ return found.sort();
64
+ }
65
+ /**
66
+ * The engine's own "use client" modules: Link, Form, the nuqs adapter, the
67
+ * boundaries. The package is excluded from pre-bundling so that a payload's
68
+ * reference to one of these files is the file itself, and an excluded
69
+ * package is one the scanner does not enter - so the dependencies these
70
+ * modules carry (the nuqs adapter's, for one) were found only when a page
71
+ * first loaded the module, and that page re-optimised under itself.
72
+ */
73
+ export function engineClientEntries() {
74
+ return clientEntries(fileURLToPath(new URL("./js/", import.meta.url)));
75
+ }
76
+ /**
77
+ * What the scanner may follow from those files: the browser's graph, not
78
+ * the server's.
79
+ *
80
+ * A client component imports a "use server" module to call its actions. In
81
+ * the browser build that module is replaced by a proxy of its exports, and
82
+ * nothing below it - the database, the runtime's own `bun` modules - is
83
+ * ever bundled. The scanner runs before that replacement exists, follows
84
+ * the raw file, reaches `import { SQL } from "bun"`, and gives up on the
85
+ * whole scan: "Failed to run dependency scan", nothing pre-bundled, and the
86
+ * re-optimisation the entries were meant to prevent. So an action module
87
+ * scans as empty, and a runtime builtin is external here as it is in the
88
+ * server bundles.
89
+ */
90
+ export function clientScanPlugin() {
91
+ return {
92
+ name: "rsc-kit:client-scan",
93
+ resolveId(id) {
94
+ return id === "bun" || id.startsWith("bun:")
95
+ ? { id, external: true }
96
+ : null;
97
+ },
98
+ load(id) {
99
+ const file = id.split("?")[0];
100
+ if (file.includes("/node_modules/") || !SOURCE.test(file))
101
+ return null;
102
+ const source = head(file);
103
+ return source !== null && SERVER.test(source) ? "export {};" : null;
104
+ },
105
+ };
106
+ }
107
+ //# sourceMappingURL=clientEntries.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clientEntries.js","sourceRoot":"","sources":["../src/clientEntries.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,GAAG,qBAAqB,CAAC;AACrC,MAAM,MAAM,GAAG,SAAS,CAAC,YAAY,CAAC,CAAC;AACvC,MAAM,MAAM,GAAG,SAAS,CAAC,YAAY,CAAC,CAAC;AAEvC,SAAS,SAAS,CAAC,IAAY;IAC7B,OAAO,IAAI,MAAM,CACf,4DAA4D,IAAI,MAAM,CACvE,CAAC;AACJ,CAAC;AAED,SAAS,IAAI,CAAC,IAAY;IACxB,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;IACpD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,SAAiB;IAC7C,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,MAAM,IAAI,GAAG,CAAC,GAAW,EAAQ,EAAE;QACjC,IAAI,OAAO,CAAC;QAEZ,IAAI,CAAC;YACH,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QACtD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO;QACT,CAAC;QAED,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,IAAI,KAAK,CAAC,IAAI,KAAK,cAAc,IAAI,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;gBAAE,SAAS;YAE1E,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YAEnC,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;gBACxB,IAAI,CAAC,IAAI,CAAC,CAAC;gBAEX,SAAS;YACX,CAAC;YAED,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;gBAAE,SAAS;YAEvE,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC;YAE1B,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;gBAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/D,CAAC;IACH,CAAC,CAAC;IAEF,IAAI,CAAC,SAAS,CAAC,CAAC;IAEhB,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC;AACtB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB;IACjC,OAAO,aAAa,CAAC,aAAa,CAAC,IAAI,GAAG,CAAC,OAAO,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AACzE,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,gBAAgB;IAK9B,OAAO;QACL,IAAI,EAAE,qBAAqB;QAC3B,SAAS,CAAC,EAAE;YACV,OAAO,EAAE,KAAK,KAAK,IAAI,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC;gBAC1C,CAAC,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;gBACxB,CAAC,CAAC,IAAI,CAAC;QACX,CAAC;QACD,IAAI,CAAC,EAAE;YACL,MAAM,IAAI,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;YAE9B,IAAI,IAAI,CAAC,QAAQ,CAAC,gBAAgB,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC;gBAAE,OAAO,IAAI,CAAC;YAEvE,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC;YAE1B,OAAO,MAAM,KAAK,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC;QACtE,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["import { readdirSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\n/**\n * The app's \"use client\" files, for Vite's dependency scanner.\n *\n * Vite pre-bundles the browser's dependencies once, at startup, from what it\n * can reach by crawling the client entry. Here that entry imports the engine\n * and React and nothing else: an app's client components are reached through\n * RSC payloads, lazily, page by page. So the first visit to a page that uses\n * base-ui, or lucide, or a form library, discovered a dependency the scanner\n * never saw, and Vite re-optimised everything mid-session: new hashes for\n * every pre-bundle, modules already in memory still pointing at the old\n * React, the new page's modules at the new one - two Reacts, a null hook\n * dispatcher, React unmounting the document. Blank, then Vite's own reload.\n *\n * Handing the scanner every \"use client\" file up front is what Next's\n * bundler gets for free by walking one graph. The scanner follows imports,\n * so a component without the directive that a client file imports is\n * covered too; a server file is never scanned, so a server-only dependency\n * is never pre-bundled for the browser by mistake.\n */\nconst SOURCE = /\\.(?:[cm]?[jt]sx?)$/;\nconst CLIENT = directive(\"use client\");\nconst SERVER = directive(\"use server\");\n\nfunction directive(name: string): RegExp {\n return new RegExp(\n `^\\\\s*(?:\\\\/\\\\/[^\\\\n]*\\\\n|\\\\/\\\\*[\\\\s\\\\S]*?\\\\*\\\\/\\\\s*)*[\"']${name}[\"']`,\n );\n}\n\nfunction head(file: string): string | null {\n try {\n return readFileSync(file, \"utf-8\").slice(0, 2048);\n } catch {\n return null;\n }\n}\n\nexport function clientEntries(sourceDir: string): string[] {\n const found: string[] = [];\n\n const walk = (dir: string): void => {\n let entries;\n\n try {\n entries = readdirSync(dir, { withFileTypes: true });\n } catch {\n return;\n }\n\n for (const entry of entries) {\n if (entry.name === \"node_modules\" || entry.name.startsWith(\".\")) continue;\n\n const path = join(dir, entry.name);\n\n if (entry.isDirectory()) {\n walk(path);\n\n continue;\n }\n\n if (!SOURCE.test(entry.name) || entry.name.endsWith(\".d.ts\")) continue;\n\n const source = head(path);\n\n if (source !== null && CLIENT.test(source)) found.push(path);\n }\n };\n\n walk(sourceDir);\n\n return found.sort();\n}\n\n/**\n * The engine's own \"use client\" modules: Link, Form, the nuqs adapter, the\n * boundaries. The package is excluded from pre-bundling so that a payload's\n * reference to one of these files is the file itself, and an excluded\n * package is one the scanner does not enter - so the dependencies these\n * modules carry (the nuqs adapter's, for one) were found only when a page\n * first loaded the module, and that page re-optimised under itself.\n */\nexport function engineClientEntries(): string[] {\n return clientEntries(fileURLToPath(new URL(\"./js/\", import.meta.url)));\n}\n\n/**\n * What the scanner may follow from those files: the browser's graph, not\n * the server's.\n *\n * A client component imports a \"use server\" module to call its actions. In\n * the browser build that module is replaced by a proxy of its exports, and\n * nothing below it - the database, the runtime's own `bun` modules - is\n * ever bundled. The scanner runs before that replacement exists, follows\n * the raw file, reaches `import { SQL } from \"bun\"`, and gives up on the\n * whole scan: \"Failed to run dependency scan\", nothing pre-bundled, and the\n * re-optimisation the entries were meant to prevent. So an action module\n * scans as empty, and a runtime builtin is external here as it is in the\n * server bundles.\n */\nexport function clientScanPlugin(): {\n name: string;\n resolveId(id: string): { id: string; external: true } | null;\n load(id: string): string | null;\n} {\n return {\n name: \"rsc-kit:client-scan\",\n resolveId(id) {\n return id === \"bun\" || id.startsWith(\"bun:\")\n ? { id, external: true }\n : null;\n },\n load(id) {\n const file = id.split(\"?\")[0];\n\n if (file.includes(\"/node_modules/\") || !SOURCE.test(file)) return null;\n\n const source = head(file);\n\n return source !== null && SERVER.test(source) ? \"export {};\" : null;\n },\n };\n}\n"]}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Server files that import a client library.
3
+ *
4
+ * A server component may import a client library - `<SheetClose>` from
5
+ * base-ui rendered inside server output is legal here and in Next.js. What
6
+ * is worth a line is the shape that cost an afternoon: a file with no
7
+ * "use client" that only wraps client components (a shadcn ui/ file that
8
+ * lost its directive in a port) imported from the server, so the library's
9
+ * internals run in the rsc environment. The build cannot tell the two
10
+ * apart; it can say which server files do it, and with which packages, so
11
+ * the person reading can.
12
+ *
13
+ * Read off the rsc environment's module graph after it is built. A file with
14
+ * the directive never appears: in that graph it is already a reference, and
15
+ * a reference imports nothing.
16
+ */
17
+ export interface ClientLibraryImport {
18
+ /** The server file, relative to the source dir. */
19
+ file: string;
20
+ /** The client packages it imports, by name. */
21
+ packages: string[];
22
+ /** The first app file that imports it, relative to the source dir - or null for a route file. */
23
+ from: string | null;
24
+ }
25
+ export interface ModuleGraph {
26
+ moduleIds(): Iterable<string>;
27
+ importedIds(id: string): readonly string[];
28
+ importers(id: string): readonly string[];
29
+ }
30
+ /** The package a node_modules id belongs to, or null for anything else. */
31
+ export declare function packageOf(id: string): string | null;
32
+ export declare function serverImportsOfClientPackages(graph: ModuleGraph, options: {
33
+ sourceDir: string;
34
+ clientPackages: Iterable<string>;
35
+ ignore?: Iterable<string>;
36
+ }): ClientLibraryImport[];
@@ -0,0 +1,36 @@
1
+ const PACKAGE = /\/node_modules\/(@[^/]+\/[^/]+|[^/@][^/]*)/;
2
+ /** The package a node_modules id belongs to, or null for anything else. */
3
+ export function packageOf(id) {
4
+ const match = PACKAGE.exec(id.split("?")[0] ?? id);
5
+ return match ? match[1] : null;
6
+ }
7
+ export function serverImportsOfClientPackages(graph, options) {
8
+ const root = options.sourceDir.replace(/\/+$/, "") + "/";
9
+ const client = new Set(options.clientPackages);
10
+ const ignore = new Set(options.ignore ?? []);
11
+ const isApp = (id) => id.startsWith(root) &&
12
+ !id.includes("/node_modules/") &&
13
+ !id.startsWith("\0");
14
+ const relative = (id) => (id.split("?")[0] ?? id).slice(root.length);
15
+ const out = [];
16
+ for (const id of graph.moduleIds()) {
17
+ if (!isApp(id))
18
+ continue;
19
+ const packages = new Set();
20
+ for (const imported of graph.importedIds(id)) {
21
+ const name = packageOf(imported);
22
+ if (name && client.has(name) && !ignore.has(name))
23
+ packages.add(name);
24
+ }
25
+ if (packages.size === 0)
26
+ continue;
27
+ const from = graph.importers(id).find(isApp);
28
+ out.push({
29
+ file: relative(id),
30
+ packages: [...packages].sort(),
31
+ from: from ? relative(from) : null,
32
+ });
33
+ }
34
+ return out.sort((a, b) => a.file.localeCompare(b.file));
35
+ }
36
+ //# sourceMappingURL=clientImports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clientImports.js","sourceRoot":"","sources":["../src/clientImports.ts"],"names":[],"mappings":"AA+BA,MAAM,OAAO,GAAG,4CAA4C,CAAC;AAE7D,2EAA2E;AAC3E,MAAM,UAAU,SAAS,CAAC,EAAU;IAClC,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IAEnD,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,6BAA6B,CAC3C,KAAkB,EAClB,OAIC;IAED,MAAM,IAAI,GAAG,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC;IACzD,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;IAC/C,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;IAC7C,MAAM,KAAK,GAAG,CAAC,EAAU,EAAE,EAAE,CAC3B,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC;QACnB,CAAC,EAAE,CAAC,QAAQ,CAAC,gBAAgB,CAAC;QAC9B,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;IACvB,MAAM,QAAQ,GAAG,CAAC,EAAU,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC7E,MAAM,GAAG,GAA0B,EAAE,CAAC;IAEtC,KAAK,MAAM,EAAE,IAAI,KAAK,CAAC,SAAS,EAAE,EAAE,CAAC;QACnC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YAAE,SAAS;QAEzB,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;QAEnC,KAAK,MAAM,QAAQ,IAAI,KAAK,CAAC,WAAW,CAAC,EAAE,CAAC,EAAE,CAAC;YAC7C,MAAM,IAAI,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;YAEjC,IAAI,IAAI,IAAI,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC;gBAAE,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACxE,CAAC;QAED,IAAI,QAAQ,CAAC,IAAI,KAAK,CAAC;YAAE,SAAS;QAElC,MAAM,IAAI,GAAG,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAE7C,GAAG,CAAC,IAAI,CAAC;YACP,IAAI,EAAE,QAAQ,CAAC,EAAE,CAAC;YAClB,QAAQ,EAAE,CAAC,GAAG,QAAQ,CAAC,CAAC,IAAI,EAAE;YAC9B,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI;SACnC,CAAC,CAAC;IACL,CAAC;IAED,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;AAC1D,CAAC","sourcesContent":["/**\n * Server files that import a client library.\n *\n * A server component may import a client library - `<SheetClose>` from\n * base-ui rendered inside server output is legal here and in Next.js. What\n * is worth a line is the shape that cost an afternoon: a file with no\n * \"use client\" that only wraps client components (a shadcn ui/ file that\n * lost its directive in a port) imported from the server, so the library's\n * internals run in the rsc environment. The build cannot tell the two\n * apart; it can say which server files do it, and with which packages, so\n * the person reading can.\n *\n * Read off the rsc environment's module graph after it is built. A file with\n * the directive never appears: in that graph it is already a reference, and\n * a reference imports nothing.\n */\nexport interface ClientLibraryImport {\n /** The server file, relative to the source dir. */\n file: string;\n /** The client packages it imports, by name. */\n packages: string[];\n /** The first app file that imports it, relative to the source dir - or null for a route file. */\n from: string | null;\n}\n\nexport interface ModuleGraph {\n moduleIds(): Iterable<string>;\n importedIds(id: string): readonly string[];\n importers(id: string): readonly string[];\n}\n\nconst PACKAGE = /\\/node_modules\\/(@[^/]+\\/[^/]+|[^/@][^/]*)/;\n\n/** The package a node_modules id belongs to, or null for anything else. */\nexport function packageOf(id: string): string | null {\n const match = PACKAGE.exec(id.split(\"?\")[0] ?? id);\n\n return match ? match[1]! : null;\n}\n\nexport function serverImportsOfClientPackages(\n graph: ModuleGraph,\n options: {\n sourceDir: string;\n clientPackages: Iterable<string>;\n ignore?: Iterable<string>;\n },\n): ClientLibraryImport[] {\n const root = options.sourceDir.replace(/\\/+$/, \"\") + \"/\";\n const client = new Set(options.clientPackages);\n const ignore = new Set(options.ignore ?? []);\n const isApp = (id: string) =>\n id.startsWith(root) &&\n !id.includes(\"/node_modules/\") &&\n !id.startsWith(\"\\0\");\n const relative = (id: string) => (id.split(\"?\")[0] ?? id).slice(root.length);\n const out: ClientLibraryImport[] = [];\n\n for (const id of graph.moduleIds()) {\n if (!isApp(id)) continue;\n\n const packages = new Set<string>();\n\n for (const imported of graph.importedIds(id)) {\n const name = packageOf(imported);\n\n if (name && client.has(name) && !ignore.has(name)) packages.add(name);\n }\n\n if (packages.size === 0) continue;\n\n const from = graph.importers(id).find(isApp);\n\n out.push({\n file: relative(id),\n packages: [...packages].sort(),\n from: from ? relative(from) : null,\n });\n }\n\n return out.sort((a, b) => a.file.localeCompare(b.file));\n}\n"]}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The answer, in development, to a render that failed because Vite
3
+ * re-optimised a dependency underneath it.
4
+ *
5
+ * When the server environments' pre-bundles change - a new dependency was
6
+ * discovered, the lockfile changed - Vite throws ERR_OUTDATED_OPTIMIZED_DEP
7
+ * from the import that hit the old one, with a message ending "a page reload
8
+ * is going to ask for it". For the browser's own requests Vite answers 504
9
+ * and its client reloads; for a render running in the server module runner
10
+ * nothing does, and the visitor gets an error page for a condition that is
11
+ * over by the time they read it.
12
+ *
13
+ * So the page is asked to reload itself. A document gets a refresh; a
14
+ * payload request gets a 503 that is not a payload, which the router
15
+ * answers by loading the document instead - and the document reloads.
16
+ */
17
+ export declare const OUTDATED_OPTIMIZED_DEP = "ERR_OUTDATED_OPTIMIZED_DEP";
18
+ export declare function isOutdatedOptimizedDep(error: unknown): boolean;
19
+ export declare function outdatedDepResponse(request: Request): Response;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The answer, in development, to a render that failed because Vite
3
+ * re-optimised a dependency underneath it.
4
+ *
5
+ * When the server environments' pre-bundles change - a new dependency was
6
+ * discovered, the lockfile changed - Vite throws ERR_OUTDATED_OPTIMIZED_DEP
7
+ * from the import that hit the old one, with a message ending "a page reload
8
+ * is going to ask for it". For the browser's own requests Vite answers 504
9
+ * and its client reloads; for a render running in the server module runner
10
+ * nothing does, and the visitor gets an error page for a condition that is
11
+ * over by the time they read it.
12
+ *
13
+ * So the page is asked to reload itself. A document gets a refresh; a
14
+ * payload request gets a 503 that is not a payload, which the router
15
+ * answers by loading the document instead - and the document reloads.
16
+ */
17
+ export const OUTDATED_OPTIMIZED_DEP = "ERR_OUTDATED_OPTIMIZED_DEP";
18
+ export function isOutdatedOptimizedDep(error) {
19
+ return error?.code === OUTDATED_OPTIMIZED_DEP;
20
+ }
21
+ export function outdatedDepResponse(request) {
22
+ const headers = {
23
+ "Cache-Control": "no-store",
24
+ "Content-Type": "text/html; charset=utf-8",
25
+ };
26
+ if (request.headers.get("X-RSC")) {
27
+ return new Response("Dependencies were re-optimized; load the page again.", { status: 503, headers });
28
+ }
29
+ return new Response('<!doctype html><meta charset="utf-8"><meta http-equiv="refresh" content="0">' +
30
+ "<title>Reloading</title><p>Vite re-optimized dependencies; reloading…</p>", { status: 503, headers });
31
+ }
32
+ //# sourceMappingURL=devReload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devReload.js","sourceRoot":"","sources":["../src/devReload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,4BAA4B,CAAC;AAEnE,MAAM,UAAU,sBAAsB,CAAC,KAAc;IACnD,OAAQ,KAAkC,EAAE,IAAI,KAAK,sBAAsB,CAAC;AAC9E,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,OAAgB;IAClD,MAAM,OAAO,GAAG;QACd,eAAe,EAAE,UAAU;QAC3B,cAAc,EAAE,0BAA0B;KAC3C,CAAC;IAEF,IAAI,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;QACjC,OAAO,IAAI,QAAQ,CACjB,sDAAsD,EACtD,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,CACzB,CAAC;IACJ,CAAC;IAED,OAAO,IAAI,QAAQ,CACjB,8EAA8E;QAC5E,2EAA2E,EAC7E,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,CACzB,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The answer, in development, to a render that failed because Vite\n * re-optimised a dependency underneath it.\n *\n * When the server environments' pre-bundles change - a new dependency was\n * discovered, the lockfile changed - Vite throws ERR_OUTDATED_OPTIMIZED_DEP\n * from the import that hit the old one, with a message ending \"a page reload\n * is going to ask for it\". For the browser's own requests Vite answers 504\n * and its client reloads; for a render running in the server module runner\n * nothing does, and the visitor gets an error page for a condition that is\n * over by the time they read it.\n *\n * So the page is asked to reload itself. A document gets a refresh; a\n * payload request gets a 503 that is not a payload, which the router\n * answers by loading the document instead - and the document reloads.\n */\nexport const OUTDATED_OPTIMIZED_DEP = \"ERR_OUTDATED_OPTIMIZED_DEP\";\n\nexport function isOutdatedOptimizedDep(error: unknown): boolean {\n return (error as { code?: string } | null)?.code === OUTDATED_OPTIMIZED_DEP;\n}\n\nexport function outdatedDepResponse(request: Request): Response {\n const headers = {\n \"Cache-Control\": \"no-store\",\n \"Content-Type\": \"text/html; charset=utf-8\",\n };\n\n if (request.headers.get(\"X-RSC\")) {\n return new Response(\n \"Dependencies were re-optimized; load the page again.\",\n { status: 503, headers },\n );\n }\n\n return new Response(\n '<!doctype html><meta charset=\"utf-8\"><meta http-equiv=\"refresh\" content=\"0\">' +\n \"<title>Reloading</title><p>Vite re-optimized dependencies; reloading…</p>\",\n { status: 503, headers },\n );\n}\n"]}