@rsc-kit/core 0.30.28 → 0.30.30

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,6 +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 edge contract's marker, as the build left it beside the pages - see edgeMarker.
437
+ let edgeMarkerRead = null;
436
438
  const maxActionBody = options.maxActionBody ?? DEFAULT_MAX_ACTION_BODY;
437
439
  const compress = options.compress ?? true;
438
440
  // Annotated rather than inferred: the narrowing below is lost inside the
@@ -517,6 +519,9 @@ export function createRscHandler(options) {
517
519
  const siteHosts = manifest.build?.hosts ?? [];
518
520
  /** Whether responses say what built them. How they were served is always said. */
519
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;
520
525
  /** How a response was answered, for X-RSC-Kit: a file the build wrote, or a shell of one. */
521
526
  const servedFrom = new WeakMap();
522
527
  /** What a stored document's head names, read once per file, for the Link header. */
@@ -635,6 +640,12 @@ export function createRscHandler(options) {
635
640
  if (!response)
636
641
  return null;
637
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
+ }
638
649
  collected.forEach((value, name) => {
639
650
  // Set-Cookie is applied below, once per cookie: iterating a Headers
640
651
  // gives it joined in some runtimes and per-cookie in others, and a
@@ -686,7 +697,7 @@ export function createRscHandler(options) {
686
697
  // plain bytes and compresses the page once, so gzip here would be
687
698
  // garbage in the middle of a document. The edge drops
688
699
  // Accept-Encoding, but this does not rely on it.
689
- if (compress && !new URL(request.url).pathname.startsWith(PPR.prefix)) {
700
+ if (compress && !new URL(request.url).pathname.startsWith(EDGE.prefix)) {
690
701
  const answer = await compressed(request, response, servedFrom.get(response) === "stored" ? storedKey(request, response) : undefined);
691
702
  const from = servedFrom.get(response);
692
703
  if (answer !== response && from)
@@ -805,10 +816,10 @@ export function createRscHandler(options) {
805
816
  if (url.pathname === HEADER.changesPath) {
806
817
  return await serveChanges(request);
807
818
  }
808
- // The PPR contract, for a platform's edge. The whole prefix is reserved,
809
- // and is a 404 to anyone without the release's token.
810
- if (url.pathname.startsWith(PPR.prefix)) {
811
- 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);
812
823
  }
813
824
  // A form submitted before the page had a runtime: the browser posts it
814
825
  // to the page's own url, as React wrote it, with the action's id among
@@ -1178,7 +1189,7 @@ export function createRscHandler(options) {
1178
1189
  return answer;
1179
1190
  }
1180
1191
  /**
1181
- * 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
1182
1193
  * route with no guard, whose shell the build wrote for this url or for its
1183
1194
  * pattern. Anything else is a 404 - the same pages edgeShells leaves out of
1184
1195
  * the marker, so the list and the origin cannot disagree.
@@ -1255,26 +1266,26 @@ export function createRscHandler(options) {
1255
1266
  "Content-Type": HTML_TYPE,
1256
1267
  // Rendered for whoever asked. Never cacheable.
1257
1268
  "Cache-Control": PER_CLIENT,
1258
- ...(version ? { [PPR.build]: version } : {}),
1269
+ ...(edgeMarkerRead ? { [EDGE.build]: edgeMarkerRead.build } : {}),
1259
1270
  }),
1260
1271
  });
1261
1272
  });
1262
1273
  }
1263
1274
  /**
1264
- * The PPR contract's two endpoints, for a platform's edge. They exist only
1265
- * for a caller holding the release's PPR_TOKEN: unset, missing or wrong,
1266
- * everything under /_ppr/ is a 404, as if there were nothing here - so an
1267
- * 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.
1268
1279
  *
1269
- * The shell goes as JSON with an empty `state`: the resume reads its own
1270
- * from the build and ignores the body. Both answers name the build in
1271
- * 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.
1272
1283
  */
1273
- async function servePprContract(request, url) {
1274
- const answer = await pprContractAnswer(request, url);
1284
+ async function serveEdgeContract(request, url) {
1285
+ const answer = await edgeContractAnswer(request, url);
1275
1286
  // Every answer here depends on the token, a 404 included: stored by a
1276
1287
  // cache in between, one would be served to callers with the wrong token
1277
- // 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.
1278
1289
  try {
1279
1290
  answer.headers.set("Cache-Control", "private, no-store");
1280
1291
  return answer;
@@ -1286,30 +1297,48 @@ export function createRscHandler(options) {
1286
1297
  });
1287
1298
  }
1288
1299
  }
1289
- async function pprContractAnswer(request, url) {
1300
+ async function edgeContractAnswer(request, url) {
1290
1301
  const missing = new Response("Not found", { status: 404 });
1291
1302
  const read = options.prerendered;
1292
- if (!read || !version || !holdsSecret(request, PPR.token, PPR.tokenEnv))
1303
+ if (!read || !holdsSecret(request, EDGE.token, EDGE.tokenEnv))
1304
+ return missing;
1305
+ const marker = await edgeMarker(read);
1306
+ if (!marker)
1293
1307
  return missing;
1294
- if (url.pathname === PPR.shellPath && (request.method === "GET" || request.method === "HEAD")) {
1295
- 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);
1296
1312
  if (pathname === null)
1297
1313
  return missing;
1298
- const shell = await shellAt(pathname, read);
1299
- if (shell instanceof Response)
1300
- return shell;
1301
- return new Response(request.method === "HEAD" ? null : JSON.stringify({ v: 1, build: version, 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
+ }), {
1302
1331
  headers: {
1303
1332
  "Content-Type": "application/json",
1304
1333
  // Fixed for the build, so no ETag. And never stored on the way:
1305
1334
  // it was answered only for the token, and a cache between the
1306
1335
  // edge and here would hand it to callers without one.
1307
1336
  "Cache-Control": "private, no-store",
1308
- [PPR.build]: version,
1337
+ [EDGE.build]: build,
1309
1338
  },
1310
1339
  });
1311
1340
  }
1312
- if (url.pathname === PPR.resumePath && request.method === "POST") {
1341
+ if (url.pathname === EDGE.resumePath && request.method === "POST") {
1313
1342
  const raw = url.searchParams.get("url");
1314
1343
  const at = raw === null ? -1 : raw.indexOf("?");
1315
1344
  const pathname = contractPathname(at === -1 ? raw : raw.slice(0, at), url);
@@ -1319,6 +1348,36 @@ export function createRscHandler(options) {
1319
1348
  }
1320
1349
  return missing;
1321
1350
  }
1351
+ /**
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.
1367
+ */
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;
1380
+ }
1322
1381
  /**
1323
1382
  * A contract path - absolute and decoded once - as the pathname this host
1324
1383
  * matches, or null for one that would not survive the trip unchanged: a