@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/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
@@ -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('</body>');
67
- 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);
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('could not be rendered'));
83
- 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"));
84
98
  const advice = [];
85
99
  if (unpaintable.length > 0) {
86
- advice.push('These read request data — params, headers, cookies, or the host — above\n' +
87
- 'every Suspense boundary, so nothing can paint without it:\n\n' +
88
- unpaintable.map((r) => ` ${r.url} — ${r.reason ?? 'nothing to paint'}`).join('\n') +
89
- '\n\nPut the part that waits inside <Suspense>, or add a loading.tsx beside\n' +
90
- '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.");
91
107
  }
92
108
  if (threw.length > 0) {
93
- advice.push('These failed while rendering:\n\n' +
94
- threw.map((r) => ` ${r.url} — ${r.reason}`).join('\n') +
95
- '\n\nPrerendering runs your application code, so it needs whatever that code\n' +
96
- 'needs. If the page is fine and this machine simply cannot reach a\n' +
97
- 'database or an API, either give the build access or turn prerendering\n' +
98
- 'off with `rscKit({ prerender: false })`.\n\n' +
99
- 'Reaching for data through the host — `await rpc(...)` — avoids this\n' +
100
- 'entirely: the build stubs that call, so the page freezes a shell\n' +
101
- '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.");
102
118
  }
103
- super('Some routes could not be prerendered.\n\n' + advice.join('\n\n') + '\n');
104
- this.name = 'NotPrerenderable';
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, 'routes', { value: routes, enumerable: false });
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) => (s.type === 'static' ? s.value : `_${s.value}_`));
121
- return parts.join('/') || 'index';
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 += '</body>';
143
+ out += "</body>";
128
144
  if (!/<\/html>/i.test(out) && /<html/i.test(out))
129
- out += '</html>';
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('frozen')) {
155
- lines.push(' \u25CB (Static) prerendered as static content');
170
+ if (has("frozen")) {
171
+ lines.push(" \u25CB (Static) prerendered as static content");
156
172
  }
157
- if (has('shell')) {
158
- 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");
159
175
  }
160
- if (has('dynamic')) {
161
- lines.push(' \u0192 (Dynamic) server-rendered on demand');
176
+ if (has("dynamic")) {
177
+ lines.push(" \u0192 (Dynamic) server-rendered on demand");
162
178
  }
163
- if (has('error') || has('blocked')) {
164
- 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");
165
181
  }
166
- return lines.join('\n');
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 'no js';
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 !== 'error' && r.type !== 'blocked');
205
- const reasons = new Set(pages.map((r) => r.reason ?? ''));
206
- 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) {
207
225
  const [reason] = reasons;
208
226
  parts.push(` Every route ${reason}. When one read makes every page dynamic it is usually\n` +
209
- ' in the root layout - a header reading the session, a locale from a cookie.\n' +
210
- ' Move that read into the component that needs it, under a <Suspense>, and\n' +
211
- ' 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.");
212
230
  }
213
- if (said('rpc('))
231
+ if (said("rpc("))
214
232
  parts.push(rpcNote());
215
- return parts.join('\n\n');
233
+ return parts.join("\n\n");
216
234
  }
217
235
  function rpcNote() {
218
- return (' A build has no backend to call. rpc() suspends instead of answering, so a page\n' +
219
- ' that reads through one ships a shell and finishes for whoever asks — which is\n' +
220
- ' almost always what you want, since that data is rarely the same for everyone.\n' +
221
- '\n' +
222
- ' If a page really should be frozen, move the read out of rpc(). If it really\n' +
223
- ' should be per visitor, say so with await connection() and the intent is on the\n' +
224
- ' 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.");
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('frozen'))
233
- parts.push(`${count('frozen')} static`);
234
- if (count('shell'))
235
- parts.push(`${count('shell')} partial prerender`);
236
- if (count('dynamic'))
237
- parts.push(`${count('dynamic')} dynamic`);
238
- if (count('error') + count('blocked'))
239
- parts.push(`${count('error') + count('blocked')} failed`);
240
- 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";
241
259
  }
242
260
  export function pathKey(url) {
243
- const key = url.replace(/^\/+|\/+$/g, '') || 'index';
244
- if (key.split('/').some((segment) => segment === '..' || segment === '.')) {
245
- 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. " +
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 === 'static')
279
+ if (segment.type === "static")
260
280
  return segment.value;
261
- const value = params[segment.value] ?? '';
262
- if (segment.type !== 'catchAll' && /[/\\]/.test(value)) {
263
- throw new Error('The ' + segment.value + ' param is ' + JSON.stringify(value) + ', which is not one url ' +
264
- '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.");
265
289
  }
266
290
  return value;
267
291
  });
268
- return '/' + parts.filter((p) => p !== '').join('/');
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 !== 'static')
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 !== 'static');
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({ 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
+ });
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('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.");
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 === 'blocked');
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) => ({ component, props: {} }));
361
- 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;
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 'dynamic — called ' + calls.join(', ');
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 'one shell for every url — add generateStaticParams to store each';
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 'data took longer than the build budget';
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 ? '/' + patternKey(route) : url,
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 !== 'static') && !route.staticParams;
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 ? '' : url),
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 { 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
+ };
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 !== 'shell')
497
+ if (result.type !== "shell")
498
+ return result;
499
+ if (!engine.handleRscPprShell)
456
500
  return result;
457
- if (route.loadings.length !== 1 || !engine.handleRscPprShell)
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
- 'nothing painted without the root loading.tsx — the fallback the whole app shares ' +
471
- '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.";
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({ status: redirected.status, location: redirected.location }));
480
- 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}`);
481
538
  }
482
539
  if (!shell)
483
- return said('error', 'refused before it rendered');
540
+ return said("error", "refused before it rendered");
484
541
  if (shell.error)
485
- return said('error', shell.error);
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('blocked', `could not be rendered at build time: ${shell.renderFailure}`);
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('blocked', (readRequest
511
- ? '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(", ")
512
574
  : shell.usedDynamicApis
513
- ? 'reaches for the host'
514
- : 'blocks') +
515
- ' before anything can paint. Add a loading.tsx beside it, or put a ' +
516
- '<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.");
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 = [...(shell.dynamicBecause ?? []), ...(readBy ?? [])];
523
- 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)));
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(' and ')} — a stored page keeps whatever that ` +
545
- 'returned at build time. If it should differ per visitor, await connection() so ' +
546
- 'the page renders per request; if only the browser needs it, use(browser()) keeps ' +
547
- '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.";
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('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");
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, shipsJs,
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('shell', 'reaches the host for data');
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. Only when
587
- // the page declared nothing: `clientJs = true` keeps the runtime, for a
588
- // page that wants the service worker or the update prompt regardless.
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 (route.clientJs === 'auto' &&
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 ? withWorkerRegistration(bare.body) : bare.body;
599
- note = 'no client components, so ships no javascript';
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({ layouts: route.layouts, component: route.component, version: version ?? null }, null, 2));
614
- 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)));
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 !== 'static');
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({ 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));
638
716
  }
639
717
  }
640
718
  //# sourceMappingURL=prerender.js.map