@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 +14 -1
- package/dist/answers.js +76 -34
- package/dist/answers.js.map +1 -1
- package/dist/recipes.js +119 -4
- package/dist/recipes.js.map +1 -1
- package/dist/report.d.ts +12 -0
- package/dist/report.js +17 -15
- package/dist/report.js.map +1 -1
- package/guides/coming-from-next.md +9 -4
- package/guides/emails.md +90 -0
- package/guides/errors.md +5 -1
- package/guides/images.md +10 -4
- package/guides/index.json +10 -0
- package/guides/installation.md +8 -9
- package/guides/mcp.md +1 -1
- package/guides/metadata.md +1 -1
- package/guides/offline.md +8 -0
- package/guides/routing.md +7 -1
- package/guides/seo-files.md +95 -0
- package/guides/typed-routes.md +9 -0
- package/guides/view-transitions.md +53 -7
- package/package.json +1 -1
package/dist/answers.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BuildReport } from
|
|
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
|
|
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
|
|
13
|
+
return "just now";
|
|
14
14
|
if (minutes < 60)
|
|
15
|
-
return `${minutes} minute${minutes === 1 ?
|
|
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 ?
|
|
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 ?
|
|
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 ?
|
|
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(
|
|
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(
|
|
58
|
-
(report.totals.failed ? `, ${report.totals.failed} failed` :
|
|
59
|
-
lines.push(
|
|
60
|
-
|
|
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
|
|
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
|
-
|
|
72
|
-
[...report.routes, ...report.apis].map((r) => ` ${r.url}`).join(
|
|
73
|
+
"Known urls:\n" +
|
|
74
|
+
[...report.routes, ...report.apis].map((r) => ` ${r.url}`).join("\n"));
|
|
73
75
|
}
|
|
74
|
-
const lines = [
|
|
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(
|
|
86
|
+
lines.push("", `Why it is not stored whole: ${route.reason}`);
|
|
83
87
|
if (isPage(route) && route.warning)
|
|
84
|
-
lines.push(
|
|
85
|
-
if (route.type ===
|
|
86
|
-
lines.push(
|
|
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(
|
|
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 !==
|
|
99
|
-
const apis = report.apis.filter((a) => a.type !==
|
|
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(
|
|
111
|
-
return lines.join(
|
|
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
|
-
|
|
130
|
-
].join(
|
|
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 [
|
|
179
|
+
return ["actions: not audited by this build (older @rsc-kit/core)"];
|
|
142
180
|
if (actions.length === 0)
|
|
143
|
-
return [
|
|
181
|
+
return ["actions: none"];
|
|
144
182
|
const bare = actions.filter((a) => !a.client);
|
|
145
|
-
const lines = [
|
|
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
|
|
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
|
}
|
package/dist/answers.js.map
CHANGED
|
@@ -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
|
-
|
|
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 ->
|
|
772
|
-
|
|
773
|
-
|
|
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
|
|