@rsc-kit/core 0.16.1 → 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/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 './redirect.js';
18
- import { withCache } from './cache.js';
19
- import { requestReadBy, requestWasRead, withRequest } from './request.js';
20
- import { watchNondeterminism, whileRendering } from './nondeterminism.js';
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
- 'DocumentTitle',
53
- 'PathnameProvider',
54
- 'RouteErrorBoundary',
55
- 'SegmentBoundary',
56
- 'SlotBoundary',
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
@@ -75,8 +75,10 @@ export function withInlineStylesheets(html, read) {
75
75
  });
76
76
  }
77
77
  export function withWorkerRegistration(html) {
78
- const at = html.lastIndexOf('</body>');
79
- return at === -1 ? html : html.slice(0, at) + WORKER_REGISTRATION + html.slice(at);
78
+ const at = html.lastIndexOf("</body>");
79
+ return at === -1
80
+ ? html
81
+ : html.slice(0, at) + WORKER_REGISTRATION + html.slice(at);
80
82
  }
81
83
  /**
82
84
  * The build's refusal to store a route it cannot answer ahead of time.
@@ -91,33 +93,35 @@ export class NotPrerenderable extends Error {
91
93
  // Two different problems arrive here, and the advice for one is useless for
92
94
  // the other. A page that reads the request above every boundary needs a
93
95
  // boundary; a page whose render threw needs whatever it was reaching for.
94
- const threw = routes.filter((r) => r.reason?.startsWith('could not be rendered'));
95
- const unpaintable = routes.filter((r) => !r.reason?.startsWith('could not be rendered'));
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"));
96
98
  const advice = [];
97
99
  if (unpaintable.length > 0) {
98
- advice.push('These read request data — params, headers, cookies, or the host — above\n' +
99
- 'every Suspense boundary, so nothing can paint without it:\n\n' +
100
- unpaintable.map((r) => ` ${r.url} — ${r.reason ?? 'nothing to paint'}`).join('\n') +
101
- '\n\nPut the part that waits inside <Suspense>, or add a loading.tsx beside\n' +
102
- 'the page, so there is something to store while the rest arrives.');
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.");
103
107
  }
104
108
  if (threw.length > 0) {
105
- advice.push('These failed while rendering:\n\n' +
106
- threw.map((r) => ` ${r.url} — ${r.reason}`).join('\n') +
107
- '\n\nPrerendering runs your application code, so it needs whatever that code\n' +
108
- 'needs. If the page is fine and this machine simply cannot reach a\n' +
109
- 'database or an API, either give the build access or turn prerendering\n' +
110
- 'off with `rscKit({ prerender: false })`.\n\n' +
111
- 'Reaching for data through the host — `await rpc(...)` — avoids this\n' +
112
- 'entirely: the build stubs that call, so the page freezes a shell\n' +
113
- 'without the data being available.');
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.");
114
118
  }
115
- super('Some routes could not be prerendered.\n\n' + advice.join('\n\n') + '\n');
116
- this.name = 'NotPrerenderable';
119
+ super("Some routes could not be prerendered.\n\n" + advice.join("\n\n") + "\n");
120
+ this.name = "NotPrerenderable";
117
121
  this.routes = routes;
118
122
  // Not enumerable: Vite prints an error's own properties under its stack,
119
123
  // and this list is the message again, as an object dump.
120
- Object.defineProperty(this, 'routes', { value: routes, enumerable: false });
124
+ Object.defineProperty(this, "routes", { value: routes, enumerable: false });
121
125
  }
122
126
  }
123
127
  /**
@@ -129,16 +133,16 @@ export class NotPrerenderable extends Error {
129
133
  * route matches, including the ones the app never listed.
130
134
  */
131
135
  export function patternKey(route) {
132
- const parts = route.segments.map((s) => (s.type === 'static' ? s.value : `_${s.value}_`));
133
- return parts.join('/') || 'index';
136
+ const parts = route.segments.map((s) => s.type === "static" ? s.value : `_${s.value}_`);
137
+ return parts.join("/") || "index";
134
138
  }
135
139
  /** Close a document the render was aborted in the middle of. */
136
140
  export function closeDocument(html) {
137
141
  let out = html;
138
142
  if (!/<\/body>/i.test(out) && /<body/i.test(out))
139
- out += '</body>';
143
+ out += "</body>";
140
144
  if (!/<\/html>/i.test(out) && /<html/i.test(out))
141
- out += '</html>';
145
+ out += "</html>";
142
146
  return out;
143
147
  }
144
148
  /**
@@ -163,19 +167,19 @@ export function closeDocument(html) {
163
167
  export function legend(results) {
164
168
  const has = (type) => results.some((r) => r.type === type);
165
169
  const lines = [];
166
- if (has('frozen')) {
167
- lines.push(' \u25CB (Static) prerendered as static content');
170
+ if (has("frozen")) {
171
+ lines.push(" \u25CB (Static) prerendered as static content");
168
172
  }
169
- if (has('shell')) {
170
- lines.push(' \u25D0 (Partial Prerender) prerendered as static HTML with dynamic server-streamed content');
173
+ if (has("shell")) {
174
+ lines.push(" \u25D0 (Partial Prerender) prerendered as static HTML with dynamic server-streamed content");
171
175
  }
172
- if (has('dynamic')) {
173
- lines.push(' \u0192 (Dynamic) server-rendered on demand');
176
+ if (has("dynamic")) {
177
+ lines.push(" \u0192 (Dynamic) server-rendered on demand");
174
178
  }
175
- if (has('error') || has('blocked')) {
176
- lines.push(' \u2717 (Failed) did not render, or painted nothing — the build stops here');
179
+ if (has("error") || has("blocked")) {
180
+ lines.push(" \u2717 (Failed) did not render, or painted nothing — the build stops here");
177
181
  }
178
- return lines.join('\n');
182
+ return lines.join("\n");
179
183
  }
180
184
  /**
181
185
  * How much JavaScript a route makes the browser download.
@@ -190,7 +194,7 @@ export function legend(results) {
190
194
  */
191
195
  export function clientJsSize(bytes) {
192
196
  if (bytes === 0)
193
- return 'no js';
197
+ return "no js";
194
198
  const kb = bytes / 1000;
195
199
  return `${kb < 10 ? kb.toFixed(1) : Math.round(kb)} kB`;
196
200
  }
@@ -213,27 +217,29 @@ export function notes(results) {
213
217
  // root layout leaves: one cookies() in a header component and nothing on
214
218
  // the site can be stored. The build cannot see which component read it -
215
219
  // only that every page did - so it says what that usually means.
216
- const pages = results.filter((r) => r.type && r.type !== 'error' && r.type !== 'blocked');
217
- const reasons = new Set(pages.map((r) => r.reason ?? ''));
218
- if (pages.length > 1 && !pages.some((r) => r.type === 'frozen') && reasons.size === 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) {
219
225
  const [reason] = reasons;
220
226
  parts.push(` Every route ${reason}. When one read makes every page dynamic it is usually\n` +
221
- ' in the root layout - a header reading the session, a locale from a cookie.\n' +
222
- ' Move that read into the component that needs it, under a <Suspense>, and\n' +
223
- ' the rest of the site can freeze around it.');
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.");
224
230
  }
225
- if (said('rpc('))
231
+ if (said("rpc("))
226
232
  parts.push(rpcNote());
227
- return parts.join('\n\n');
233
+ return parts.join("\n\n");
228
234
  }
229
235
  function rpcNote() {
230
- return (' A build has no backend to call. rpc() suspends instead of answering, so a page\n' +
231
- ' that reads through one ships a shell and finishes for whoever asks — which is\n' +
232
- ' almost always what you want, since that data is rarely the same for everyone.\n' +
233
- '\n' +
234
- ' If a page really should be frozen, move the read out of rpc(). If it really\n' +
235
- ' should be per visitor, say so with await connection() and the intent is on the\n' +
236
- ' page rather than inferred from a call that happened to suspend.');
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.");
237
243
  }
238
244
  /**
239
245
  * The one-line tally under the legend, in the legend's own words.
@@ -241,20 +247,22 @@ function rpcNote() {
241
247
  export function summary(results) {
242
248
  const count = (type) => results.filter((r) => r.type === type).length;
243
249
  const parts = [];
244
- if (count('frozen'))
245
- parts.push(`${count('frozen')} static`);
246
- if (count('shell'))
247
- parts.push(`${count('shell')} partial prerender`);
248
- if (count('dynamic'))
249
- parts.push(`${count('dynamic')} dynamic`);
250
- if (count('error') + count('blocked'))
251
- parts.push(`${count('error') + count('blocked')} failed`);
252
- return parts.join(', ') || 'nothing to store';
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";
253
259
  }
254
260
  export function pathKey(url) {
255
- const key = url.replace(/^\/+|\/+$/g, '') || 'index';
256
- if (key.split('/').some((segment) => segment === '..' || segment === '.')) {
257
- throw new Error('Refusing to store ' + JSON.stringify(url) + ': the path leaves the output directory. ' +
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. " +
258
266
  'A url segment came from generateStaticParams() or a route param and contains "..".');
259
267
  }
260
268
  return key;
@@ -268,16 +276,20 @@ export function pathKey(url) {
268
276
  */
269
277
  export function urlFor(route, params) {
270
278
  const parts = route.segments.map((segment) => {
271
- if (segment.type === 'static')
279
+ if (segment.type === "static")
272
280
  return segment.value;
273
- const value = params[segment.value] ?? '';
274
- if (segment.type !== 'catchAll' && /[/\\]/.test(value)) {
275
- throw new Error('The ' + segment.value + ' param is ' + JSON.stringify(value) + ', which is not one url ' +
276
- 'segment. A value with a slash in it has to be a catch-all route, or be encoded.');
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.");
277
289
  }
278
290
  return value;
279
291
  });
280
- return '/' + parts.filter((p) => p !== '').join('/');
292
+ return "/" + parts.filter((p) => p !== "").join("/");
281
293
  }
282
294
  /**
283
295
  * Every url this build should attempt, and the params each was built from.
@@ -290,15 +302,15 @@ export function urlFor(route, params) {
290
302
  function placeholders(route) {
291
303
  const params = {};
292
304
  for (const segment of route.segments) {
293
- if (segment.type !== 'static')
294
- params[segment.value] = '_';
305
+ if (segment.type !== "static")
306
+ params[segment.value] = "_";
295
307
  }
296
308
  return params;
297
309
  }
298
310
  export async function urlsToBuild(manifest, engine) {
299
311
  const entries = [];
300
312
  for (const route of manifest.routes) {
301
- const parameterised = route.segments.some((s) => s.type !== 'static');
313
+ const parameterised = route.segments.some((s) => s.type !== "static");
302
314
  if (!parameterised) {
303
315
  entries.push({ route, params: {}, url: urlFor(route, {}) });
304
316
  continue;
@@ -306,7 +318,11 @@ export async function urlsToBuild(manifest, engine) {
306
318
  // A route that lists no urls still gets one attempt, with placeholder
307
319
  // params, so it can ship a shell. Nothing in a shell varies by param.
308
320
  if (!route.staticParams) {
309
- entries.push({ route, params: placeholders(route), url: urlFor(route, placeholders(route)) });
321
+ entries.push({
322
+ route,
323
+ params: placeholders(route),
324
+ url: urlFor(route, placeholders(route)),
325
+ });
310
326
  continue;
311
327
  }
312
328
  const sets = (await engine.getStaticParams?.(route.component)) ?? null;
@@ -325,7 +341,7 @@ export async function prerender(options) {
325
341
  const { engine, write, version } = options;
326
342
  const manifest = options.manifest ?? engine.manifest?.();
327
343
  if (!manifest) {
328
- throw new Error('No route table. Pass `manifest`, or build with a plugin version that embeds one.');
344
+ throw new Error("No route table. Pass `manifest`, or build with a plugin version that embeds one.");
329
345
  }
330
346
  const entries = await urlsToBuild(manifest, engine);
331
347
  const results = new Array(entries.length);
@@ -362,15 +378,19 @@ export async function prerender(options) {
362
378
  finally {
363
379
  unwatch();
364
380
  }
365
- const refused = results.filter((r) => r.type === 'blocked');
381
+ const refused = results.filter((r) => r.type === "blocked");
366
382
  if (refused.length > 0) {
367
383
  throw new NotPrerenderable(refused);
368
384
  }
369
385
  return results;
370
386
  async function prerenderOne(route, params, url) {
371
387
  const props = options.props ? options.props(route, params) : params;
372
- const layouts = route.layouts.map((component) => ({ component, props: {} }));
373
- const unlistedNow = route.segments.some((seg) => seg.type !== 'static') && !route.staticParams;
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;
374
394
  /**
375
395
  * Why this route ships a shell rather than a whole page.
376
396
  *
@@ -380,26 +400,27 @@ export async function prerender(options) {
380
400
  */
381
401
  const shellReason = (calls) => {
382
402
  if (calls.length)
383
- return 'dynamic — called ' + calls.join(', ');
403
+ return "dynamic — called " + calls.join(", ");
384
404
  // One shell serving every url the route matches. generateStaticParams is
385
405
  // what turns it into a page per url.
386
406
  if (unlistedNow)
387
- return 'one shell for every url — add generateStaticParams to store each';
407
+ return "one shell for every url — add generateStaticParams to store each";
388
408
  // Nothing reached for the request; the data simply took too long. The
389
409
  // page is fine, it just finishes per visitor.
390
410
  if (shell?.timedOut)
391
- return 'data took longer than the build budget';
411
+ return "data took longer than the build budget";
392
412
  return null;
393
413
  };
394
414
  const said = (type, reason) => ({
395
415
  // A route standing in for many urls reports the pattern. Reporting the
396
416
  // placeholder url instead prints `/posts/_`, which looks like a page.
397
- url: unlistedNow ? '/' + patternKey(route) : url,
417
+ url: unlistedNow ? "/" + patternKey(route) : url,
398
418
  component: route.component,
399
419
  type,
400
420
  reason,
401
421
  });
402
- const unlisted = route.segments.some((seg) => seg.type !== 'static') && !route.staticParams;
422
+ const unlisted = route.segments.some((seg) => seg.type !== "static") &&
423
+ !route.staticParams;
403
424
  // Classify by rendering, never by asking. The probe is cheap and cannot
404
425
  // hang: anything still suspended when its budget expires is the answer.
405
426
  //
@@ -413,19 +434,22 @@ export async function prerender(options) {
413
434
  // must not share an answer just because they were built in the same run.
414
435
  // No request, deliberately: a page that reads one is caught below rather
415
436
  // than frozen holding whatever the build machine happened to send.
416
- 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) => {
417
438
  try {
418
439
  return {
419
440
  shell: await engine.handleRscPprShell(route.component, props, layouts, route.loadings, route.slots,
420
441
  // Only when this shell serves one url. A parameterised route's
421
442
  // shell is shared, so baking a url into it would put the wrong one
422
443
  // on every page but the one that happened to be built.
423
- unlistedNow ? '' : url),
444
+ unlistedNow ? "" : url),
424
445
  redirected: taken(),
425
446
  readRequest: requestWasRead(),
426
447
  // What reached for it, so the build can name the call rather than
427
448
  // only report that the page is dynamic.
428
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(),
429
453
  };
430
454
  }
431
455
  catch (error) {
@@ -436,7 +460,13 @@ export async function prerender(options) {
436
460
  const refused = taken();
437
461
  if (!refused)
438
462
  throw error;
439
- return { shell: null, redirected: refused, readRequest: requestWasRead(), readBy: requestReadBy() };
463
+ return {
464
+ shell: null,
465
+ redirected: refused,
466
+ readRequest: requestWasRead(),
467
+ readBy: requestReadBy(),
468
+ readWhere: requestReadWhere(),
469
+ };
440
470
  }
441
471
  }))));
442
472
  // A page that leaves rather than renders is not a build failure, and it is
@@ -464,9 +494,21 @@ export async function prerender(options) {
464
494
  * the prerender.
465
495
  */
466
496
  async function withRootFallbackChecked(result) {
467
- if (result.type !== 'shell')
497
+ if (result.type !== "shell")
468
498
  return result;
469
- if (route.loadings.length !== 1 || !engine.handleRscPprShell)
499
+ if (!engine.handleRscPprShell)
500
+ return result;
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))
470
512
  return result;
471
513
  // A tenth of the shell budget, because this is a different question.
472
514
  //
@@ -477,10 +519,10 @@ export async function prerender(options) {
477
519
  // to learn what the first millisecond already said, which on this
478
520
  // example was 40% of the entire prerender.
479
521
  const withoutRoot = await withRequest(null, () => withCache(() => withRedirect(async () => engine.handleRscPprShell(route.component, props, layouts, [], route.slots, url, ROOT_FALLBACK_BUDGET_MS)))).catch(() => null);
480
- if (withoutRoot && closeDocument(withoutRoot.shellHtml.trim()) === '') {
522
+ if (withoutRoot && closeDocument(withoutRoot.shellHtml.trim()) === "") {
481
523
  result.warning =
482
- 'nothing painted without the root loading.tsx — the fallback the whole app shares ' +
483
- 'is standing in for this page. Put a boundary where the waiting is.';
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.";
484
526
  }
485
527
  return result;
486
528
  }
@@ -488,13 +530,16 @@ export async function prerender(options) {
488
530
  // is the redirect, not a page. Rendering it per request re-derives a
489
531
  // constant, and on a static host there would be nothing to derive it with.
490
532
  if (redirected) {
491
- await write(`${pathKey(url)}.redirect.json`, JSON.stringify({ status: redirected.status, location: redirected.location }));
492
- return said('frozen', `redirects to ${redirected.location}`);
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}`);
493
538
  }
494
539
  if (!shell)
495
- return said('error', 'refused before it rendered');
540
+ return said("error", "refused before it rendered");
496
541
  if (shell.error)
497
- return said('error', shell.error);
542
+ return said("error", shell.error);
498
543
  // Something failed while rendering. Not necessarily the page's fault — a
499
544
  // build machine that cannot reach the database produces this, and so does a
500
545
  // genuinely broken component — so it is not fatal. It is simply not
@@ -504,7 +549,7 @@ export async function prerender(options) {
504
549
  // Rendering per request is the honest answer. It works as soon as whatever
505
550
  // was missing is reachable, which at runtime it usually is.
506
551
  if (shell.renderFailure) {
507
- return said('blocked', `could not be rendered at build time: ${shell.renderFailure}`);
552
+ return said("blocked", `could not be rendered at build time: ${shell.renderFailure}`);
508
553
  }
509
554
  // A timeout is the ordinary path for a page that streams, not a failure:
510
555
  // the probe hands the page a host global that never resolves, React
@@ -518,21 +563,29 @@ export async function prerender(options) {
518
563
  // ship — a page that blocks above every boundary can only be rendered on
519
564
  // demand. `reaches for the host` is the specific reason when both are
520
565
  // true; a page that merely ran long says the other thing.
521
- if (body === '') {
522
- return said('blocked', (readRequest
523
- ? 'reads the request'
566
+ if (body === "") {
567
+ return said("blocked", (readRequest
568
+ ? "reads " +
569
+ (readWhere.length
570
+ ? readWhere
571
+ : readBy.length
572
+ ? readBy
573
+ : ["the request"]).join(", ")
524
574
  : shell.usedDynamicApis
525
- ? 'reaches for the host'
526
- : 'blocks') +
527
- ' before anything can paint. Add a loading.tsx beside it, or put a ' +
528
- '<Suspense> above the waiting, and it has a skeleton to store.');
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.");
529
579
  }
530
580
  await writeShell(route, url, body, shell.postponed);
531
581
  // Why it is a shell rather than a whole page. "◐ /orders" on its own
532
582
  // leaves someone reading the build output to guess what did it, which is
533
583
  // the question this line exists to answer.
534
- const calls = [...(shell.dynamicBecause ?? []), ...(readBy ?? [])];
535
- return await withRootFallbackChecked(said('shell', shellReason(calls)));
584
+ const calls = [
585
+ ...(shell.dynamicBecause ?? []),
586
+ ...(readWhere.length ? readWhere : readBy),
587
+ ];
588
+ return await withRootFallbackChecked(said("shell", shellReason(calls)));
536
589
  }
537
590
  // Warned, not refused.
538
591
  //
@@ -553,10 +606,10 @@ export async function prerender(options) {
553
606
  if (nondeterministic.length === 0)
554
607
  return result;
555
608
  result.warning =
556
- `froze ${nondeterministic.join(' and ')} — a stored page keeps whatever that ` +
557
- 'returned at build time. If it should differ per visitor, await connection() so ' +
558
- 'the page renders per request; if only the browser needs it, use(browser()) keeps ' +
559
- 'it out of the build entirely.';
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.";
560
613
  return result;
561
614
  };
562
615
  // Rendered whole, with params that were invented because the route listed
@@ -566,7 +619,7 @@ export async function prerender(options) {
566
619
  // before it can paint, which is exactly the shape that cannot be shared
567
620
  // across urls.
568
621
  if (unlisted) {
569
- return said('blocked', 'renders its params before it can paint, and lists no urls to build');
622
+ return said("blocked", "renders its params before it can paint, and lists no urls to build");
570
623
  }
571
624
  const rendered = await engine.handleRsc(route.component, props, null, layouts,
572
625
  // Not empty: a page whose layout declares a parallel slot renders without
@@ -582,7 +635,7 @@ export async function prerender(options) {
582
635
  // everyone and there is no build-time answer for it. Not frozen: a shell
583
636
  // is shipped instead and the page finishes for whoever asks.
584
637
  if (rendered.usedDynamicApis) {
585
- return said('shell', 'reaches the host for data');
638
+ return said("shell", "reaches the host for data");
586
639
  }
587
640
  // Nothing for a runtime to do. No client component means nothing to
588
641
  // hydrate, and no server reference means no form that would post through
@@ -601,14 +654,18 @@ export async function prerender(options) {
601
654
  // a Link elsewhere navigating here fetches it and expects the segment
602
655
  // and title wrappers that render puts in, which this one leaves out.
603
656
  const bare = await engine.handleRsc(route.component, props, null, layouts, route.loadings, route.slots, 0, url, false, false);
604
- body = options.serviceWorker ? withWorkerRegistration(bare.body) : bare.body;
657
+ body = options.serviceWorker
658
+ ? withWorkerRegistration(bare.body)
659
+ : bare.body;
605
660
  let inlined = false;
606
661
  if (options.stylesheet) {
607
662
  const before = body;
608
663
  body = withInlineStylesheets(body, options.stylesheet);
609
664
  inlined = body !== before;
610
665
  }
611
- note = 'no client components, so ships no javascript' + (inlined ? '; stylesheet inlined' : '');
666
+ note =
667
+ "no client components, so ships no javascript" +
668
+ (inlined ? "; stylesheet inlined" : "");
612
669
  }
613
670
  const key = pathKey(url);
614
671
  await write(`${key}.html`, body);
@@ -622,8 +679,12 @@ export async function prerender(options) {
622
679
  const { rscPayload } = await engine.handleRscPayload(route.component, props, layouts, route.loadings, route.slots, depth, url);
623
680
  await write(`${key}.seg${depth}.flight`, rscPayload);
624
681
  }
625
- await write(`${key}.meta.json`, JSON.stringify({ layouts: route.layouts, component: route.component, version: version ?? null }, null, 2));
626
- const frozen = noteNondeterminism(await withRootFallbackChecked(said('frozen', null)));
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)));
627
688
  return note ? { ...frozen, note } : frozen;
628
689
  }
629
690
  /**
@@ -636,7 +697,7 @@ export async function prerender(options) {
636
697
  * ones you could already freeze whole.
637
698
  */
638
699
  async function writeShell(route, url, body, postponed) {
639
- const parameterised = route.segments.some((s) => s.type !== 'static');
700
+ const parameterised = route.segments.some((s) => s.type !== "static");
640
701
  const key = parameterised && !route.staticParams ? patternKey(route) : pathKey(url);
641
702
  await write(`${key}.ppr.html`, body);
642
703
  // Written only when there is something to resume from. Its absence is
@@ -646,7 +707,12 @@ export async function prerender(options) {
646
707
  if (postponed != null) {
647
708
  await write(`${key}.postponed.json`, JSON.stringify(postponed));
648
709
  }
649
- await write(`${key}.ppr-meta.json`, JSON.stringify({ layouts: route.layouts, component: route.component, parameterised, version: version ?? null }, null, 2));
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));
650
716
  }
651
717
  }
652
718
  //# sourceMappingURL=prerender.js.map