@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/earlyHints.js +5 -1
- package/dist/earlyHints.js.map +1 -1
- package/dist/edgeShells.d.ts +49 -7
- package/dist/edgeShells.js +117 -8
- package/dist/edgeShells.js.map +1 -1
- package/dist/headers.d.ts +18 -12
- package/dist/headers.js +33 -12
- package/dist/headers.js.map +1 -1
- package/dist/host.js +83 -38
- package/dist/host.js.map +1 -1
- package/dist/js/createViteRscApp.js +12 -3
- package/dist/js/createViteRscApp.js.map +1 -1
- package/dist/js/pagePayload.d.ts +1 -1
- package/dist/js/pagePayload.js +5 -2
- package/dist/js/pagePayload.js.map +1 -1
- package/dist/manifest.d.ts +5 -0
- package/dist/manifest.js.map +1 -1
- package/dist/vite.d.ts +44 -10
- package/dist/vite.js +133 -33
- package/dist/vite.js.map +1 -1
- package/package.json +1 -1
package/dist/vite.js
CHANGED
|
@@ -23,7 +23,8 @@ import rsc, { getPluginApi } from "@vitejs/plugin-rsc";
|
|
|
23
23
|
import { loadEnv, parseAst } from "vite";
|
|
24
24
|
import { stampDisplayNames } from "./stableNames.js";
|
|
25
25
|
import { REPORT_FILE, buildReport } from "./buildReport.js";
|
|
26
|
-
import { edgeShells,
|
|
26
|
+
import { contractPath, edgeGuarded, edgeMarker, edgeShells, edgeStatic } from "./edgeShells.js";
|
|
27
|
+
import { EDGE, EDGE_HEADERS } from "./headers.js";
|
|
27
28
|
import { MANIFEST_PATH, manifestWarning, webManifest } from "./webManifest.js";
|
|
28
29
|
import { ASSET_BASE, allAppAssets, assetFile, appAssets, headTags, shareImageUrls, typeOf, } from "./appAssets.js";
|
|
29
30
|
import { reactCacheImports } from "./reactCache.js";
|
|
@@ -240,6 +241,8 @@ let siteHosts = [];
|
|
|
240
241
|
let hostsOption = [];
|
|
241
242
|
let barrelImports = true;
|
|
242
243
|
let identify = true;
|
|
244
|
+
/** RscKitOptions.headers, checked against the edge contract's allow list. */
|
|
245
|
+
let pageHeaders = {};
|
|
243
246
|
/** Resolved from RscKitOptions.openapi; null when off. */
|
|
244
247
|
let openapi = null;
|
|
245
248
|
let foundAssets = {
|
|
@@ -401,6 +404,36 @@ function aliasEntries() {
|
|
|
401
404
|
});
|
|
402
405
|
return entries;
|
|
403
406
|
}
|
|
407
|
+
/**
|
|
408
|
+
* rscKit({ headers }), in the case the contract spells each name. Refused at
|
|
409
|
+
* build, never trimmed at runtime: a header that was asked for and quietly not
|
|
410
|
+
* sent is a security header missing from every page.
|
|
411
|
+
*/
|
|
412
|
+
export function normalisePageHeaders(raw) {
|
|
413
|
+
if (raw === undefined)
|
|
414
|
+
return {};
|
|
415
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
416
|
+
throw new Error("[rsc-kit] headers: an object of header names to values, as in { 'X-Frame-Options': 'DENY' }.");
|
|
417
|
+
const known = new Map(EDGE_HEADERS.map((name) => [name.toLowerCase(), name]));
|
|
418
|
+
// The renderer's own: an asset is immutable, a guarded page no-store, and
|
|
419
|
+
// the Link header is the document's early hints. One value for every
|
|
420
|
+
// response would be wrong for most of them.
|
|
421
|
+
const owned = new Set(["cache-control", "link"]);
|
|
422
|
+
const headers = {};
|
|
423
|
+
for (const [name, value] of Object.entries(raw)) {
|
|
424
|
+
if (owned.has(name.toLowerCase()))
|
|
425
|
+
throw new Error(`[rsc-kit] headers: ${name} is the renderer's to set per response. Set it in a middleware if a page needs its own.`);
|
|
426
|
+
const canonical = known.get(name.toLowerCase());
|
|
427
|
+
if (!canonical)
|
|
428
|
+
throw new Error(`[rsc-kit] headers: ${JSON.stringify(name)} is not one an edge may send with a cached page (${EDGE_HEADERS.join(", ")}). Set it in a middleware instead, which runs per request.`);
|
|
429
|
+
if (typeof value !== "string" || value.trim() === "" || /[\r\n]/.test(value))
|
|
430
|
+
throw new Error(`[rsc-kit] headers: ${canonical} needs a value on one line, not ${JSON.stringify(value)}.`);
|
|
431
|
+
if (canonical in headers)
|
|
432
|
+
throw new Error(`[rsc-kit] headers: ${canonical} is named twice.`);
|
|
433
|
+
headers[canonical] = value.trim();
|
|
434
|
+
}
|
|
435
|
+
return headers;
|
|
436
|
+
}
|
|
404
437
|
/**
|
|
405
438
|
* The prefixes a backend owns, as the matcher reads them: `/auth`, no trailing
|
|
406
439
|
* slash. Refused rather than corrected where a mistake would silently send the
|
|
@@ -515,6 +548,7 @@ function resolvePaths(options) {
|
|
|
515
548
|
hostsOption = options.hosts ?? [];
|
|
516
549
|
barrelImports = options.barrelImports !== false;
|
|
517
550
|
identify = options.identify !== false;
|
|
551
|
+
pageHeaders = normalisePageHeaders(options.headers);
|
|
518
552
|
openapi = options.openapi
|
|
519
553
|
? (() => {
|
|
520
554
|
const { path, ...document } = typeof options.openapi === "object" ? options.openapi : {};
|
|
@@ -836,6 +870,7 @@ function routeManifest() {
|
|
|
836
870
|
hosts: siteHosts,
|
|
837
871
|
identify,
|
|
838
872
|
refreshOn: usesRefreshOn() || undefined,
|
|
873
|
+
headers: Object.keys(pageHeaders).length ? pageHeaders : undefined,
|
|
839
874
|
},
|
|
840
875
|
routes,
|
|
841
876
|
intercepts,
|
|
@@ -1609,40 +1644,84 @@ export function workerVersion(precache, storedDir) {
|
|
|
1609
1644
|
return version.digest("hex").slice(0, 12);
|
|
1610
1645
|
}
|
|
1611
1646
|
/**
|
|
1612
|
-
* The
|
|
1647
|
+
* The edge contract's build: what an edge checks a cached page against.
|
|
1613
1648
|
*
|
|
1614
1649
|
* Not the client's build id, which is what a browser tab is told and covers
|
|
1615
1650
|
* only what the browser downloads - a release that changed only what the
|
|
1616
|
-
* server renders kept it, and an edge holding the old
|
|
1617
|
-
* it to the new server's holes. So it is that id and the bytes
|
|
1618
|
-
* and
|
|
1619
|
-
* build to the edge; one that leaves them all byte for byte the
|
|
1620
|
-
* because
|
|
1621
|
-
* reload for either.
|
|
1622
|
-
*/
|
|
1623
|
-
export function
|
|
1651
|
+
* server renders kept it, and an edge holding the old page would have served
|
|
1652
|
+
* it, or joined it to the new server's holes. So it is that id and the bytes
|
|
1653
|
+
* of every stored document and postponed state. A server change that reaches
|
|
1654
|
+
* one is a new build to the edge; one that leaves them all byte for byte the
|
|
1655
|
+
* same is not, because what the edge holds is then this build's. A tab is not
|
|
1656
|
+
* made to reload for either.
|
|
1657
|
+
*/
|
|
1658
|
+
export function edgeBuildId(clientBuild, storedDir) {
|
|
1624
1659
|
const id = createHash("sha256");
|
|
1625
1660
|
id.update(clientBuild);
|
|
1626
|
-
const
|
|
1661
|
+
const pages = [];
|
|
1627
1662
|
const walk = (dir) => {
|
|
1628
1663
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1629
1664
|
const path = join(dir, entry.name);
|
|
1630
1665
|
if (entry.isDirectory())
|
|
1631
1666
|
walk(path);
|
|
1632
|
-
else if (/\.(?:
|
|
1633
|
-
|
|
1667
|
+
else if (/\.(?:html|postponed\.json)$/.test(entry.name))
|
|
1668
|
+
pages.push(path);
|
|
1634
1669
|
}
|
|
1635
1670
|
};
|
|
1636
1671
|
if (existsSync(storedDir))
|
|
1637
1672
|
walk(storedDir);
|
|
1638
|
-
for (const path of
|
|
1673
|
+
for (const path of pages.sort()) {
|
|
1639
1674
|
id.update("\n" + relative(storedDir, path) + "\n");
|
|
1640
1675
|
id.update(readFileSync(path));
|
|
1641
1676
|
}
|
|
1642
1677
|
return id.digest("hex").slice(0, 16);
|
|
1643
1678
|
}
|
|
1644
|
-
/**
|
|
1645
|
-
|
|
1679
|
+
/**
|
|
1680
|
+
* Give a static page a payload it can boot from without the app.
|
|
1681
|
+
*
|
|
1682
|
+
* A page the build wrote whole hydrates from its stored payload, which it
|
|
1683
|
+
* fetched from its own url with X-RSC - a request an edge passes to the app,
|
|
1684
|
+
* so a page the edge answered whole still woke a sleeping app on every visit
|
|
1685
|
+
* from a browser. So its payload is copied to a hashed asset, served as
|
|
1686
|
+
* immutable like every other, and the document names it: a preload, so the
|
|
1687
|
+
* fetch starts as the head parses, and a meta the client boots from. Its
|
|
1688
|
+
* layouts ride the meta, since an asset carries no X-RSC-Layouts.
|
|
1689
|
+
*
|
|
1690
|
+
* Only for the pages the marker lists: a guarded page's payload at a public
|
|
1691
|
+
* url would be read past its guard. Returns the paths it could do it for.
|
|
1692
|
+
*/
|
|
1693
|
+
export function storedPayloadAssets(staticDir, clientDir, pages) {
|
|
1694
|
+
const done = [];
|
|
1695
|
+
const attribute = (value) => value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
1696
|
+
for (const { path, key } of pages) {
|
|
1697
|
+
const htmlFile = join(staticDir, `${key}.html`);
|
|
1698
|
+
const flightFile = join(staticDir, `${key}.flight`);
|
|
1699
|
+
if (!existsSync(htmlFile) || !existsSync(flightFile))
|
|
1700
|
+
continue;
|
|
1701
|
+
const html = readFileSync(htmlFile, "utf8");
|
|
1702
|
+
const at = html.indexOf("</head>");
|
|
1703
|
+
if (at === -1)
|
|
1704
|
+
continue;
|
|
1705
|
+
const flight = readFileSync(flightFile);
|
|
1706
|
+
const name = `${createHash("sha256").update(flight).digest("hex").slice(0, 16)}.rsc`;
|
|
1707
|
+
const url = `/assets/_rsc/${name}`;
|
|
1708
|
+
mkdirSync(join(clientDir, "assets", "_rsc"), { recursive: true });
|
|
1709
|
+
writeFileSync(join(clientDir, "assets", "_rsc", name), flight);
|
|
1710
|
+
let layouts = [];
|
|
1711
|
+
try {
|
|
1712
|
+
layouts = JSON.parse(readFileSync(join(staticDir, `${key}.meta.json`), "utf8")).layouts ?? [];
|
|
1713
|
+
}
|
|
1714
|
+
catch {
|
|
1715
|
+
continue;
|
|
1716
|
+
}
|
|
1717
|
+
writeFileSync(htmlFile, html.slice(0, at) +
|
|
1718
|
+
`<link rel="preload" as="fetch" crossorigin="" href="${attribute(url)}"/>` +
|
|
1719
|
+
`<meta name="rsc-kit:payload" content="${attribute(url)}" data-layouts="${attribute(layouts.join(","))}"/>` +
|
|
1720
|
+
html.slice(at));
|
|
1721
|
+
done.push(path);
|
|
1722
|
+
}
|
|
1723
|
+
return done;
|
|
1724
|
+
}
|
|
1646
1725
|
function writeServiceWorker(clientDir, frozen = [], offlineUrl = null,
|
|
1647
1726
|
// The pages the build stored, which the worker serves cache-first. Their
|
|
1648
1727
|
// bytes are part of what a version is - see below.
|
|
@@ -2010,6 +2089,8 @@ ${legend(counted)}
|
|
|
2010
2089
|
.map((r) => r.url),
|
|
2011
2090
|
results,
|
|
2012
2091
|
shells: edgeShells(results, manifest.routes),
|
|
2092
|
+
routes: manifest.routes,
|
|
2093
|
+
pathKey: pathKeyOf,
|
|
2013
2094
|
build,
|
|
2014
2095
|
};
|
|
2015
2096
|
}
|
|
@@ -8112,12 +8193,23 @@ export function rscKit(options = {}) {
|
|
|
8112
8193
|
const staticDir = clientOut
|
|
8113
8194
|
? join(dirname(clientOut), "server", NITRO_STATIC_DIR)
|
|
8114
8195
|
: join(outDir, NITRO_STATIC_DIR);
|
|
8115
|
-
const { frozen, results, shells, build: clientBuild } = await prerenderAfterBundles(bundle, staticDir, clientOut ?? publicAssetsDir, builder ? knownActionsOf(builder.config, projectRoot) : []);
|
|
8116
|
-
//
|
|
8117
|
-
//
|
|
8118
|
-
|
|
8119
|
-
|
|
8120
|
-
|
|
8196
|
+
const { frozen, results, shells, routes, pathKey: pathKeyOf, build: clientBuild } = await prerenderAfterBundles(bundle, staticDir, clientOut ?? publicAssetsDir, builder ? knownActionsOf(builder.config, projectRoot) : []);
|
|
8197
|
+
// The edge contract's static pages, each given a payload it boots from
|
|
8198
|
+
// without the app - before the build id, which hashes the documents
|
|
8199
|
+
// this changes, and before the pages are packed into a module or the
|
|
8200
|
+
// assets. A server build only: an export has no edge in front of an app.
|
|
8201
|
+
const listed = output !== "export" && clientOut ? edgeStatic(results, routes, shells.shells) : { static: [], left: [] };
|
|
8202
|
+
const keyOf = new Map(results.map((r) => [contractPath(r.url), pathKeyOf(r.url)]));
|
|
8203
|
+
const statics = clientOut
|
|
8204
|
+
? storedPayloadAssets(staticDir, clientOut, listed.static.map((path) => ({ path, key: keyOf.get(path) })).filter((page) => page.key))
|
|
8205
|
+
: [];
|
|
8206
|
+
const build = clientBuild ? edgeBuildId(clientBuild, staticDir) : undefined;
|
|
8207
|
+
const marker = build ? edgeMarker(build, shells.shells, statics) : null;
|
|
8208
|
+
// Beside the pages too, so the host finds the build and the static
|
|
8209
|
+
// list wherever it reads the pages from.
|
|
8210
|
+
if (marker && output !== "export" && existsSync(staticDir)) {
|
|
8211
|
+
writeFileSync(join(staticDir, EDGE.storedMarker), JSON.stringify(marker));
|
|
8212
|
+
}
|
|
8121
8213
|
// The same pages once more, as a module. A Worker has no filesystem, and
|
|
8122
8214
|
// until this existed every Cloudflare deploy rendered its "static" pages
|
|
8123
8215
|
// live, silently - the reader found no directory and fell through. The
|
|
@@ -8153,8 +8245,13 @@ export function rscKit(options = {}) {
|
|
|
8153
8245
|
// after the prerender, and an export taken any earlier left them out.
|
|
8154
8246
|
if (output === "export")
|
|
8155
8247
|
await exportAfterPrerender(results, staticDir, clientOut ?? publicAssetsDir);
|
|
8156
|
-
if (shells.left.length) {
|
|
8157
|
-
log(`
|
|
8248
|
+
if (shells.left.length || listed.left.length) {
|
|
8249
|
+
log(`edge: not listed in edge.json, so served by the origin: ${[...shells.left, ...listed.left].join("; ")}`);
|
|
8250
|
+
}
|
|
8251
|
+
const guarded = output !== "export" && clientOut ? edgeGuarded(results, routes) : [];
|
|
8252
|
+
if (guarded.length) {
|
|
8253
|
+
const none = marker && !marker.shells.length && !marker.static.length;
|
|
8254
|
+
log(`edge: ${none ? "edge.json lists no page: every one is under middleware" : "left out of edge.json, under middleware"}, which runs per request: ${guarded.join(", ")}`);
|
|
8158
8255
|
}
|
|
8159
8256
|
// What this build produced, where a deploy can read it without parsing
|
|
8160
8257
|
// vite.config.ts - which can turn export on through RSC_OUTPUT, or
|
|
@@ -8169,19 +8266,22 @@ export function rscKit(options = {}) {
|
|
|
8169
8266
|
...compileStep(projectRoot),
|
|
8170
8267
|
...(buildCommands().length ? { commands: buildCommands().map((c) => c.name) } : {}),
|
|
8171
8268
|
}, null, 2) + "\n");
|
|
8172
|
-
// The
|
|
8173
|
-
// cached shell, and the build both endpoints will say they
|
|
8174
|
-
// Written by every server build, with
|
|
8175
|
-
//
|
|
8176
|
-
// say". Not by an export, which has no origin to
|
|
8177
|
-
const
|
|
8178
|
-
|
|
8179
|
-
|
|
8269
|
+
// The edge contract's marker: the pages an edge may answer from a
|
|
8270
|
+
// cached shell or whole, and the build both endpoints will say they
|
|
8271
|
+
// belong to. Written by every server build, with empty lists when
|
|
8272
|
+
// there are none, so a platform can tell "none" from "an rsc-kit too
|
|
8273
|
+
// old to say". Not by an export, which has no origin to answer for it.
|
|
8274
|
+
const markerFile = join(dirname(clientOut), "edge.json");
|
|
8275
|
+
// v1's marker, from a build before this one: an edge reading it
|
|
8276
|
+
// would ask for endpoints that are no longer here.
|
|
8277
|
+
rmSync(join(dirname(clientOut), "ppr.json"), { force: true });
|
|
8278
|
+
if (output !== "export" && marker) {
|
|
8279
|
+
writeFileSync(markerFile, JSON.stringify(marker, null, 2) + "\n");
|
|
8180
8280
|
}
|
|
8181
8281
|
else {
|
|
8182
8282
|
// One left by an earlier server build would promise an origin that
|
|
8183
8283
|
// is not there.
|
|
8184
|
-
rmSync(
|
|
8284
|
+
rmSync(markerFile, { force: true });
|
|
8185
8285
|
}
|
|
8186
8286
|
}
|
|
8187
8287
|
},
|