@rsc-kit/core 0.16.2 → 0.17.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 (71) hide show
  1. package/dist/appAssets.d.ts +4 -1
  2. package/dist/appAssets.js +55 -19
  3. package/dist/appAssets.js.map +1 -1
  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/js/Form.js +54 -6
  19. package/dist/js/Form.js.map +1 -1
  20. package/dist/js/Link.d.ts +0 -3
  21. package/dist/js/Link.js +3 -6
  22. package/dist/js/Link.js.map +1 -1
  23. package/dist/js/SegmentBoundary.d.ts +38 -2
  24. package/dist/js/SegmentBoundary.js +51 -13
  25. package/dist/js/SegmentBoundary.js.map +1 -1
  26. package/dist/js/createViteRscApp.js +72 -11
  27. package/dist/js/createViteRscApp.js.map +1 -1
  28. package/dist/js/devNotice.d.ts +13 -0
  29. package/dist/js/devNotice.js +55 -0
  30. package/dist/js/devNotice.js.map +1 -0
  31. package/dist/js/fallbackReport.d.ts +2 -0
  32. package/dist/js/fallbackReport.js +51 -0
  33. package/dist/js/fallbackReport.js.map +1 -0
  34. package/dist/js/navigate.d.ts +1 -1
  35. package/dist/js/navigate.js +55 -23
  36. package/dist/js/navigate.js.map +1 -1
  37. package/dist/js/segmentStore.d.ts +2 -0
  38. package/dist/js/segmentStore.js +21 -1
  39. package/dist/js/segmentStore.js.map +1 -1
  40. package/dist/js/staleAssets.d.ts +17 -0
  41. package/dist/js/staleAssets.js +48 -0
  42. package/dist/js/staleAssets.js.map +1 -0
  43. package/dist/js/standardSchema.d.ts +6 -0
  44. package/dist/js/standardSchema.js +19 -1
  45. package/dist/js/standardSchema.js.map +1 -1
  46. package/dist/js/useLinkStatus.d.ts +14 -1
  47. package/dist/js/useLinkStatus.js +15 -1
  48. package/dist/js/useLinkStatus.js.map +1 -1
  49. package/dist/js/useSearchParams.d.ts +6 -0
  50. package/dist/js/useSearchParams.js +16 -3
  51. package/dist/js/useSearchParams.js.map +1 -1
  52. package/dist/metadata.d.ts +94 -15
  53. package/dist/metadata.js.map +1 -1
  54. package/dist/metadataRoutes.d.ts +41 -0
  55. package/dist/metadataRoutes.js +138 -0
  56. package/dist/metadataRoutes.js.map +1 -0
  57. package/dist/prerender.js +43 -10
  58. package/dist/prerender.js.map +1 -1
  59. package/dist/reactCache.d.ts +1 -0
  60. package/dist/reactCache.js +52 -0
  61. package/dist/reactCache.js.map +1 -0
  62. package/dist/request.d.ts +18 -1
  63. package/dist/request.js +91 -5
  64. package/dist/request.js.map +1 -1
  65. package/dist/useSsr.d.ts +19 -0
  66. package/dist/useSsr.js +104 -0
  67. package/dist/useSsr.js.map +1 -0
  68. package/dist/vite.d.ts +48 -11
  69. package/dist/vite.js +526 -26
  70. package/dist/vite.js.map +1 -1
  71. package/package.json +1 -1
@@ -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":"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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 {\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,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":"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","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"]}