@rsc-kit/core 0.30.29 → 0.30.31

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/host.js CHANGED
@@ -81,7 +81,7 @@ export { redirect } from "./redirect.js";
81
81
  // public surface so a host imports from one place.
82
82
  /** @internal For a host adapter. An app never matches its own routes. */
83
83
  export { matchIntercept, matchRoute, sharedDepth } from "./routing.js";
84
- import { FLIGHT_TYPE, HEADER, HTML_TYPE, PER_CLIENT, PER_REQUEST, REVALIDATE, PPR, VARY_ON_RSC, } from "./headers.js";
84
+ import { FLIGHT_TYPE, HEADER, HTML_TYPE, PER_CLIENT, PER_REQUEST, REVALIDATE, EDGE, VARY_ON_RSC, } from "./headers.js";
85
85
  import { ServerAuthenticationError, ServerAuthorizationError, } from "./js/errors.js";
86
86
  import { assertSigningSecret, serveChanges } from "./changed.js";
87
87
  /**
@@ -433,8 +433,8 @@ export function createRscHandler(options) {
433
433
  // own. Resolved on the first request: the engine reads it from a build
434
434
  // product it only has at runtime.
435
435
  let version = engine.buildId ? undefined : options.version;
436
- // The PPR contract's build, read once - see pprBuild.
437
- let pprBuildRead = null;
436
+ // The edge contract's marker, as the build left it beside the pages - see edgeMarker.
437
+ let edgeMarkerRead = null;
438
438
  const maxActionBody = options.maxActionBody ?? DEFAULT_MAX_ACTION_BODY;
439
439
  const compress = options.compress ?? true;
440
440
  // Annotated rather than inferred: the narrowing below is lost inside the
@@ -519,6 +519,9 @@ export function createRscHandler(options) {
519
519
  const siteHosts = manifest.build?.hosts ?? [];
520
520
  /** Whether responses say what built them. How they were served is always said. */
521
521
  const identify = Boolean(manifest.build?.identify);
522
+ // rscKit({ headers }): every response, and every page an edge serves.
523
+ const pageHeaders = manifest.build?.headers ?? {};
524
+ const hasPageHeaders = Object.keys(pageHeaders).length > 0;
522
525
  /** How a response was answered, for X-RSC-Kit: a file the build wrote, or a shell of one. */
523
526
  const servedFrom = new WeakMap();
524
527
  /** What a stored document's head names, read once per file, for the Link header. */
@@ -637,6 +640,12 @@ export function createRscHandler(options) {
637
640
  if (!response)
638
641
  return null;
639
642
  const collected = taken();
643
+ // Under whatever the answer already has, and before the middleware's,
644
+ // so a page or a middleware that asked for its own value keeps it.
645
+ for (const [name, value] of Object.entries(pageHeaders)) {
646
+ if (!response.headers.has(name))
647
+ response.headers.set(name, value);
648
+ }
640
649
  collected.forEach((value, name) => {
641
650
  // Set-Cookie is applied below, once per cookie: iterating a Headers
642
651
  // gives it joined in some runtimes and per-cookie in others, and a
@@ -688,7 +697,7 @@ export function createRscHandler(options) {
688
697
  // plain bytes and compresses the page once, so gzip here would be
689
698
  // garbage in the middle of a document. The edge drops
690
699
  // Accept-Encoding, but this does not rely on it.
691
- if (compress && !new URL(request.url).pathname.startsWith(PPR.prefix)) {
700
+ if (compress && !new URL(request.url).pathname.startsWith(EDGE.prefix)) {
692
701
  const answer = await compressed(request, response, servedFrom.get(response) === "stored" ? storedKey(request, response) : undefined);
693
702
  const from = servedFrom.get(response);
694
703
  if (answer !== response && from)
@@ -807,10 +816,10 @@ export function createRscHandler(options) {
807
816
  if (url.pathname === HEADER.changesPath) {
808
817
  return await serveChanges(request);
809
818
  }
810
- // The PPR contract, for a platform's edge. The whole prefix is reserved,
811
- // and is a 404 to anyone without the release's token.
812
- if (url.pathname.startsWith(PPR.prefix)) {
813
- return await servePprContract(request, url);
819
+ // The edge contract, for a platform's edge. The whole prefix is reserved,
820
+ // and is a 404 to anyone without the token.
821
+ if (url.pathname.startsWith(EDGE.prefix)) {
822
+ return await serveEdgeContract(request, url);
814
823
  }
815
824
  // A form submitted before the page had a runtime: the browser posts it
816
825
  // to the page's own url, as React wrote it, with the action's id among
@@ -1180,7 +1189,7 @@ export function createRscHandler(options) {
1180
1189
  return answer;
1181
1190
  }
1182
1191
  /**
1183
- * A page's shell, by the rule both shell endpoints apply: a PPR page, on a
1192
+ * A page's shell, by the rule the page endpoint applies: a PPR page, on a
1184
1193
  * route with no guard, whose shell the build wrote for this url or for its
1185
1194
  * pattern. Anything else is a 404 - the same pages edgeShells leaves out of
1186
1195
  * the marker, so the list and the origin cannot disagree.
@@ -1257,26 +1266,26 @@ export function createRscHandler(options) {
1257
1266
  "Content-Type": HTML_TYPE,
1258
1267
  // Rendered for whoever asked. Never cacheable.
1259
1268
  "Cache-Control": PER_CLIENT,
1260
- ...(pprBuildRead ? { [PPR.build]: pprBuildRead } : {}),
1269
+ ...(edgeMarkerRead ? { [EDGE.build]: edgeMarkerRead.build } : {}),
1261
1270
  }),
1262
1271
  });
1263
1272
  });
1264
1273
  }
1265
1274
  /**
1266
- * The PPR contract's two endpoints, for a platform's edge. They exist only
1267
- * for a caller holding the PPR_TOKEN the platform set: unset, missing or wrong,
1268
- * everything under /_ppr/ is a 404, as if there were nothing here - so an
1269
- * app is safe wherever it runs, and only the platform can ask.
1275
+ * The edge contract's two endpoints, for a platform's edge. They exist only
1276
+ * for a caller holding the EDGE_TOKEN the platform set: unset, missing or
1277
+ * wrong, everything under /_edge/ is a 404, as if there were nothing here -
1278
+ * so an app is safe wherever it runs, and only the platform can ask.
1270
1279
  *
1271
- * The shell goes as JSON with an empty `state`: the resume reads its own
1272
- * from the build and ignores the body. Both answers name the build in
1273
- * PPR-Build.
1280
+ * A page the marker lists as static goes whole, `complete`; a shell goes
1281
+ * with an empty `state`, since the resume reads its own from the build and
1282
+ * ignores the body. Both answers name the build in Edge-Build.
1274
1283
  */
1275
- async function servePprContract(request, url) {
1276
- const answer = await pprContractAnswer(request, url);
1284
+ async function serveEdgeContract(request, url) {
1285
+ const answer = await edgeContractAnswer(request, url);
1277
1286
  // Every answer here depends on the token, a 404 included: stored by a
1278
1287
  // cache in between, one would be served to callers with the wrong token
1279
- // or none - and a stored 404 would hide a real shell from the edge.
1288
+ // or none - and a stored 404 would hide a real page from the edge.
1280
1289
  try {
1281
1290
  answer.headers.set("Cache-Control", "private, no-store");
1282
1291
  return answer;
@@ -1288,33 +1297,48 @@ export function createRscHandler(options) {
1288
1297
  });
1289
1298
  }
1290
1299
  }
1291
- async function pprContractAnswer(request, url) {
1300
+ async function edgeContractAnswer(request, url) {
1292
1301
  const missing = new Response("Not found", { status: 404 });
1293
1302
  const read = options.prerendered;
1294
- if (!read || !holdsSecret(request, PPR.token, PPR.tokenEnv))
1303
+ if (!read || !holdsSecret(request, EDGE.token, EDGE.tokenEnv))
1295
1304
  return missing;
1296
- const build = await pprBuild(read);
1297
- if (!build)
1305
+ const marker = await edgeMarker(read);
1306
+ if (!marker)
1298
1307
  return missing;
1299
- if (url.pathname === PPR.shellPath && (request.method === "GET" || request.method === "HEAD")) {
1300
- const pathname = contractPathname(url.searchParams.get("path"), url);
1308
+ const { build } = marker;
1309
+ if (url.pathname === EDGE.pagePath && (request.method === "GET" || request.method === "HEAD")) {
1310
+ const asked = url.searchParams.get("path");
1311
+ const pathname = contractPathname(asked, url);
1301
1312
  if (pathname === null)
1302
1313
  return missing;
1303
- const shell = await shellAt(pathname, read);
1304
- if (shell instanceof Response)
1305
- return shell;
1306
- return new Response(request.method === "HEAD" ? null : JSON.stringify({ v: 1, build, html: shell, state: "" }), {
1314
+ // Whole only for a path the marker names, and only from the document
1315
+ // the build stored for it; anything else is a shell or nothing.
1316
+ const complete = marker.static.has(asked);
1317
+ const html = complete ? await pageAt(pathname, read) : await shellAt(pathname, read);
1318
+ if (html instanceof Response)
1319
+ return html;
1320
+ return new Response(request.method === "HEAD"
1321
+ ? null
1322
+ : JSON.stringify({
1323
+ v: 2,
1324
+ build,
1325
+ html,
1326
+ state: "",
1327
+ ...(complete ? { complete: true } : {}),
1328
+ // What the page itself goes out with, so the edge's copy does too.
1329
+ ...(hasPageHeaders ? { headers: pageHeaders } : {}),
1330
+ }), {
1307
1331
  headers: {
1308
1332
  "Content-Type": "application/json",
1309
1333
  // Fixed for the build, so no ETag. And never stored on the way:
1310
1334
  // it was answered only for the token, and a cache between the
1311
1335
  // edge and here would hand it to callers without one.
1312
1336
  "Cache-Control": "private, no-store",
1313
- [PPR.build]: build,
1337
+ [EDGE.build]: build,
1314
1338
  },
1315
1339
  });
1316
1340
  }
1317
- if (url.pathname === PPR.resumePath && request.method === "POST") {
1341
+ if (url.pathname === EDGE.resumePath && request.method === "POST") {
1318
1342
  const raw = url.searchParams.get("url");
1319
1343
  const at = raw === null ? -1 : raw.indexOf("?");
1320
1344
  const pathname = contractPathname(at === -1 ? raw : raw.slice(0, at), url);
@@ -1325,13 +1349,34 @@ export function createRscHandler(options) {
1325
1349
  return missing;
1326
1350
  }
1327
1351
  /**
1328
- * The build the marker names, which the build wrote beside the shells - not
1329
- * the client's id this host sends a browser, which a server-only change
1330
- * keeps. See pprBuildId.
1352
+ * A static page's whole document, as the build stored it. The marker lists
1353
+ * no guarded page, and this checks again rather than trusting the list:
1354
+ * a document handed to an edge is served to everyone.
1355
+ */
1356
+ async function pageAt(pathname, read) {
1357
+ const route = matchRoute(routes, pathname);
1358
+ const key = route && !isGuarded(route) ? storedKeyFor(pathname, route) : null;
1359
+ const html = key === null ? null : await read(`${key}.html`);
1360
+ return html ?? new Response("No such page", { status: 404 });
1361
+ }
1362
+ /**
1363
+ * The marker, which the build wrote beside the pages as well as into
1364
+ * .output - its build is not the client's id this host sends a browser,
1365
+ * which a server-only change keeps (see edgeBuildId), and its static list
1366
+ * is what may go whole.
1331
1367
  */
1332
- async function pprBuild(read) {
1333
- pprBuildRead ??= (await read("_ppr.build"))?.trim() || null;
1334
- return pprBuildRead;
1368
+ async function edgeMarker(read) {
1369
+ if (edgeMarkerRead)
1370
+ return edgeMarkerRead;
1371
+ try {
1372
+ const marker = JSON.parse((await read(EDGE.storedMarker)) ?? "null");
1373
+ if (marker?.build)
1374
+ edgeMarkerRead = { build: marker.build, static: new Set(marker.static ?? []) };
1375
+ }
1376
+ catch {
1377
+ // A marker this build wrote and cannot read back: no edge, not a guess.
1378
+ }
1379
+ return edgeMarkerRead;
1335
1380
  }
1336
1381
  /**
1337
1382
  * A contract path - absolute and decoded once - as the pathname this host