@rsc-kit/core 0.16.0 → 0.16.2
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/README.md +1 -1
- package/dist/apiPrerender.d.ts +2 -0
- package/dist/apiPrerender.js +111 -81
- package/dist/apiPrerender.js.map +1 -1
- package/dist/buildReport.d.ts +2 -0
- package/dist/buildReport.js.map +1 -1
- package/dist/files.d.ts +7 -0
- package/dist/files.js +113 -17
- package/dist/files.js.map +1 -1
- package/dist/host.d.ts +11 -0
- package/dist/host.js +62 -6
- package/dist/host.js.map +1 -1
- package/dist/js/errors.js +4 -0
- package/dist/js/errors.js.map +1 -1
- package/dist/js/navigate.js +23 -0
- package/dist/js/navigate.js.map +1 -1
- package/dist/manifest.d.ts +0 -14
- package/dist/manifest.js.map +1 -1
- package/dist/prerender.d.ts +18 -2
- package/dist/prerender.js +216 -138
- package/dist/prerender.js.map +1 -1
- package/dist/request.d.ts +3 -1
- package/dist/request.js +131 -45
- package/dist/request.js.map +1 -1
- package/dist/routing.d.ts +0 -7
- package/dist/routing.js +54 -20
- package/dist/routing.js.map +1 -1
- package/dist/vite.d.ts +27 -10
- package/dist/vite.js +550 -323
- package/dist/vite.js.map +1 -1
- package/package.json +2 -2
package/dist/prerender.js
CHANGED
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
//
|
|
15
15
|
// The file layout matches the one Laravel writes, so both hosts serve the same
|
|
16
16
|
// shapes and anything that reads them works for either.
|
|
17
|
-
import { withRedirect } from
|
|
18
|
-
import { withCache } from
|
|
19
|
-
import { requestReadBy, requestWasRead, withRequest } from
|
|
20
|
-
import { watchNondeterminism, whileRendering } from
|
|
17
|
+
import { withRedirect } from "./redirect.js";
|
|
18
|
+
import { withCache } from "./cache.js";
|
|
19
|
+
import { requestReadBy, requestReadWhere, requestWasRead, withRequest, } from "./request.js";
|
|
20
|
+
import { watchNondeterminism, whileRendering } from "./nondeterminism.js";
|
|
21
21
|
/** What a prerenderer needs from the built bundle, beyond serving a request. */
|
|
22
22
|
/**
|
|
23
23
|
* How long the root-fallback diagnostic waits before answering.
|
|
@@ -49,11 +49,11 @@ const DEFAULT_PRERENDER_CONCURRENCY = 4;
|
|
|
49
49
|
* else in the list is the app's, and needs the runtime.
|
|
50
50
|
*/
|
|
51
51
|
const RUNTIME_OWN = new Set([
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
"DocumentTitle",
|
|
53
|
+
"PathnameProvider",
|
|
54
|
+
"RouteErrorBoundary",
|
|
55
|
+
"SegmentBoundary",
|
|
56
|
+
"SlotBoundary",
|
|
57
57
|
]);
|
|
58
58
|
/**
|
|
59
59
|
* The one line the bootstrap would have run for the worker, for a document
|
|
@@ -62,9 +62,23 @@ const RUNTIME_OWN = new Set([
|
|
|
62
62
|
* no closing body tag is left alone.
|
|
63
63
|
*/
|
|
64
64
|
const WORKER_REGISTRATION = "<script>'serviceWorker'in navigator&&addEventListener('load',function(){navigator.serviceWorker.register('/sw.js').catch(function(){})})</script>";
|
|
65
|
+
/**
|
|
66
|
+
* Every `<link rel="stylesheet">` whose source the reader answers for,
|
|
67
|
+
* replaced by a `<style>` holding it. A link the reader declines - too big,
|
|
68
|
+
* or not the build's - is left as it was.
|
|
69
|
+
*/
|
|
70
|
+
export function withInlineStylesheets(html, read) {
|
|
71
|
+
return html.replace(/<link\b[^>]*\brel="stylesheet"[^>]*>/g, (tag) => {
|
|
72
|
+
const href = /\bhref="([^"]+)"/.exec(tag)?.[1];
|
|
73
|
+
const css = href ? read(href) : null;
|
|
74
|
+
return css === null ? tag : `<style>${css}</style>`;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
65
77
|
export function withWorkerRegistration(html) {
|
|
66
|
-
const at = html.lastIndexOf(
|
|
67
|
-
return at === -1
|
|
78
|
+
const at = html.lastIndexOf("</body>");
|
|
79
|
+
return at === -1
|
|
80
|
+
? html
|
|
81
|
+
: html.slice(0, at) + WORKER_REGISTRATION + html.slice(at);
|
|
68
82
|
}
|
|
69
83
|
/**
|
|
70
84
|
* The build's refusal to store a route it cannot answer ahead of time.
|
|
@@ -79,33 +93,35 @@ export class NotPrerenderable extends Error {
|
|
|
79
93
|
// Two different problems arrive here, and the advice for one is useless for
|
|
80
94
|
// the other. A page that reads the request above every boundary needs a
|
|
81
95
|
// boundary; a page whose render threw needs whatever it was reaching for.
|
|
82
|
-
const threw = routes.filter((r) => r.reason?.startsWith(
|
|
83
|
-
const unpaintable = routes.filter((r) => !r.reason?.startsWith(
|
|
96
|
+
const threw = routes.filter((r) => r.reason?.startsWith("could not be rendered"));
|
|
97
|
+
const unpaintable = routes.filter((r) => !r.reason?.startsWith("could not be rendered"));
|
|
84
98
|
const advice = [];
|
|
85
99
|
if (unpaintable.length > 0) {
|
|
86
|
-
advice.push(
|
|
87
|
-
|
|
88
|
-
unpaintable
|
|
89
|
-
|
|
90
|
-
|
|
100
|
+
advice.push("These read request data — params, headers, cookies, or the host — above\n" +
|
|
101
|
+
"every Suspense boundary, so nothing can paint without it:\n\n" +
|
|
102
|
+
unpaintable
|
|
103
|
+
.map((r) => ` ${r.url} — ${r.reason ?? "nothing to paint"}`)
|
|
104
|
+
.join("\n") +
|
|
105
|
+
"\n\nPut the part that waits inside <Suspense>, or add a loading.tsx beside\n" +
|
|
106
|
+
"the page, so there is something to store while the rest arrives.");
|
|
91
107
|
}
|
|
92
108
|
if (threw.length > 0) {
|
|
93
|
-
advice.push(
|
|
94
|
-
threw.map((r) => ` ${r.url} — ${r.reason}`).join(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
109
|
+
advice.push("These failed while rendering:\n\n" +
|
|
110
|
+
threw.map((r) => ` ${r.url} — ${r.reason}`).join("\n") +
|
|
111
|
+
"\n\nPrerendering runs your application code, so it needs whatever that code\n" +
|
|
112
|
+
"needs. If the page is fine and this machine simply cannot reach a\n" +
|
|
113
|
+
"database or an API, either give the build access or put `await connection()`\n" +
|
|
114
|
+
"before the read: the page then renders per request and the rest is still stored.\n\n" +
|
|
115
|
+
"Reaching for data through the host — `await rpc(...)` — avoids this\n" +
|
|
116
|
+
"entirely: the build stubs that call, so the page freezes a shell\n" +
|
|
117
|
+
"without the data being available.");
|
|
102
118
|
}
|
|
103
|
-
super(
|
|
104
|
-
this.name =
|
|
119
|
+
super("Some routes could not be prerendered.\n\n" + advice.join("\n\n") + "\n");
|
|
120
|
+
this.name = "NotPrerenderable";
|
|
105
121
|
this.routes = routes;
|
|
106
122
|
// Not enumerable: Vite prints an error's own properties under its stack,
|
|
107
123
|
// and this list is the message again, as an object dump.
|
|
108
|
-
Object.defineProperty(this,
|
|
124
|
+
Object.defineProperty(this, "routes", { value: routes, enumerable: false });
|
|
109
125
|
}
|
|
110
126
|
}
|
|
111
127
|
/**
|
|
@@ -117,16 +133,16 @@ export class NotPrerenderable extends Error {
|
|
|
117
133
|
* route matches, including the ones the app never listed.
|
|
118
134
|
*/
|
|
119
135
|
export function patternKey(route) {
|
|
120
|
-
const parts = route.segments.map((s) =>
|
|
121
|
-
return parts.join(
|
|
136
|
+
const parts = route.segments.map((s) => s.type === "static" ? s.value : `_${s.value}_`);
|
|
137
|
+
return parts.join("/") || "index";
|
|
122
138
|
}
|
|
123
139
|
/** Close a document the render was aborted in the middle of. */
|
|
124
140
|
export function closeDocument(html) {
|
|
125
141
|
let out = html;
|
|
126
142
|
if (!/<\/body>/i.test(out) && /<body/i.test(out))
|
|
127
|
-
out +=
|
|
143
|
+
out += "</body>";
|
|
128
144
|
if (!/<\/html>/i.test(out) && /<html/i.test(out))
|
|
129
|
-
out +=
|
|
145
|
+
out += "</html>";
|
|
130
146
|
return out;
|
|
131
147
|
}
|
|
132
148
|
/**
|
|
@@ -151,19 +167,19 @@ export function closeDocument(html) {
|
|
|
151
167
|
export function legend(results) {
|
|
152
168
|
const has = (type) => results.some((r) => r.type === type);
|
|
153
169
|
const lines = [];
|
|
154
|
-
if (has(
|
|
155
|
-
lines.push(
|
|
170
|
+
if (has("frozen")) {
|
|
171
|
+
lines.push(" \u25CB (Static) prerendered as static content");
|
|
156
172
|
}
|
|
157
|
-
if (has(
|
|
158
|
-
lines.push(
|
|
173
|
+
if (has("shell")) {
|
|
174
|
+
lines.push(" \u25D0 (Partial Prerender) prerendered as static HTML with dynamic server-streamed content");
|
|
159
175
|
}
|
|
160
|
-
if (has(
|
|
161
|
-
lines.push(
|
|
176
|
+
if (has("dynamic")) {
|
|
177
|
+
lines.push(" \u0192 (Dynamic) server-rendered on demand");
|
|
162
178
|
}
|
|
163
|
-
if (has(
|
|
164
|
-
lines.push(
|
|
179
|
+
if (has("error") || has("blocked")) {
|
|
180
|
+
lines.push(" \u2717 (Failed) did not render, or painted nothing — the build stops here");
|
|
165
181
|
}
|
|
166
|
-
return lines.join(
|
|
182
|
+
return lines.join("\n");
|
|
167
183
|
}
|
|
168
184
|
/**
|
|
169
185
|
* How much JavaScript a route makes the browser download.
|
|
@@ -178,7 +194,7 @@ export function legend(results) {
|
|
|
178
194
|
*/
|
|
179
195
|
export function clientJsSize(bytes) {
|
|
180
196
|
if (bytes === 0)
|
|
181
|
-
return
|
|
197
|
+
return "no js";
|
|
182
198
|
const kb = bytes / 1000;
|
|
183
199
|
return `${kb < 10 ? kb.toFixed(1) : Math.round(kb)} kB`;
|
|
184
200
|
}
|
|
@@ -201,27 +217,29 @@ export function notes(results) {
|
|
|
201
217
|
// root layout leaves: one cookies() in a header component and nothing on
|
|
202
218
|
// the site can be stored. The build cannot see which component read it -
|
|
203
219
|
// only that every page did - so it says what that usually means.
|
|
204
|
-
const pages = results.filter((r) => r.type && r.type !==
|
|
205
|
-
const reasons = new Set(pages.map((r) => r.reason ??
|
|
206
|
-
if (pages.length > 1 &&
|
|
220
|
+
const pages = results.filter((r) => r.type && r.type !== "error" && r.type !== "blocked");
|
|
221
|
+
const reasons = new Set(pages.map((r) => r.reason ?? ""));
|
|
222
|
+
if (pages.length > 1 &&
|
|
223
|
+
!pages.some((r) => r.type === "frozen") &&
|
|
224
|
+
reasons.size === 1) {
|
|
207
225
|
const [reason] = reasons;
|
|
208
226
|
parts.push(` Every route ${reason}. When one read makes every page dynamic it is usually\n` +
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
227
|
+
" in the root layout - a header reading the session, a locale from a cookie.\n" +
|
|
228
|
+
" Move that read into the component that needs it, under a <Suspense>, and\n" +
|
|
229
|
+
" the rest of the site can freeze around it.");
|
|
212
230
|
}
|
|
213
|
-
if (said(
|
|
231
|
+
if (said("rpc("))
|
|
214
232
|
parts.push(rpcNote());
|
|
215
|
-
return parts.join(
|
|
233
|
+
return parts.join("\n\n");
|
|
216
234
|
}
|
|
217
235
|
function rpcNote() {
|
|
218
|
-
return (
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
236
|
+
return (" A build has no backend to call. rpc() suspends instead of answering, so a page\n" +
|
|
237
|
+
" that reads through one ships a shell and finishes for whoever asks — which is\n" +
|
|
238
|
+
" almost always what you want, since that data is rarely the same for everyone.\n" +
|
|
239
|
+
"\n" +
|
|
240
|
+
" If a page really should be frozen, move the read out of rpc(). If it really\n" +
|
|
241
|
+
" should be per visitor, say so with await connection() and the intent is on the\n" +
|
|
242
|
+
" page rather than inferred from a call that happened to suspend.");
|
|
225
243
|
}
|
|
226
244
|
/**
|
|
227
245
|
* The one-line tally under the legend, in the legend's own words.
|
|
@@ -229,20 +247,22 @@ function rpcNote() {
|
|
|
229
247
|
export function summary(results) {
|
|
230
248
|
const count = (type) => results.filter((r) => r.type === type).length;
|
|
231
249
|
const parts = [];
|
|
232
|
-
if (count(
|
|
233
|
-
parts.push(`${count(
|
|
234
|
-
if (count(
|
|
235
|
-
parts.push(`${count(
|
|
236
|
-
if (count(
|
|
237
|
-
parts.push(`${count(
|
|
238
|
-
if (count(
|
|
239
|
-
parts.push(`${count(
|
|
240
|
-
return parts.join(
|
|
250
|
+
if (count("frozen"))
|
|
251
|
+
parts.push(`${count("frozen")} static`);
|
|
252
|
+
if (count("shell"))
|
|
253
|
+
parts.push(`${count("shell")} partial prerender`);
|
|
254
|
+
if (count("dynamic"))
|
|
255
|
+
parts.push(`${count("dynamic")} dynamic`);
|
|
256
|
+
if (count("error") + count("blocked"))
|
|
257
|
+
parts.push(`${count("error") + count("blocked")} failed`);
|
|
258
|
+
return parts.join(", ") || "nothing to store";
|
|
241
259
|
}
|
|
242
260
|
export function pathKey(url) {
|
|
243
|
-
const key = url.replace(/^\/+|\/+$/g,
|
|
244
|
-
if (key.split(
|
|
245
|
-
throw new Error(
|
|
261
|
+
const key = url.replace(/^\/+|\/+$/g, "") || "index";
|
|
262
|
+
if (key.split("/").some((segment) => segment === ".." || segment === ".")) {
|
|
263
|
+
throw new Error("Refusing to store " +
|
|
264
|
+
JSON.stringify(url) +
|
|
265
|
+
": the path leaves the output directory. " +
|
|
246
266
|
'A url segment came from generateStaticParams() or a route param and contains "..".');
|
|
247
267
|
}
|
|
248
268
|
return key;
|
|
@@ -256,16 +276,20 @@ export function pathKey(url) {
|
|
|
256
276
|
*/
|
|
257
277
|
export function urlFor(route, params) {
|
|
258
278
|
const parts = route.segments.map((segment) => {
|
|
259
|
-
if (segment.type ===
|
|
279
|
+
if (segment.type === "static")
|
|
260
280
|
return segment.value;
|
|
261
|
-
const value = params[segment.value] ??
|
|
262
|
-
if (segment.type !==
|
|
263
|
-
throw new Error(
|
|
264
|
-
|
|
281
|
+
const value = params[segment.value] ?? "";
|
|
282
|
+
if (segment.type !== "catchAll" && /[/\\]/.test(value)) {
|
|
283
|
+
throw new Error("The " +
|
|
284
|
+
segment.value +
|
|
285
|
+
" param is " +
|
|
286
|
+
JSON.stringify(value) +
|
|
287
|
+
", which is not one url " +
|
|
288
|
+
"segment. A value with a slash in it has to be a catch-all route, or be encoded.");
|
|
265
289
|
}
|
|
266
290
|
return value;
|
|
267
291
|
});
|
|
268
|
-
return
|
|
292
|
+
return "/" + parts.filter((p) => p !== "").join("/");
|
|
269
293
|
}
|
|
270
294
|
/**
|
|
271
295
|
* Every url this build should attempt, and the params each was built from.
|
|
@@ -278,15 +302,15 @@ export function urlFor(route, params) {
|
|
|
278
302
|
function placeholders(route) {
|
|
279
303
|
const params = {};
|
|
280
304
|
for (const segment of route.segments) {
|
|
281
|
-
if (segment.type !==
|
|
282
|
-
params[segment.value] =
|
|
305
|
+
if (segment.type !== "static")
|
|
306
|
+
params[segment.value] = "_";
|
|
283
307
|
}
|
|
284
308
|
return params;
|
|
285
309
|
}
|
|
286
310
|
export async function urlsToBuild(manifest, engine) {
|
|
287
311
|
const entries = [];
|
|
288
312
|
for (const route of manifest.routes) {
|
|
289
|
-
const parameterised = route.segments.some((s) => s.type !==
|
|
313
|
+
const parameterised = route.segments.some((s) => s.type !== "static");
|
|
290
314
|
if (!parameterised) {
|
|
291
315
|
entries.push({ route, params: {}, url: urlFor(route, {}) });
|
|
292
316
|
continue;
|
|
@@ -294,7 +318,11 @@ export async function urlsToBuild(manifest, engine) {
|
|
|
294
318
|
// A route that lists no urls still gets one attempt, with placeholder
|
|
295
319
|
// params, so it can ship a shell. Nothing in a shell varies by param.
|
|
296
320
|
if (!route.staticParams) {
|
|
297
|
-
entries.push({
|
|
321
|
+
entries.push({
|
|
322
|
+
route,
|
|
323
|
+
params: placeholders(route),
|
|
324
|
+
url: urlFor(route, placeholders(route)),
|
|
325
|
+
});
|
|
298
326
|
continue;
|
|
299
327
|
}
|
|
300
328
|
const sets = (await engine.getStaticParams?.(route.component)) ?? null;
|
|
@@ -313,7 +341,7 @@ export async function prerender(options) {
|
|
|
313
341
|
const { engine, write, version } = options;
|
|
314
342
|
const manifest = options.manifest ?? engine.manifest?.();
|
|
315
343
|
if (!manifest) {
|
|
316
|
-
throw new Error(
|
|
344
|
+
throw new Error("No route table. Pass `manifest`, or build with a plugin version that embeds one.");
|
|
317
345
|
}
|
|
318
346
|
const entries = await urlsToBuild(manifest, engine);
|
|
319
347
|
const results = new Array(entries.length);
|
|
@@ -350,15 +378,19 @@ export async function prerender(options) {
|
|
|
350
378
|
finally {
|
|
351
379
|
unwatch();
|
|
352
380
|
}
|
|
353
|
-
const refused = results.filter((r) => r.type ===
|
|
381
|
+
const refused = results.filter((r) => r.type === "blocked");
|
|
354
382
|
if (refused.length > 0) {
|
|
355
383
|
throw new NotPrerenderable(refused);
|
|
356
384
|
}
|
|
357
385
|
return results;
|
|
358
386
|
async function prerenderOne(route, params, url) {
|
|
359
387
|
const props = options.props ? options.props(route, params) : params;
|
|
360
|
-
const layouts = route.layouts.map((component) => ({
|
|
361
|
-
|
|
388
|
+
const layouts = route.layouts.map((component) => ({
|
|
389
|
+
component,
|
|
390
|
+
props: {},
|
|
391
|
+
}));
|
|
392
|
+
const unlistedNow = route.segments.some((seg) => seg.type !== "static") &&
|
|
393
|
+
!route.staticParams;
|
|
362
394
|
/**
|
|
363
395
|
* Why this route ships a shell rather than a whole page.
|
|
364
396
|
*
|
|
@@ -368,26 +400,27 @@ export async function prerender(options) {
|
|
|
368
400
|
*/
|
|
369
401
|
const shellReason = (calls) => {
|
|
370
402
|
if (calls.length)
|
|
371
|
-
return
|
|
403
|
+
return "dynamic — called " + calls.join(", ");
|
|
372
404
|
// One shell serving every url the route matches. generateStaticParams is
|
|
373
405
|
// what turns it into a page per url.
|
|
374
406
|
if (unlistedNow)
|
|
375
|
-
return
|
|
407
|
+
return "one shell for every url — add generateStaticParams to store each";
|
|
376
408
|
// Nothing reached for the request; the data simply took too long. The
|
|
377
409
|
// page is fine, it just finishes per visitor.
|
|
378
410
|
if (shell?.timedOut)
|
|
379
|
-
return
|
|
411
|
+
return "data took longer than the build budget";
|
|
380
412
|
return null;
|
|
381
413
|
};
|
|
382
414
|
const said = (type, reason) => ({
|
|
383
415
|
// A route standing in for many urls reports the pattern. Reporting the
|
|
384
416
|
// placeholder url instead prints `/posts/_`, which looks like a page.
|
|
385
|
-
url: unlistedNow ?
|
|
417
|
+
url: unlistedNow ? "/" + patternKey(route) : url,
|
|
386
418
|
component: route.component,
|
|
387
419
|
type,
|
|
388
420
|
reason,
|
|
389
421
|
});
|
|
390
|
-
const unlisted = route.segments.some((seg) => seg.type !==
|
|
422
|
+
const unlisted = route.segments.some((seg) => seg.type !== "static") &&
|
|
423
|
+
!route.staticParams;
|
|
391
424
|
// Classify by rendering, never by asking. The probe is cheap and cannot
|
|
392
425
|
// hang: anything still suspended when its budget expires is the answer.
|
|
393
426
|
//
|
|
@@ -401,19 +434,22 @@ export async function prerender(options) {
|
|
|
401
434
|
// must not share an answer just because they were built in the same run.
|
|
402
435
|
// No request, deliberately: a page that reads one is caught below rather
|
|
403
436
|
// than frozen holding whatever the build machine happened to send.
|
|
404
|
-
const [{ shell, redirected, readRequest, readBy }, nondeterministic] = await whileRendering(() => withRequest(null, () => withCache(() => withRedirect(async (taken) => {
|
|
437
|
+
const [{ shell, redirected, readRequest, readBy, readWhere }, nondeterministic,] = await whileRendering(() => withRequest(null, () => withCache(() => withRedirect(async (taken) => {
|
|
405
438
|
try {
|
|
406
439
|
return {
|
|
407
440
|
shell: await engine.handleRscPprShell(route.component, props, layouts, route.loadings, route.slots,
|
|
408
441
|
// Only when this shell serves one url. A parameterised route's
|
|
409
442
|
// shell is shared, so baking a url into it would put the wrong one
|
|
410
443
|
// on every page but the one that happened to be built.
|
|
411
|
-
unlistedNow ?
|
|
444
|
+
unlistedNow ? "" : url),
|
|
412
445
|
redirected: taken(),
|
|
413
446
|
readRequest: requestWasRead(),
|
|
414
447
|
// What reached for it, so the build can name the call rather than
|
|
415
448
|
// only report that the page is dynamic.
|
|
416
449
|
readBy: requestReadBy(),
|
|
450
|
+
// And from which component, so the message is a line to open
|
|
451
|
+
// rather than a category to search for.
|
|
452
|
+
readWhere: requestReadWhere(),
|
|
417
453
|
};
|
|
418
454
|
}
|
|
419
455
|
catch (error) {
|
|
@@ -424,7 +460,13 @@ export async function prerender(options) {
|
|
|
424
460
|
const refused = taken();
|
|
425
461
|
if (!refused)
|
|
426
462
|
throw error;
|
|
427
|
-
return {
|
|
463
|
+
return {
|
|
464
|
+
shell: null,
|
|
465
|
+
redirected: refused,
|
|
466
|
+
readRequest: requestWasRead(),
|
|
467
|
+
readBy: requestReadBy(),
|
|
468
|
+
readWhere: requestReadWhere(),
|
|
469
|
+
};
|
|
428
470
|
}
|
|
429
471
|
}))));
|
|
430
472
|
// A page that leaves rather than renders is not a build failure, and it is
|
|
@@ -452,9 +494,21 @@ export async function prerender(options) {
|
|
|
452
494
|
* the prerender.
|
|
453
495
|
*/
|
|
454
496
|
async function withRootFallbackChecked(result) {
|
|
455
|
-
if (result.type !==
|
|
497
|
+
if (result.type !== "shell")
|
|
498
|
+
return result;
|
|
499
|
+
if (!engine.handleRscPprShell)
|
|
456
500
|
return result;
|
|
457
|
-
|
|
501
|
+
// Only when the one fallback in the chain is the root layout's. A page
|
|
502
|
+
// with its own loading.tsx beside it has put the boundary exactly where
|
|
503
|
+
// the waiting is, and this used to tell it otherwise - "exactly one
|
|
504
|
+
// loading" is not the same as "only the root one".
|
|
505
|
+
const [only] = route.loadings;
|
|
506
|
+
const root = route.layouts[0];
|
|
507
|
+
const dirOf = (name) => name.slice(0, name.lastIndexOf("/"));
|
|
508
|
+
if (route.loadings.length !== 1 ||
|
|
509
|
+
!only ||
|
|
510
|
+
!root ||
|
|
511
|
+
dirOf(only) !== dirOf(root))
|
|
458
512
|
return result;
|
|
459
513
|
// A tenth of the shell budget, because this is a different question.
|
|
460
514
|
//
|
|
@@ -465,10 +519,10 @@ export async function prerender(options) {
|
|
|
465
519
|
// to learn what the first millisecond already said, which on this
|
|
466
520
|
// example was 40% of the entire prerender.
|
|
467
521
|
const withoutRoot = await withRequest(null, () => withCache(() => withRedirect(async () => engine.handleRscPprShell(route.component, props, layouts, [], route.slots, url, ROOT_FALLBACK_BUDGET_MS)))).catch(() => null);
|
|
468
|
-
if (withoutRoot && closeDocument(withoutRoot.shellHtml.trim()) ===
|
|
522
|
+
if (withoutRoot && closeDocument(withoutRoot.shellHtml.trim()) === "") {
|
|
469
523
|
result.warning =
|
|
470
|
-
|
|
471
|
-
|
|
524
|
+
"nothing painted without the root loading.tsx — the fallback the whole app shares " +
|
|
525
|
+
"is standing in for this page. Put a boundary where the waiting is.";
|
|
472
526
|
}
|
|
473
527
|
return result;
|
|
474
528
|
}
|
|
@@ -476,13 +530,16 @@ export async function prerender(options) {
|
|
|
476
530
|
// is the redirect, not a page. Rendering it per request re-derives a
|
|
477
531
|
// constant, and on a static host there would be nothing to derive it with.
|
|
478
532
|
if (redirected) {
|
|
479
|
-
await write(`${pathKey(url)}.redirect.json`, JSON.stringify({
|
|
480
|
-
|
|
533
|
+
await write(`${pathKey(url)}.redirect.json`, JSON.stringify({
|
|
534
|
+
status: redirected.status,
|
|
535
|
+
location: redirected.location,
|
|
536
|
+
}));
|
|
537
|
+
return said("frozen", `redirects to ${redirected.location}`);
|
|
481
538
|
}
|
|
482
539
|
if (!shell)
|
|
483
|
-
return said(
|
|
540
|
+
return said("error", "refused before it rendered");
|
|
484
541
|
if (shell.error)
|
|
485
|
-
return said(
|
|
542
|
+
return said("error", shell.error);
|
|
486
543
|
// Something failed while rendering. Not necessarily the page's fault — a
|
|
487
544
|
// build machine that cannot reach the database produces this, and so does a
|
|
488
545
|
// genuinely broken component — so it is not fatal. It is simply not
|
|
@@ -492,7 +549,7 @@ export async function prerender(options) {
|
|
|
492
549
|
// Rendering per request is the honest answer. It works as soon as whatever
|
|
493
550
|
// was missing is reachable, which at runtime it usually is.
|
|
494
551
|
if (shell.renderFailure) {
|
|
495
|
-
return said(
|
|
552
|
+
return said("blocked", `could not be rendered at build time: ${shell.renderFailure}`);
|
|
496
553
|
}
|
|
497
554
|
// A timeout is the ordinary path for a page that streams, not a failure:
|
|
498
555
|
// the probe hands the page a host global that never resolves, React
|
|
@@ -506,21 +563,29 @@ export async function prerender(options) {
|
|
|
506
563
|
// ship — a page that blocks above every boundary can only be rendered on
|
|
507
564
|
// demand. `reaches for the host` is the specific reason when both are
|
|
508
565
|
// true; a page that merely ran long says the other thing.
|
|
509
|
-
if (body ===
|
|
510
|
-
return said(
|
|
511
|
-
?
|
|
566
|
+
if (body === "") {
|
|
567
|
+
return said("blocked", (readRequest
|
|
568
|
+
? "reads " +
|
|
569
|
+
(readWhere.length
|
|
570
|
+
? readWhere
|
|
571
|
+
: readBy.length
|
|
572
|
+
? readBy
|
|
573
|
+
: ["the request"]).join(", ")
|
|
512
574
|
: shell.usedDynamicApis
|
|
513
|
-
?
|
|
514
|
-
:
|
|
515
|
-
|
|
516
|
-
|
|
575
|
+
? "reaches for the host"
|
|
576
|
+
: "blocks") +
|
|
577
|
+
" before anything can paint. Add a loading.tsx beside it, or put a " +
|
|
578
|
+
"<Suspense> above the waiting, and it has a skeleton to store.");
|
|
517
579
|
}
|
|
518
580
|
await writeShell(route, url, body, shell.postponed);
|
|
519
581
|
// Why it is a shell rather than a whole page. "◐ /orders" on its own
|
|
520
582
|
// leaves someone reading the build output to guess what did it, which is
|
|
521
583
|
// the question this line exists to answer.
|
|
522
|
-
const calls = [
|
|
523
|
-
|
|
584
|
+
const calls = [
|
|
585
|
+
...(shell.dynamicBecause ?? []),
|
|
586
|
+
...(readWhere.length ? readWhere : readBy),
|
|
587
|
+
];
|
|
588
|
+
return await withRootFallbackChecked(said("shell", shellReason(calls)));
|
|
524
589
|
}
|
|
525
590
|
// Warned, not refused.
|
|
526
591
|
//
|
|
@@ -541,10 +606,10 @@ export async function prerender(options) {
|
|
|
541
606
|
if (nondeterministic.length === 0)
|
|
542
607
|
return result;
|
|
543
608
|
result.warning =
|
|
544
|
-
`froze ${nondeterministic.join(
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
609
|
+
`froze ${nondeterministic.join(" and ")} — a stored page keeps whatever that ` +
|
|
610
|
+
"returned at build time. If it should differ per visitor, await connection() so " +
|
|
611
|
+
"the page renders per request; if only the browser needs it, use(browser()) keeps " +
|
|
612
|
+
"it out of the build entirely.";
|
|
548
613
|
return result;
|
|
549
614
|
};
|
|
550
615
|
// Rendered whole, with params that were invented because the route listed
|
|
@@ -554,14 +619,13 @@ export async function prerender(options) {
|
|
|
554
619
|
// before it can paint, which is exactly the shape that cannot be shared
|
|
555
620
|
// across urls.
|
|
556
621
|
if (unlisted) {
|
|
557
|
-
return said(
|
|
622
|
+
return said("blocked", "renders its params before it can paint, and lists no urls to build");
|
|
558
623
|
}
|
|
559
|
-
const shipsJs = route.clientJs !== false;
|
|
560
624
|
const rendered = await engine.handleRsc(route.component, props, null, layouts,
|
|
561
625
|
// Not empty: a page whose layout declares a parallel slot renders without
|
|
562
626
|
// it otherwise, and nothing says so — the page comes out whole apart from
|
|
563
627
|
// the missing region.
|
|
564
|
-
route.loadings, route.slots, 0, url,
|
|
628
|
+
route.loadings, route.slots, 0, url, true,
|
|
565
629
|
// A build has no host to answer rpc(), so every host call must suspend
|
|
566
630
|
// rather than resolve. Without this they found no host at all and the
|
|
567
631
|
// page was frozen holding whatever undefined rendered to — a document
|
|
@@ -571,32 +635,37 @@ export async function prerender(options) {
|
|
|
571
635
|
// everyone and there is no build-time answer for it. Not frozen: a shell
|
|
572
636
|
// is shipped instead and the page finishes for whoever asks.
|
|
573
637
|
if (rendered.usedDynamicApis) {
|
|
574
|
-
return said(
|
|
575
|
-
}
|
|
576
|
-
// A client component without a runtime is inert markup — a button that
|
|
577
|
-
// does nothing. Refused rather than shipped, and named, because they are
|
|
578
|
-
// usually inherited from a shared layout rather than written on the page.
|
|
579
|
-
if (!shipsJs && rendered.clientComponents.length > 0) {
|
|
580
|
-
return said('error', `ships no client runtime, but the tree renders ${rendered.clientComponents.join(', ')}. ` +
|
|
581
|
-
'These usually come from a shared layout rather than the page itself.');
|
|
638
|
+
return said("shell", "reaches the host for data");
|
|
582
639
|
}
|
|
583
640
|
// Nothing for a runtime to do. No client component means nothing to
|
|
584
641
|
// hydrate, and no server reference means no form that would post through
|
|
585
642
|
// React - so the bootstrap is 70 kB of javascript that runs and changes
|
|
586
|
-
// nothing. Rendered once more without it and stored that way.
|
|
587
|
-
//
|
|
588
|
-
//
|
|
643
|
+
// nothing. Rendered once more without it and stored that way.
|
|
644
|
+
//
|
|
645
|
+
// No declaration in either direction, on purpose. A page cannot need the
|
|
646
|
+
// runtime without a client component or an action in its tree - "use
|
|
647
|
+
// client" IS the opt-in - and a page that must stay this way is an
|
|
648
|
+
// assertion for the build report, not a switch.
|
|
589
649
|
let note;
|
|
590
650
|
let body = rendered.body;
|
|
591
|
-
if (
|
|
592
|
-
rendered.clientComponents.every((name) => RUNTIME_OWN.has(name)) &&
|
|
651
|
+
if (rendered.clientComponents.every((name) => RUNTIME_OWN.has(name)) &&
|
|
593
652
|
!rendered.serverReferences) {
|
|
594
653
|
// Only the document. The flight payload stays the bootstrap render's:
|
|
595
654
|
// a Link elsewhere navigating here fetches it and expects the segment
|
|
596
655
|
// and title wrappers that render puts in, which this one leaves out.
|
|
597
656
|
const bare = await engine.handleRsc(route.component, props, null, layouts, route.loadings, route.slots, 0, url, false, false);
|
|
598
|
-
body = options.serviceWorker
|
|
599
|
-
|
|
657
|
+
body = options.serviceWorker
|
|
658
|
+
? withWorkerRegistration(bare.body)
|
|
659
|
+
: bare.body;
|
|
660
|
+
let inlined = false;
|
|
661
|
+
if (options.stylesheet) {
|
|
662
|
+
const before = body;
|
|
663
|
+
body = withInlineStylesheets(body, options.stylesheet);
|
|
664
|
+
inlined = body !== before;
|
|
665
|
+
}
|
|
666
|
+
note =
|
|
667
|
+
"no client components, so ships no javascript" +
|
|
668
|
+
(inlined ? "; stylesheet inlined" : "");
|
|
600
669
|
}
|
|
601
670
|
const key = pathKey(url);
|
|
602
671
|
await write(`${key}.html`, body);
|
|
@@ -610,8 +679,12 @@ export async function prerender(options) {
|
|
|
610
679
|
const { rscPayload } = await engine.handleRscPayload(route.component, props, layouts, route.loadings, route.slots, depth, url);
|
|
611
680
|
await write(`${key}.seg${depth}.flight`, rscPayload);
|
|
612
681
|
}
|
|
613
|
-
await write(`${key}.meta.json`, JSON.stringify({
|
|
614
|
-
|
|
682
|
+
await write(`${key}.meta.json`, JSON.stringify({
|
|
683
|
+
layouts: route.layouts,
|
|
684
|
+
component: route.component,
|
|
685
|
+
version: version ?? null,
|
|
686
|
+
}, null, 2));
|
|
687
|
+
const frozen = noteNondeterminism(await withRootFallbackChecked(said("frozen", null)));
|
|
615
688
|
return note ? { ...frozen, note } : frozen;
|
|
616
689
|
}
|
|
617
690
|
/**
|
|
@@ -624,7 +697,7 @@ export async function prerender(options) {
|
|
|
624
697
|
* ones you could already freeze whole.
|
|
625
698
|
*/
|
|
626
699
|
async function writeShell(route, url, body, postponed) {
|
|
627
|
-
const parameterised = route.segments.some((s) => s.type !==
|
|
700
|
+
const parameterised = route.segments.some((s) => s.type !== "static");
|
|
628
701
|
const key = parameterised && !route.staticParams ? patternKey(route) : pathKey(url);
|
|
629
702
|
await write(`${key}.ppr.html`, body);
|
|
630
703
|
// Written only when there is something to resume from. Its absence is
|
|
@@ -634,7 +707,12 @@ export async function prerender(options) {
|
|
|
634
707
|
if (postponed != null) {
|
|
635
708
|
await write(`${key}.postponed.json`, JSON.stringify(postponed));
|
|
636
709
|
}
|
|
637
|
-
await write(`${key}.ppr-meta.json`, JSON.stringify({
|
|
710
|
+
await write(`${key}.ppr-meta.json`, JSON.stringify({
|
|
711
|
+
layouts: route.layouts,
|
|
712
|
+
component: route.component,
|
|
713
|
+
parameterised,
|
|
714
|
+
version: version ?? null,
|
|
715
|
+
}, null, 2));
|
|
638
716
|
}
|
|
639
717
|
}
|
|
640
718
|
//# sourceMappingURL=prerender.js.map
|