@rsc-kit/mcp 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.
package/dist/answers.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { BuildReport } from './report.js';
1
+ import type { BuildReport } from "./report.js";
2
2
  /**
3
3
  * Every answer says how old it is.
4
4
  *
@@ -25,4 +25,17 @@ export declare function heaviestRoutes(report: BuildReport, builtAt: Date, now:
25
25
  * middleware; an agent adding a delete button needs to know that before it
26
26
  * trusts the id it was handed.
27
27
  */
28
+ /**
29
+ * Server files still importing cache from React. React's dedupes only inside
30
+ * a component render; in a guard, an action or an api route it calls straight
31
+ * through, silently - the helper runs twice and nothing says so but this.
32
+ */
33
+ export declare function reactCacheLines(report: BuildReport): string[];
34
+ /**
35
+ * Server files importing a client library. Legal for a server component; a
36
+ * file with no "use client" that only wraps client components (a shadcn ui/
37
+ * file that lost its directive) wants the directive back, so the library's
38
+ * internals stop running on the server.
39
+ */
40
+ export declare function clientImportLines(report: BuildReport): string[];
28
41
  export declare function actionLines(report: BuildReport): string[];
package/dist/answers.js CHANGED
@@ -5,17 +5,17 @@
5
5
  // them, so a vague sentence here becomes a wrong edit somewhere else — "this
6
6
  // route is dynamic" invites a fix, "this route reads cookies, which is why"
7
7
  // invites the right one.
8
- import { MEANING, routeFor } from './report.js';
8
+ import { MEANING, routeFor } from "./report.js";
9
9
  const kb = (bytes) => `${bytes < 10_000 ? (bytes / 1000).toFixed(1) : Math.round(bytes / 1000)} kB`;
10
10
  const age = (builtAt, now) => {
11
11
  const minutes = Math.round((now - builtAt.getTime()) / 60_000);
12
12
  if (minutes < 1)
13
- return 'just now';
13
+ return "just now";
14
14
  if (minutes < 60)
15
- return `${minutes} minute${minutes === 1 ? '' : 's'} ago`;
15
+ return `${minutes} minute${minutes === 1 ? "" : "s"} ago`;
16
16
  const hours = Math.round(minutes / 60);
17
17
  if (hours < 24)
18
- return `${hours} hour${hours === 1 ? '' : 's'} ago`;
18
+ return `${hours} hour${hours === 1 ? "" : "s"} ago`;
19
19
  return `${Math.round(hours / 24)} days ago`;
20
20
  };
21
21
  /**
@@ -31,15 +31,15 @@ export function asOf(builtAt, now) {
31
31
  export function listRoutes(report, builtAt, now) {
32
32
  const lines = [
33
33
  `${report.routes.length} routes and ${report.apis.length} api routes ${asOf(builtAt, now)}`,
34
- '',
34
+ "",
35
35
  ];
36
36
  // First, before the table, because it changes what the table means: these
37
37
  // rows are from a build that did not finish, and nothing below is deployed.
38
38
  if (report.totals.failed > 0) {
39
- lines.unshift(`THE LAST BUILD FAILED: ${report.totals.failed} route${report.totals.failed === 1 ? '' : 's'} refused. Each one's line below says what to change. Fix it and build again.`, '');
39
+ lines.unshift(`THE LAST BUILD FAILED: ${report.totals.failed} route${report.totals.failed === 1 ? "" : "s"} refused. Each one's line below says what to change. Fix it and build again.`, "");
40
40
  }
41
41
  for (const route of report.routes) {
42
- const size = route.clientJs === null ? '' : ` ${kb(route.clientJs)}`;
42
+ const size = route.clientJs === null ? "" : ` ${kb(route.clientJs)}`;
43
43
  lines.push(`${route.url}${size} — ${MEANING[route.type] ?? route.type}`);
44
44
  if (route.reason)
45
45
  lines.push(` ${route.reason}`);
@@ -47,20 +47,22 @@ export function listRoutes(report, builtAt, now) {
47
47
  lines.push(` ${route.note}`);
48
48
  }
49
49
  if (report.apis.length) {
50
- lines.push('', 'api routes:');
50
+ lines.push("", "api routes:");
51
51
  for (const api of report.apis) {
52
52
  lines.push(`${api.url} — ${MEANING[api.type] ?? api.type}`);
53
53
  if (api.reason)
54
54
  lines.push(` ${api.reason}`);
55
55
  }
56
56
  }
57
- lines.push('', `${report.totals.static} static, ${report.totals.partial} partial prerender, ${report.totals.dynamic} dynamic` +
58
- (report.totals.failed ? `, ${report.totals.failed} failed` : ''));
59
- lines.push('', ...actionLines(report));
60
- return lines.join('\n');
57
+ lines.push("", `${report.totals.static} static, ${report.totals.partial} partial prerender, ${report.totals.dynamic} dynamic` +
58
+ (report.totals.failed ? `, ${report.totals.failed} failed` : ""));
59
+ lines.push("", ...actionLines(report));
60
+ lines.push(...reactCacheLines(report));
61
+ lines.push(...clientImportLines(report));
62
+ return lines.join("\n");
61
63
  }
62
64
  function isPage(route) {
63
- return 'component' in route;
65
+ return "component" in route;
64
66
  }
65
67
  export function explainRoute(report, url, builtAt, now) {
66
68
  const route = routeFor(report, url);
@@ -68,10 +70,12 @@ export function explainRoute(report, url, builtAt, now) {
68
70
  // The urls, not just "not found": the caller has a url that does not exist,
69
71
  // and the most useful next thing is the ones that do.
70
72
  return (`No route for ${url} ${asOf(builtAt, now)}.\n\n` +
71
- 'Known urls:\n' +
72
- [...report.routes, ...report.apis].map((r) => ` ${r.url}`).join('\n'));
73
+ "Known urls:\n" +
74
+ [...report.routes, ...report.apis].map((r) => ` ${r.url}`).join("\n"));
73
75
  }
74
- const lines = [`${route.url} — ${MEANING[route.type] ?? route.type} ${asOf(builtAt, now)}`];
76
+ const lines = [
77
+ `${route.url} — ${MEANING[route.type] ?? route.type} ${asOf(builtAt, now)}`,
78
+ ];
75
79
  if (isPage(route)) {
76
80
  lines.push(`Rendered by ${route.component}.`);
77
81
  if (route.clientJs !== null) {
@@ -79,13 +83,13 @@ export function explainRoute(report, url, builtAt, now) {
79
83
  }
80
84
  }
81
85
  if (route.reason)
82
- lines.push('', `Why it is not stored whole: ${route.reason}`);
86
+ lines.push("", `Why it is not stored whole: ${route.reason}`);
83
87
  if (isPage(route) && route.warning)
84
- lines.push('', `Warning: ${route.warning}`);
85
- if (route.type === 'frozen') {
86
- lines.push('', 'Nothing to fix. It is rendered once at build time and served as a file.');
88
+ lines.push("", `Warning: ${route.warning}`);
89
+ if (route.type === "frozen") {
90
+ lines.push("", "Nothing to fix. It is rendered once at build time and served as a file.");
87
91
  }
88
- return lines.join('\n');
92
+ return lines.join("\n");
89
93
  }
90
94
  /**
91
95
  * The routes that are not stored, and why.
@@ -95,20 +99,20 @@ export function explainRoute(report, url, builtAt, now) {
95
99
  * out entirely rather than listed and dismissed.
96
100
  */
97
101
  export function whatIsDynamic(report, builtAt, now) {
98
- const pages = report.routes.filter((r) => r.type !== 'frozen');
99
- const apis = report.apis.filter((a) => a.type !== 'frozen');
102
+ const pages = report.routes.filter((r) => r.type !== "frozen");
103
+ const apis = report.apis.filter((a) => a.type !== "frozen");
100
104
  if (pages.length === 0 && apis.length === 0) {
101
105
  return `Every route is stored at build time ${asOf(builtAt, now)}. Nothing renders per request.`;
102
106
  }
103
107
  const lines = [
104
108
  `${pages.length + apis.length} of ${report.routes.length + report.apis.length} routes render per request ${asOf(builtAt, now)}:`,
105
- '',
109
+ "",
106
110
  ];
107
111
  for (const route of [...pages, ...apis]) {
108
112
  lines.push(`${route.url} — ${route.reason ?? MEANING[route.type] ?? route.type}`);
109
113
  }
110
- lines.push('', 'Reading the request is what makes a route dynamic: cookies(), headers(), searchParams(),', 'or connection() said deliberately. That is usually correct — a page whose content depends', 'on who is asking cannot be one stored file. Change it only if the read was accidental.');
111
- return lines.join('\n');
114
+ lines.push("", "Reading the request is what makes a route dynamic: cookies(), headers(), searchParams(),", "or connection() said deliberately. That is usually correct — a page whose content depends", "on who is asking cannot be one stored file. Change it only if the read was accidental.");
115
+ return lines.join("\n");
112
116
  }
113
117
  /** The heaviest routes, for the question that follows the size column. */
114
118
  export function heaviestRoutes(report, builtAt, now, top = 10) {
@@ -121,13 +125,13 @@ export function heaviestRoutes(report, builtAt, now, top = 10) {
121
125
  const lightest = weighed[weighed.length - 1].clientJs ?? 0;
122
126
  return [
123
127
  `Heaviest routes ${asOf(builtAt, now)}:`,
124
- '',
128
+ "",
125
129
  ...weighed.slice(0, top).map((r) => `${kb(r.clientJs ?? 0)} ${r.url}`),
126
- '',
130
+ "",
127
131
  `The lightest route ships ${kb(lightest)}, so the difference between them is`,
128
132
  `${kb((weighed[0].clientJs ?? 0) - lightest)} of client components — most of the rest is React itself,`,
129
- 'which every route pays for.',
130
- ].join('\n');
133
+ "which every route pays for.",
134
+ ].join("\n");
131
135
  }
132
136
  /**
133
137
  * The actions, and the one fact about each that nothing else states: whether
@@ -135,17 +139,55 @@ export function heaviestRoutes(report, builtAt, now, top = 10) {
135
139
  * middleware; an agent adding a delete button needs to know that before it
136
140
  * trusts the id it was handed.
137
141
  */
142
+ /**
143
+ * Server files still importing cache from React. React's dedupes only inside
144
+ * a component render; in a guard, an action or an api route it calls straight
145
+ * through, silently - the helper runs twice and nothing says so but this.
146
+ */
147
+ export function reactCacheLines(report) {
148
+ const files = report.reactCache;
149
+ if (!files || files.length === 0)
150
+ return [];
151
+ return [
152
+ "",
153
+ `${files.length} server ${files.length === 1 ? "file imports" : "files import"} cache from 'react': ${files.join(", ")}`,
154
+ "React's cache() dedupes only inside a component render; in a guard, an action or an api route it calls straight through. Import cache from @rsc-kit/core/cache, which spans the request.",
155
+ ];
156
+ }
157
+ /**
158
+ * Server files importing a client library. Legal for a server component; a
159
+ * file with no "use client" that only wraps client components (a shadcn ui/
160
+ * file that lost its directive) wants the directive back, so the library's
161
+ * internals stop running on the server.
162
+ */
163
+ export function clientImportLines(report) {
164
+ const found = report.clientImports;
165
+ if (!found || found.length === 0)
166
+ return [];
167
+ return [
168
+ "",
169
+ `${found.length} server ${found.length === 1 ? "file imports" : "files import"} a client library: ` +
170
+ found
171
+ .map((c) => `${c.file} (${c.packages.join(", ")}${c.from ? `; imported by ${c.from}` : ""})`)
172
+ .join("; "),
173
+ 'Legal for a server component. A file that only wraps client components wants "use client" - as shadcn ships it - so the server stops at the boundary.',
174
+ ];
175
+ }
138
176
  export function actionLines(report) {
139
177
  const actions = report.actions;
140
178
  if (!actions)
141
- return ['actions: not audited by this build (older @rsc-kit/core)'];
179
+ return ["actions: not audited by this build (older @rsc-kit/core)"];
142
180
  if (actions.length === 0)
143
- return ['actions: none'];
181
+ return ["actions: none"];
144
182
  const bare = actions.filter((a) => !a.client);
145
- const lines = [`actions: ${actions.length}, ${actions.length - bare.length} built from an action client`];
183
+ const lines = [
184
+ `actions: ${actions.length}, ${actions.length - bare.length} built from an action client`,
185
+ ];
146
186
  if (bare.length > 0) {
147
187
  lines.push(`${bare.length} run NO middleware — nothing checks who calls them: ` +
148
- bare.map((a) => `${a.name}${a.query ? ' (query)' : ''} in ${a.file}`).join(', '), 'Fine for a public action. For anything else, build it from an action client so the check cannot be forgotten — how_to({ topic: "action-client" }).');
188
+ bare
189
+ .map((a) => `${a.name}${a.query ? " (query)" : ""} in ${a.file}`)
190
+ .join(", "), 'Fine for a public action. For anything else, build it from an action client so the check cannot be forgotten — how_to({ topic: "action-client" }).');
149
191
  }
150
192
  return lines;
151
193
  }
@@ -1 +1 @@
1
- {"version":3,"file":"answers.js","sourceRoot":"","sources":["../src/answers.ts"],"names":[],"mappings":"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","sourcesContent":["// The answers, as text, with no protocol in them.\n//\n// Separated from the server so they can be tested by calling them, and so the\n// wording is reviewable in one place. An agent reads these as prose and acts on\n// them, so a vague sentence here becomes a wrong edit somewhere else — \"this\n// route is dynamic\" invites a fix, \"this route reads cookies, which is why\"\n// invites the right one.\n\nimport type { BuildReport, ReportedApiRoute, ReportedRoute } from './report.js'\nimport { MEANING, routeFor } from './report.js'\n\nconst kb = (bytes: number) => `${bytes < 10_000 ? (bytes / 1000).toFixed(1) : Math.round(bytes / 1000)} kB`\n\nconst age = (builtAt: Date, now: number): string => {\n const minutes = Math.round((now - builtAt.getTime()) / 60_000)\n\n if (minutes < 1) return 'just now'\n if (minutes < 60) return `${minutes} minute${minutes === 1 ? '' : 's'} ago`\n\n const hours = Math.round(minutes / 60)\n\n if (hours < 24) return `${hours} hour${hours === 1 ? '' : 's'} ago`\n\n return `${Math.round(hours / 24)} days ago`\n}\n\n/**\n * Every answer says how old it is.\n *\n * The one way this server misleads is by being confidently stale: it reports\n * the last build, and the file on disk may have changed since. Saying so on\n * every answer is cheaper than being wrong once.\n */\nexport function asOf(builtAt: Date, now: number): string {\n return `(from the last build, ${age(builtAt, now)})`\n}\n\nexport function listRoutes(report: BuildReport, builtAt: Date, now: number): string {\n const lines = [\n `${report.routes.length} routes and ${report.apis.length} api routes ${asOf(builtAt, now)}`,\n '',\n ]\n\n // First, before the table, because it changes what the table means: these\n // rows are from a build that did not finish, and nothing below is deployed.\n if (report.totals.failed > 0) {\n lines.unshift(\n `THE LAST BUILD FAILED: ${report.totals.failed} route${report.totals.failed === 1 ? '' : 's'} refused. Each one's line below says what to change. Fix it and build again.`,\n '',\n )\n }\n\n for (const route of report.routes) {\n const size = route.clientJs === null ? '' : ` ${kb(route.clientJs)}`\n\n lines.push(`${route.url}${size} — ${MEANING[route.type] ?? route.type}`)\n\n if (route.reason) lines.push(` ${route.reason}`)\n if (route.note) lines.push(` ${route.note}`)\n }\n\n if (report.apis.length) {\n lines.push('', 'api routes:')\n\n for (const api of report.apis) {\n lines.push(`${api.url} — ${MEANING[api.type] ?? api.type}`)\n\n if (api.reason) lines.push(` ${api.reason}`)\n }\n }\n\n lines.push(\n '',\n `${report.totals.static} static, ${report.totals.partial} partial prerender, ${report.totals.dynamic} dynamic` +\n (report.totals.failed ? `, ${report.totals.failed} failed` : ''),\n )\n\n lines.push('', ...actionLines(report))\n\n return lines.join('\\n')\n}\n\nfunction isPage(route: ReportedRoute | ReportedApiRoute): route is ReportedRoute {\n return 'component' in route\n}\n\nexport function explainRoute(\n report: BuildReport,\n url: string,\n builtAt: Date,\n now: number,\n): string {\n const route = routeFor(report, url)\n\n if (!route) {\n // The urls, not just \"not found\": the caller has a url that does not exist,\n // and the most useful next thing is the ones that do.\n return (\n `No route for ${url} ${asOf(builtAt, now)}.\\n\\n` +\n 'Known urls:\\n' +\n [...report.routes, ...report.apis].map((r) => ` ${r.url}`).join('\\n')\n )\n }\n\n const lines = [`${route.url} — ${MEANING[route.type] ?? route.type} ${asOf(builtAt, now)}`]\n\n if (isPage(route)) {\n lines.push(`Rendered by ${route.component}.`)\n\n if (route.clientJs !== null) {\n lines.push(`Ships ${kb(route.clientJs)} of javascript, gzipped.`)\n }\n }\n\n if (route.reason) lines.push('', `Why it is not stored whole: ${route.reason}`)\n\n if (isPage(route) && route.warning) lines.push('', `Warning: ${route.warning}`)\n\n if (route.type === 'frozen') {\n lines.push(\n '',\n 'Nothing to fix. It is rendered once at build time and served as a file.',\n )\n }\n\n return lines.join('\\n')\n}\n\n/**\n * The routes that are not stored, and why.\n *\n * The question behind most of the others — someone asking \"why is my site\n * slow\" wants this list, not the whole table. Routes that are fine are left\n * out entirely rather than listed and dismissed.\n */\nexport function whatIsDynamic(report: BuildReport, builtAt: Date, now: number): string {\n const pages = report.routes.filter((r) => r.type !== 'frozen')\n const apis = report.apis.filter((a) => a.type !== 'frozen')\n\n if (pages.length === 0 && apis.length === 0) {\n return `Every route is stored at build time ${asOf(builtAt, now)}. Nothing renders per request.`\n }\n\n const lines = [\n `${pages.length + apis.length} of ${report.routes.length + report.apis.length} routes render per request ${asOf(builtAt, now)}:`,\n '',\n ]\n\n for (const route of [...pages, ...apis]) {\n lines.push(`${route.url} — ${route.reason ?? MEANING[route.type] ?? route.type}`)\n }\n\n lines.push(\n '',\n 'Reading the request is what makes a route dynamic: cookies(), headers(), searchParams(),',\n 'or connection() said deliberately. That is usually correct — a page whose content depends',\n 'on who is asking cannot be one stored file. Change it only if the read was accidental.',\n )\n\n return lines.join('\\n')\n}\n\n/** The heaviest routes, for the question that follows the size column. */\nexport function heaviestRoutes(report: BuildReport, builtAt: Date, now: number, top = 10): string {\n const weighed = report.routes\n .filter((r) => r.clientJs !== null)\n .sort((a, b) => (b.clientJs ?? 0) - (a.clientJs ?? 0))\n\n if (weighed.length === 0) {\n return `No route shipped measurable javascript ${asOf(builtAt, now)}.`\n }\n\n const lightest = weighed[weighed.length - 1].clientJs ?? 0\n\n return [\n `Heaviest routes ${asOf(builtAt, now)}:`,\n '',\n ...weighed.slice(0, top).map((r) => `${kb(r.clientJs ?? 0)} ${r.url}`),\n '',\n `The lightest route ships ${kb(lightest)}, so the difference between them is`,\n `${kb((weighed[0].clientJs ?? 0) - lightest)} of client components — most of the rest is React itself,`,\n 'which every route pays for.',\n ].join('\\n')\n}\n\n/**\n * The actions, and the one fact about each that nothing else states: whether\n * anything checks who calls it. A bare \"use server\" export runs with no\n * middleware; an agent adding a delete button needs to know that before it\n * trusts the id it was handed.\n */\nexport function actionLines(report: BuildReport): string[] {\n const actions = report.actions\n\n if (!actions) return ['actions: not audited by this build (older @rsc-kit/core)']\n if (actions.length === 0) return ['actions: none']\n\n const bare = actions.filter((a) => !a.client)\n const lines = [`actions: ${actions.length}, ${actions.length - bare.length} built from an action client`]\n\n if (bare.length > 0) {\n lines.push(\n `${bare.length} run NO middleware — nothing checks who calls them: ` +\n bare.map((a) => `${a.name}${a.query ? ' (query)' : ''} in ${a.file}`).join(', '),\n 'Fine for a public action. For anything else, build it from an action client so the check cannot be forgotten — how_to({ topic: \"action-client\" }).',\n )\n }\n\n return lines\n}\n"]}
1
+ {"version":3,"file":"answers.js","sourceRoot":"","sources":["../src/answers.ts"],"names":[],"mappings":"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","sourcesContent":["// The answers, as text, with no protocol in them.\n//\n// Separated from the server so they can be tested by calling them, and so the\n// wording is reviewable in one place. An agent reads these as prose and acts on\n// them, so a vague sentence here becomes a wrong edit somewhere else — \"this\n// route is dynamic\" invites a fix, \"this route reads cookies, which is why\"\n// invites the right one.\n\nimport type { BuildReport, ReportedApiRoute, ReportedRoute } from \"./report.js\";\nimport { MEANING, routeFor } from \"./report.js\";\n\nconst kb = (bytes: number) =>\n `${bytes < 10_000 ? (bytes / 1000).toFixed(1) : Math.round(bytes / 1000)} kB`;\n\nconst age = (builtAt: Date, now: number): string => {\n const minutes = Math.round((now - builtAt.getTime()) / 60_000);\n\n if (minutes < 1) return \"just now\";\n if (minutes < 60) return `${minutes} minute${minutes === 1 ? \"\" : \"s\"} ago`;\n\n const hours = Math.round(minutes / 60);\n\n if (hours < 24) return `${hours} hour${hours === 1 ? \"\" : \"s\"} ago`;\n\n return `${Math.round(hours / 24)} days ago`;\n};\n\n/**\n * Every answer says how old it is.\n *\n * The one way this server misleads is by being confidently stale: it reports\n * the last build, and the file on disk may have changed since. Saying so on\n * every answer is cheaper than being wrong once.\n */\nexport function asOf(builtAt: Date, now: number): string {\n return `(from the last build, ${age(builtAt, now)})`;\n}\n\nexport function listRoutes(\n report: BuildReport,\n builtAt: Date,\n now: number,\n): string {\n const lines = [\n `${report.routes.length} routes and ${report.apis.length} api routes ${asOf(builtAt, now)}`,\n \"\",\n ];\n\n // First, before the table, because it changes what the table means: these\n // rows are from a build that did not finish, and nothing below is deployed.\n if (report.totals.failed > 0) {\n lines.unshift(\n `THE LAST BUILD FAILED: ${report.totals.failed} route${report.totals.failed === 1 ? \"\" : \"s\"} refused. Each one's line below says what to change. Fix it and build again.`,\n \"\",\n );\n }\n\n for (const route of report.routes) {\n const size = route.clientJs === null ? \"\" : ` ${kb(route.clientJs)}`;\n\n lines.push(`${route.url}${size} — ${MEANING[route.type] ?? route.type}`);\n\n if (route.reason) lines.push(` ${route.reason}`);\n if (route.note) lines.push(` ${route.note}`);\n }\n\n if (report.apis.length) {\n lines.push(\"\", \"api routes:\");\n\n for (const api of report.apis) {\n lines.push(`${api.url} — ${MEANING[api.type] ?? api.type}`);\n\n if (api.reason) lines.push(` ${api.reason}`);\n }\n }\n\n lines.push(\n \"\",\n `${report.totals.static} static, ${report.totals.partial} partial prerender, ${report.totals.dynamic} dynamic` +\n (report.totals.failed ? `, ${report.totals.failed} failed` : \"\"),\n );\n\n lines.push(\"\", ...actionLines(report));\n lines.push(...reactCacheLines(report));\n lines.push(...clientImportLines(report));\n\n return lines.join(\"\\n\");\n}\n\nfunction isPage(\n route: ReportedRoute | ReportedApiRoute,\n): route is ReportedRoute {\n return \"component\" in route;\n}\n\nexport function explainRoute(\n report: BuildReport,\n url: string,\n builtAt: Date,\n now: number,\n): string {\n const route = routeFor(report, url);\n\n if (!route) {\n // The urls, not just \"not found\": the caller has a url that does not exist,\n // and the most useful next thing is the ones that do.\n return (\n `No route for ${url} ${asOf(builtAt, now)}.\\n\\n` +\n \"Known urls:\\n\" +\n [...report.routes, ...report.apis].map((r) => ` ${r.url}`).join(\"\\n\")\n );\n }\n\n const lines = [\n `${route.url} — ${MEANING[route.type] ?? route.type} ${asOf(builtAt, now)}`,\n ];\n\n if (isPage(route)) {\n lines.push(`Rendered by ${route.component}.`);\n\n if (route.clientJs !== null) {\n lines.push(`Ships ${kb(route.clientJs)} of javascript, gzipped.`);\n }\n }\n\n if (route.reason)\n lines.push(\"\", `Why it is not stored whole: ${route.reason}`);\n\n if (isPage(route) && route.warning)\n lines.push(\"\", `Warning: ${route.warning}`);\n\n if (route.type === \"frozen\") {\n lines.push(\n \"\",\n \"Nothing to fix. It is rendered once at build time and served as a file.\",\n );\n }\n\n return lines.join(\"\\n\");\n}\n\n/**\n * The routes that are not stored, and why.\n *\n * The question behind most of the others — someone asking \"why is my site\n * slow\" wants this list, not the whole table. Routes that are fine are left\n * out entirely rather than listed and dismissed.\n */\nexport function whatIsDynamic(\n report: BuildReport,\n builtAt: Date,\n now: number,\n): string {\n const pages = report.routes.filter((r) => r.type !== \"frozen\");\n const apis = report.apis.filter((a) => a.type !== \"frozen\");\n\n if (pages.length === 0 && apis.length === 0) {\n return `Every route is stored at build time ${asOf(builtAt, now)}. Nothing renders per request.`;\n }\n\n const lines = [\n `${pages.length + apis.length} of ${report.routes.length + report.apis.length} routes render per request ${asOf(builtAt, now)}:`,\n \"\",\n ];\n\n for (const route of [...pages, ...apis]) {\n lines.push(\n `${route.url} — ${route.reason ?? MEANING[route.type] ?? route.type}`,\n );\n }\n\n lines.push(\n \"\",\n \"Reading the request is what makes a route dynamic: cookies(), headers(), searchParams(),\",\n \"or connection() said deliberately. That is usually correct — a page whose content depends\",\n \"on who is asking cannot be one stored file. Change it only if the read was accidental.\",\n );\n\n return lines.join(\"\\n\");\n}\n\n/** The heaviest routes, for the question that follows the size column. */\nexport function heaviestRoutes(\n report: BuildReport,\n builtAt: Date,\n now: number,\n top = 10,\n): string {\n const weighed = report.routes\n .filter((r) => r.clientJs !== null)\n .sort((a, b) => (b.clientJs ?? 0) - (a.clientJs ?? 0));\n\n if (weighed.length === 0) {\n return `No route shipped measurable javascript ${asOf(builtAt, now)}.`;\n }\n\n const lightest = weighed[weighed.length - 1].clientJs ?? 0;\n\n return [\n `Heaviest routes ${asOf(builtAt, now)}:`,\n \"\",\n ...weighed.slice(0, top).map((r) => `${kb(r.clientJs ?? 0)} ${r.url}`),\n \"\",\n `The lightest route ships ${kb(lightest)}, so the difference between them is`,\n `${kb((weighed[0].clientJs ?? 0) - lightest)} of client components — most of the rest is React itself,`,\n \"which every route pays for.\",\n ].join(\"\\n\");\n}\n\n/**\n * The actions, and the one fact about each that nothing else states: whether\n * anything checks who calls it. A bare \"use server\" export runs with no\n * middleware; an agent adding a delete button needs to know that before it\n * trusts the id it was handed.\n */\n/**\n * Server files still importing cache from React. React's dedupes only inside\n * a component render; in a guard, an action or an api route it calls straight\n * through, silently - the helper runs twice and nothing says so but this.\n */\nexport function reactCacheLines(report: BuildReport): string[] {\n const files = report.reactCache;\n\n if (!files || files.length === 0) return [];\n\n return [\n \"\",\n `${files.length} server ${files.length === 1 ? \"file imports\" : \"files import\"} cache from 'react': ${files.join(\", \")}`,\n \"React's cache() dedupes only inside a component render; in a guard, an action or an api route it calls straight through. Import cache from @rsc-kit/core/cache, which spans the request.\",\n ];\n}\n\n/**\n * Server files importing a client library. Legal for a server component; a\n * file with no \"use client\" that only wraps client components (a shadcn ui/\n * file that lost its directive) wants the directive back, so the library's\n * internals stop running on the server.\n */\nexport function clientImportLines(report: BuildReport): string[] {\n const found = report.clientImports;\n\n if (!found || found.length === 0) return [];\n\n return [\n \"\",\n `${found.length} server ${found.length === 1 ? \"file imports\" : \"files import\"} a client library: ` +\n found\n .map(\n (c) =>\n `${c.file} (${c.packages.join(\", \")}${c.from ? `; imported by ${c.from}` : \"\"})`,\n )\n .join(\"; \"),\n 'Legal for a server component. A file that only wraps client components wants \"use client\" - as shadcn ships it - so the server stops at the boundary.',\n ];\n}\n\nexport function actionLines(report: BuildReport): string[] {\n const actions = report.actions;\n\n if (!actions)\n return [\"actions: not audited by this build (older @rsc-kit/core)\"];\n if (actions.length === 0) return [\"actions: none\"];\n\n const bare = actions.filter((a) => !a.client);\n const lines = [\n `actions: ${actions.length}, ${actions.length - bare.length} built from an action client`,\n ];\n\n if (bare.length > 0) {\n lines.push(\n `${bare.length} run NO middleware — nothing checks who calls them: ` +\n bare\n .map((a) => `${a.name}${a.query ? \" (query)\" : \"\"} in ${a.file}`)\n .join(\", \"),\n 'Fine for a public action. For anything else, build it from an action client so the check cannot be forgotten — how_to({ topic: \"action-client\" }).',\n );\n }\n\n return lines;\n}\n"]}
package/dist/recipes.js CHANGED
@@ -603,7 +603,7 @@ A page's \`params\` and \`searchParams\` props are promises for the same reason.
603
603
  The build says which call did it, per route:
604
604
 
605
605
  ◐ /locale 85 kB
606
- dynamic — called cookies(), headers()
606
+ cookies(), headers() stream per request; the rest is stored
607
607
 
608
608
  That is usually correct — a page whose content depends on who is asking cannot
609
609
  be one stored file. Change it only when the read was accidental.
@@ -749,11 +749,16 @@ IMPORTS
749
749
  redirect() / notFound() -> @rsc-kit/core/redirect / @rsc-kit/core/not-found
750
750
  revalidatePath/Tag -> revalidate('tag') on a section() - targeted, rides back with the action
751
751
  Metadata -> @rsc-kit/core/metadata (metadataBase, openGraph, twitter, icons as-is)
752
+ app/robots.ts, app/sitemap.ts -> the same files and shapes; app/llms.ts beside them (how_to seo-files)
752
753
  next/font -> Fontsource (how_to fonts)
753
754
  next/image -> unpic or vite-imagetools (how_to images)
754
755
  next/script -> a <script> tag (how_to scripts)
755
756
  NEXT_PUBLIC_* -> VITE_* via import.meta.env; server vars stay process.env
756
757
  next-safe-action -> createActionClient() (how_to action-client); returnValidationErrors -> return fieldErrors({...})
758
+ cache from 'react' -> cache from @rsc-kit/core/cache: React's dedupes only inside a render; this one
759
+ spans the request (guards, actions, api routes). The build names files still on React's
760
+ @react-email/render, renderToString in an action -> the same call, in a module that starts with "use ssr"
761
+ (how_to emails). Next gets away with it only for externalised packages; here it is explicit
757
762
 
758
763
  DIFFERENT ON PURPOSE
759
764
  - No export const dynamic / revalidate = 60. A page is frozen unless it READS
@@ -768,11 +773,115 @@ DIFFERENT ON PURPOSE
768
773
  the top of each file, as shipped; without it the server evaluates the
769
774
  library's internals for nothing.
770
775
 
771
- ORDER: scaffold -> copy src/app -> fix imports -> typecheck -> build and READ
772
- the output (a cookies() in a layout makes everything dynamic; the build says
773
- so) -> decide each action the build lists as running no middleware -> check.
776
+ ORDER: scaffold -> copy src/app -> fix imports -> build (it typechecks first,
777
+ so a Link to a route that does not exist fails here) and READ the output: a
778
+ route that is not names what streams and from which component (a cookies()
779
+ in a layout reaches every page; the build says so) -> decide each action the
780
+ build lists as running no middleware -> check.
774
781
 
775
782
  Full guide: read_guide({ slug: 'coming-from-next' }).`,
783
+ },
784
+ {
785
+ topic: 'emails',
786
+ summary: 'Render React to HTML on the server - an email, a PDF, a feed - from an action or a route, with "use ssr"',
787
+ body: `@react-email/render, renderToString, anything on react-dom/server, called from
788
+ a server action or a route, fails: "react-dom/server is not supported in React
789
+ Server Components". React means it: where server components render, react is
790
+ the server-only build - the renderer needs the client build's internals, and
791
+ the components it would render import that same react (no useState, no
792
+ useContext). No alias fixes it. The rendering has to run in the ssr
793
+ environment, the one that turns pages into HTML for the browser.
794
+
795
+ Put the rendering - the template AND the call that renders it - in a module
796
+ that starts with "use ssr". Everything else imports it normally:
797
+
798
+ \`\`\`tsx
799
+ // src/lib/email/render.tsx
800
+ "use ssr";
801
+ import { render } from '@react-email/render'
802
+ import { OtpEmail } from './otp-email'
803
+
804
+ export async function renderOtpEmail(code: string) {
805
+ const email = <OtpEmail code={code} />
806
+ const [html, text] = await Promise.all([render(email), render(email, { plainText: true })])
807
+ return { html, text }
808
+ }
809
+ \`\`\`
810
+
811
+ \`\`\`ts
812
+ // src/lib/email/send-otp.ts - a plain server module, called from the action
813
+ import { renderOtpEmail } from './render'
814
+ export async function sendOtpEmail(to: string, code: string) {
815
+ const { html, text } = await renderOtpEmail(code)
816
+ await transporter.sendMail({ to, subject: 'Your code', html, text })
817
+ }
818
+ \`\`\`
819
+
820
+ Where server components render, the build replaces the module with async
821
+ proxies of its exports that call across - what "use client" does for a
822
+ component, in the other direction. Same process; dev and build; nothing to
823
+ configure.
824
+
825
+ RULES
826
+ - Exports are async functions. The call crosses environments, so the answer is
827
+ a promise. A sync function, a value, a class, export { } or export * is
828
+ refused at build with its name. Types are fine.
829
+ - Pass DATA across, not elements: renderOtpEmail(code), never
830
+ render(<OtpEmail/>) from the caller. An element built on the calling side
831
+ carries components from that side's react, and they render with no hooks.
832
+ - The module's imports are the ssr side's: @react-email/components,
833
+ react-dom/server, a PDF or Markdown renderer. Keep the module to rendering;
834
+ the database call belongs on the calling side.
835
+
836
+ Imported react-dom/server directly (through a library, usually)? It now throws
837
+ the fix in its message, naming the app file that pulled it in, and the build
838
+ warns once with the same. Do NOT alias react-dom/server, externalise react, or
839
+ move the action out of the app - the directive is the whole fix.
840
+
841
+ Full guide: read_guide({ slug: 'emails' }).`,
842
+ },
843
+ {
844
+ topic: 'seo-files',
845
+ summary: 'robots.txt, sitemap.xml and llms.txt from a file beside the root layout - the shapes Next uses, stored at build when they can be',
846
+ body: `Files beside the root layout, named for what they answer:
847
+ src/app/robots.ts -> /robots.txt default export returns MetadataRoute.Robots
848
+ src/app/sitemap.ts -> /sitemap.xml default export returns MetadataRoute.Sitemap (an array)
849
+ src/app/llms.ts -> /llms.txt default export returns MetadataRoute.Llms
850
+ src/app/llms-full.ts -> /llms-full.txt default export returns a string
851
+ The types: import type { MetadataRoute } from '@rsc-kit/core/metadata'. The
852
+ same names and shapes as Next's app/robots.ts and app/sitemap.ts; copy them.
853
+
854
+ \`\`\`ts
855
+ // src/app/robots.ts
856
+ export default function robots(): MetadataRoute.Robots {
857
+ return { rules: [{ userAgent: '*', allow: '/', disallow: ['/api/'] }], sitemap: '/sitemap.xml' }
858
+ }
859
+ // src/app/sitemap.ts
860
+ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
861
+ const posts = await db.post.findMany()
862
+ return [{ url: '/', priority: 1 }, ...posts.map((p) => ({ url: \`/blog/\${p.slug}\`, lastModified: p.updatedAt }))]
863
+ }
864
+ // src/app/llms.ts
865
+ export default function llms(): MetadataRoute.Llms {
866
+ return { title: 'Acme', summary: 'What it is.', sections: [{ title: 'Pages', links: [{ title: 'Pricing', url: '/pricing' }] }] }
867
+ }
868
+ \`\`\`
869
+
870
+ A relative url is made absolute with the root layout's metadataBase; without
871
+ one it is a build error. Any of them may return a string, served as written.
872
+
873
+ Each becomes an api route, so: one that reads nothing per request (the
874
+ database is fine) is stored at build and served from the file; one that reads
875
+ cookies() or awaits connection() runs per request. No middleware runs for
876
+ them - a root guard must not 401 the crawler. The url is typed
877
+ (route('/sitemap.xml')).
878
+
879
+ A file as written beside the root layout is served at the root as it is:
880
+ robots.txt, sitemap.xml, sitemap-*.xml, llms.txt, llms-full.txt, humans.txt,
881
+ security.txt, ads.txt. A file and a function for the same url is a build
882
+ error. Do NOT put these in public/ and do NOT write a route.ts for them.
883
+
884
+ Full guide: read_guide({ slug: 'seo-files' }).`,
776
885
  },
777
886
  {
778
887
  topic: 'images',
@@ -802,6 +911,12 @@ import heroSrc from '../hero.png?w=800&format=webp'
802
911
  <img srcSet={hero} src={heroSrc} sizes="(min-width: 800px) 800px, 100vw" width={800} height={600} alt="..." />
803
912
  \`\`\`
804
913
 
914
+ Declare the query tails in src/images.d.ts, or the build's typecheck stops on
915
+ the imports (a pattern may hold ONE *, so '*?*' matches nothing):
916
+
917
+ declare module '*&as=srcset' { const srcset: string; export default srcset }
918
+ declare module '*&format=webp' { const url: string; export default url }
919
+
805
920
  Hundreds of files in the repo: that is a CDN's job; move them and use unpic.
806
921
  An icon or a logo: a plain <img>, or inline the svg.
807
922