@rsc-kit/core 0.16.3 → 0.18.0
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/barrelImports.d.ts +6 -0
- package/dist/barrelImports.js +93 -0
- package/dist/barrelImports.js.map +1 -0
- package/dist/buildReport.d.ts +6 -1
- package/dist/buildReport.js +16 -18
- package/dist/buildReport.js.map +1 -1
- package/dist/cache.js +8 -4
- package/dist/cache.js.map +1 -1
- package/dist/clientEntries.d.ts +32 -0
- package/dist/clientEntries.js +107 -0
- package/dist/clientEntries.js.map +1 -0
- package/dist/clientImports.d.ts +36 -0
- package/dist/clientImports.js +36 -0
- package/dist/clientImports.js.map +1 -0
- package/dist/devReload.d.ts +19 -0
- package/dist/devReload.js +32 -0
- package/dist/devReload.js.map +1 -0
- package/dist/events.d.ts +57 -0
- package/dist/events.js +113 -0
- package/dist/events.js.map +1 -0
- package/dist/host.d.ts +16 -6
- package/dist/host.js +219 -124
- package/dist/host.js.map +1 -1
- package/dist/hostCalls.d.ts +27 -0
- package/dist/hostCalls.js +127 -12
- package/dist/hostCalls.js.map +1 -1
- package/dist/hostRouting.d.ts +53 -0
- package/dist/hostRouting.js +100 -0
- package/dist/hostRouting.js.map +1 -0
- package/dist/js/DefaultRouteError.d.ts +3 -0
- package/dist/js/DefaultRouteError.js +61 -0
- package/dist/js/DefaultRouteError.js.map +1 -0
- package/dist/js/Form.js +54 -6
- package/dist/js/Form.js.map +1 -1
- package/dist/js/Link.d.ts +0 -3
- package/dist/js/Link.js +3 -6
- package/dist/js/Link.js.map +1 -1
- package/dist/js/SegmentBoundary.d.ts +38 -2
- package/dist/js/SegmentBoundary.js +51 -13
- package/dist/js/SegmentBoundary.js.map +1 -1
- package/dist/js/createViteRscApp.js +32 -17
- package/dist/js/createViteRscApp.js.map +1 -1
- package/dist/js/devNotice.d.ts +2 -2
- package/dist/js/devNotice.js +17 -7
- package/dist/js/devNotice.js.map +1 -1
- package/dist/js/fallbackReport.d.ts +2 -0
- package/dist/js/fallbackReport.js +51 -0
- package/dist/js/fallbackReport.js.map +1 -0
- package/dist/js/navigate.d.ts +1 -1
- package/dist/js/navigate.js +55 -23
- package/dist/js/navigate.js.map +1 -1
- package/dist/js/queryClient.d.ts +2 -1
- package/dist/js/queryClient.js +0 -12
- package/dist/js/queryClient.js.map +1 -1
- package/dist/js/router.d.ts +2 -2
- package/dist/js/router.js.map +1 -1
- package/dist/js/segmentStore.d.ts +2 -0
- package/dist/js/segmentStore.js +21 -1
- package/dist/js/segmentStore.js.map +1 -1
- package/dist/js/staleAssets.js +10 -1
- package/dist/js/staleAssets.js.map +1 -1
- package/dist/js/standardSchema.d.ts +6 -0
- package/dist/js/standardSchema.js +19 -1
- package/dist/js/standardSchema.js.map +1 -1
- package/dist/js/useEvents.d.ts +25 -0
- package/dist/js/useEvents.js +78 -0
- package/dist/js/useEvents.js.map +1 -0
- package/dist/js/useLinkStatus.d.ts +14 -1
- package/dist/js/useLinkStatus.js +15 -1
- package/dist/js/useLinkStatus.js.map +1 -1
- package/dist/js/usePolling.d.ts +29 -0
- package/dist/js/usePolling.js +141 -0
- package/dist/js/usePolling.js.map +1 -0
- package/dist/manifest.d.ts +18 -1
- package/dist/manifest.js.map +1 -1
- package/dist/metadata.d.ts +94 -15
- package/dist/metadata.js.map +1 -1
- package/dist/metadataRoutes.d.ts +61 -0
- package/dist/metadataRoutes.js +173 -0
- package/dist/metadataRoutes.js.map +1 -0
- package/dist/prerender.js +44 -10
- package/dist/prerender.js.map +1 -1
- package/dist/reactCache.d.ts +1 -0
- package/dist/reactCache.js +52 -0
- package/dist/reactCache.js.map +1 -0
- package/dist/redirect.d.ts +16 -2
- package/dist/redirect.js +10 -19
- package/dist/redirect.js.map +1 -1
- package/dist/request.d.ts +18 -1
- package/dist/request.js +91 -5
- package/dist/request.js.map +1 -1
- package/dist/revalidate.d.ts +6 -4
- package/dist/revalidate.js +6 -5
- package/dist/revalidate.js.map +1 -1
- package/dist/routes.d.ts +16 -3
- package/dist/routes.js +5 -5
- package/dist/routes.js.map +1 -1
- package/dist/routing.d.ts +2 -2
- package/dist/routing.js +47 -25
- package/dist/routing.js.map +1 -1
- package/dist/useSsr.d.ts +19 -0
- package/dist/useSsr.js +104 -0
- package/dist/useSsr.js.map +1 -0
- package/dist/vite.d.ts +86 -9
- package/dist/vite.js +827 -24
- package/dist/vite.js.map +1 -1
- package/package.json +13 -1
package/dist/vite.js
CHANGED
|
@@ -16,13 +16,21 @@ import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync,
|
|
|
16
16
|
import { gzipSync } from "node:zlib";
|
|
17
17
|
import { createHash } from "node:crypto";
|
|
18
18
|
import { createRequire } from "node:module";
|
|
19
|
-
import {
|
|
19
|
+
import { spawnSync } from "node:child_process";
|
|
20
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
20
21
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
21
22
|
import rsc, { getPluginApi } from "@vitejs/plugin-rsc";
|
|
22
23
|
import { loadEnv } from "vite";
|
|
23
24
|
import { REPORT_FILE, buildReport } from "./buildReport.js";
|
|
24
25
|
import { MANIFEST_PATH, manifestWarning, webManifest } from "./webManifest.js";
|
|
25
26
|
import { ASSET_BASE, allAppAssets, appAssets, headTags, typeOf, } from "./appAssets.js";
|
|
27
|
+
import { reactCacheImports } from "./reactCache.js";
|
|
28
|
+
import { clientEntries, clientScanPlugin, engineClientEntries, } from "./clientEntries.js";
|
|
29
|
+
import { serverImportsOfClientPackages } from "./clientImports.js";
|
|
30
|
+
import { automaticSitemap, METADATA_ROUTES, ROOT_FILES, rootFileType, } from "./metadataRoutes.js";
|
|
31
|
+
import { ownHosts } from "./hostRouting.js";
|
|
32
|
+
import { unrollBarrelImports } from "./barrelImports.js";
|
|
33
|
+
import { serverRendererMessage, SERVER_RENDERER, ssrProxyModule, UseSsrError, } from "./useSsr.js";
|
|
26
34
|
import { httpHostCalls } from "./hostCalls.js";
|
|
27
35
|
// Resolved once per rscKit() call. One build runs in one process, so these are
|
|
28
36
|
// module state rather than threaded through every helper.
|
|
@@ -30,8 +38,13 @@ let projectRoot;
|
|
|
30
38
|
let sourceDir;
|
|
31
39
|
let inlineStylesheets = "auto";
|
|
32
40
|
let resolvedConfig = null;
|
|
41
|
+
/** Server files importing a client library, read off the rsc graph when it is built. */
|
|
42
|
+
let clientLibraryImports = [];
|
|
33
43
|
/** Modules a runtime provides and no bundle should try to carry. */
|
|
34
44
|
const RUNTIME_BUILTINS = ["bun", /^bun:/];
|
|
45
|
+
function arrayOf(value) {
|
|
46
|
+
return value == null ? [] : Array.isArray(value) ? value : [value];
|
|
47
|
+
}
|
|
35
48
|
/**
|
|
36
49
|
* What a client chunk is called on disk.
|
|
37
50
|
*
|
|
@@ -93,11 +106,15 @@ let routeConfig;
|
|
|
93
106
|
let prerenderAfterBuild;
|
|
94
107
|
/** True during `vite build --watch`, where re-rendering every route is noise. */
|
|
95
108
|
let isWatch = false;
|
|
96
|
-
/** Whether navigations are wrapped in React's ViewTransition — see options. */
|
|
97
|
-
let viewTransitions = false;
|
|
98
109
|
/** Whether a service worker is generated and registered — see options. */
|
|
99
110
|
let offline = false;
|
|
111
|
+
let typecheck = true;
|
|
100
112
|
let webManifestOptions = null;
|
|
113
|
+
/** The site's own hosts, from the root layout's metadataBase and rscKit({ hosts }). */
|
|
114
|
+
let siteHosts = [];
|
|
115
|
+
let hostsOption = [];
|
|
116
|
+
let barrelImports = true;
|
|
117
|
+
let identify = true;
|
|
101
118
|
let foundAssets = {
|
|
102
119
|
favicon: null,
|
|
103
120
|
icons: [],
|
|
@@ -258,8 +275,11 @@ function resolvePaths(options) {
|
|
|
258
275
|
// sets it, and so does a host that drives the build out of process and
|
|
259
276
|
// prerenders itself afterwards with paths only it knows.
|
|
260
277
|
prerenderAfterBuild = process.env.RSC_PRERENDER !== "0";
|
|
261
|
-
viewTransitions = options.viewTransitions === true;
|
|
262
278
|
offline = options.offline === true;
|
|
279
|
+
typecheck = options.typecheck !== false;
|
|
280
|
+
hostsOption = options.hosts ?? [];
|
|
281
|
+
barrelImports = options.barrelImports !== false;
|
|
282
|
+
identify = options.identify !== false;
|
|
263
283
|
inlineStylesheets = options.inlineStylesheets ?? "auto";
|
|
264
284
|
maxActionBody = options.maxActionBody;
|
|
265
285
|
// One place, and it is the file. A plugin option as well would be the same
|
|
@@ -313,7 +333,12 @@ function urlSegments(componentName) {
|
|
|
313
333
|
continue;
|
|
314
334
|
}
|
|
315
335
|
if (part.startsWith("[") && part.endsWith("]")) {
|
|
316
|
-
|
|
336
|
+
// At the top of app/ - the first segment the url has - a parameter is
|
|
337
|
+
// the host's: bound from acme.example.com, never from example.com/acme.
|
|
338
|
+
segments.push({
|
|
339
|
+
type: segments.length === 0 ? "host" : "param",
|
|
340
|
+
value: part.slice(1, -1),
|
|
341
|
+
});
|
|
317
342
|
continue;
|
|
318
343
|
}
|
|
319
344
|
// An interception marker says which url this replaces, not what it is
|
|
@@ -385,7 +410,11 @@ function routeManifest() {
|
|
|
385
410
|
* to whoever knows what they mean.
|
|
386
411
|
*/
|
|
387
412
|
const middlewareIn = (absDir) => {
|
|
388
|
-
|
|
413
|
+
// middleware.ts first: it is the file named for what this is. A guard
|
|
414
|
+
// the engine runs is its default export; the names a host runs are its
|
|
415
|
+
// `middleware` export, and a file may carry either or both. route.ts is
|
|
416
|
+
// read too, for the apps written before middleware.ts could.
|
|
417
|
+
for (const file of ["middleware.ts", "middleware.tsx", "route.ts", "route.tsx"]) {
|
|
389
418
|
const path = join(absDir, file);
|
|
390
419
|
if (!existsSync(path))
|
|
391
420
|
continue;
|
|
@@ -471,14 +500,22 @@ function routeManifest() {
|
|
|
471
500
|
// writing the site out, and knowing which filename the client will ask for.
|
|
472
501
|
return {
|
|
473
502
|
version: 1,
|
|
474
|
-
build: {
|
|
503
|
+
build: {
|
|
504
|
+
output,
|
|
505
|
+
exportPath,
|
|
506
|
+
payloadName: staticPayloads,
|
|
507
|
+
hosts: siteHosts,
|
|
508
|
+
identify,
|
|
509
|
+
},
|
|
475
510
|
routes,
|
|
476
511
|
intercepts,
|
|
477
|
-
apis: [...apiRoutes.values()].map(({ name, methods }) => ({
|
|
512
|
+
apis: [...apiRoutes.values()].map(({ name, methods, generated }) => ({
|
|
478
513
|
name,
|
|
479
514
|
segments: urlSegments(name),
|
|
480
515
|
methods,
|
|
481
|
-
|
|
516
|
+
// A synthesised robots.txt or sitemap.xml runs no guard: it exists to be
|
|
517
|
+
// read by anyone, and a guard on the root would 401 the crawler.
|
|
518
|
+
middleware: generated ? [] : ancestors(name, "middleware"),
|
|
482
519
|
})),
|
|
483
520
|
};
|
|
484
521
|
}
|
|
@@ -956,6 +993,9 @@ export function declaredManifest(appDir) {
|
|
|
956
993
|
function copyAppAssets(clientDir) {
|
|
957
994
|
if (!existsSync(clientDir))
|
|
958
995
|
return;
|
|
996
|
+
for (const file of rootFiles) {
|
|
997
|
+
copyFileSync(join(sourceDir, "app", file), join(clientDir, file));
|
|
998
|
+
}
|
|
959
999
|
const all = allAppAssets(foundAssets);
|
|
960
1000
|
if (all.length === 0)
|
|
961
1001
|
return;
|
|
@@ -1221,6 +1261,41 @@ async function prerenderAfterBundles(bundle, staticDir, assetsDir, knownActions
|
|
|
1221
1261
|
// places to look for one fact.
|
|
1222
1262
|
const manifest = engine.manifest();
|
|
1223
1263
|
const apis = await prerenderApiRoutes(engine, manifest, writeTo(staticDir));
|
|
1264
|
+
// A sitemap the build writes itself, when the app wrote none: every url
|
|
1265
|
+
// it stored or was told about, minus the guarded ones. It needs the site's
|
|
1266
|
+
// host, which the root layout's metadataBase is for; without one there is
|
|
1267
|
+
// nothing to write and the report says so once.
|
|
1268
|
+
const writesSitemap = manifest.apis?.some((api) => api.name === "app/sitemap.xml/route");
|
|
1269
|
+
const hasSitemapFile = rootFiles.includes("sitemap.xml");
|
|
1270
|
+
if (!writesSitemap && !hasSitemapFile) {
|
|
1271
|
+
const base = rootMetadataBase(join(sourceDir, "app"));
|
|
1272
|
+
if (base) {
|
|
1273
|
+
const xml = automaticSitemap(results, manifest.routes, base);
|
|
1274
|
+
const stored = JSON.stringify({
|
|
1275
|
+
status: 200,
|
|
1276
|
+
headers: [["content-type", "application/xml; charset=utf-8"]],
|
|
1277
|
+
body: xml,
|
|
1278
|
+
varies: false,
|
|
1279
|
+
});
|
|
1280
|
+
await writeTo(staticDir)("sitemap.xml.api.json", stored);
|
|
1281
|
+
pending.push({
|
|
1282
|
+
line: " ○ /sitemap.xml",
|
|
1283
|
+
bytes: null,
|
|
1284
|
+
extra: [
|
|
1285
|
+
` written by the build: ${xml.split("<url>").length - 1} urls; a sitemap.ts beside the root layout replaces it`,
|
|
1286
|
+
],
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
else {
|
|
1290
|
+
pending.push({
|
|
1291
|
+
line: " - /sitemap.xml",
|
|
1292
|
+
bytes: null,
|
|
1293
|
+
extra: [
|
|
1294
|
+
" not written: the root layout has no metadataBase to make the urls absolute",
|
|
1295
|
+
],
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1224
1299
|
for (const api of apis) {
|
|
1225
1300
|
pending.push({
|
|
1226
1301
|
line: ` ${api.type === "frozen" ? "○" : "ƒ"} ${api.url}`,
|
|
@@ -1258,6 +1333,30 @@ async function prerenderAfterBundles(bundle, staticDir, assetsDir, knownActions
|
|
|
1258
1333
|
console.log(" Nothing checks who calls them. Fine for a public one; otherwise build it\n" +
|
|
1259
1334
|
" from an action client, so the check cannot be forgotten.");
|
|
1260
1335
|
}
|
|
1336
|
+
// Server files importing cache from React. Its cache() memoises on the
|
|
1337
|
+
// dispatcher a render installs, so in a guard, an action, an api route or
|
|
1338
|
+
// the SSR pass it calls straight through - no dedupe, no error, the helper
|
|
1339
|
+
// runs twice. The difference is silent, which is why this line exists.
|
|
1340
|
+
const reactCache = reactCacheImports(sourceDir);
|
|
1341
|
+
if (reactCache.length > 0) {
|
|
1342
|
+
console.log(`\n \u26a0 ${reactCache.length} server ${reactCache.length === 1 ? "file imports" : "files import"} cache from 'react': ` +
|
|
1343
|
+
reactCache.join(", "));
|
|
1344
|
+
console.log(" React's cache() dedupes only inside a component render; in a guard, an action or\n" +
|
|
1345
|
+
" an api route it calls straight through. Import it from @rsc-kit/core/cache instead.");
|
|
1346
|
+
}
|
|
1347
|
+
// Server files importing a client library. Legal - a server component may
|
|
1348
|
+
// render a client component from a package - but the shape that costs an
|
|
1349
|
+
// afternoon is a file with no "use client" that only wraps them, so the
|
|
1350
|
+
// library's internals run on the server. Said, with the packages and the
|
|
1351
|
+
// importer, so the person reading can tell which it is.
|
|
1352
|
+
if (clientLibraryImports.length > 0) {
|
|
1353
|
+
console.log(`\n \u2139 ${clientLibraryImports.length} server ${clientLibraryImports.length === 1 ? "file imports" : "files import"} a client library: ` +
|
|
1354
|
+
clientLibraryImports
|
|
1355
|
+
.map((c) => `${c.file} (${c.packages.join(", ")}${c.from ? `; imported by ${c.from}` : ""})`)
|
|
1356
|
+
.join("; "));
|
|
1357
|
+
console.log(' Legal for a server component. A file that only wraps client components wants "use client" -\n' +
|
|
1358
|
+
" as shadcn ships it - so the server stops at the boundary.");
|
|
1359
|
+
}
|
|
1261
1360
|
// Written from the rows that were just printed rather than recomputed: the
|
|
1262
1361
|
// report and the terminal must not be able to disagree about what happened.
|
|
1263
1362
|
writeFileSync(join(outDir, REPORT_FILE), buildReport(results.map((r) => ({
|
|
@@ -1274,7 +1373,7 @@ async function prerenderAfterBundles(bundle, staticDir, assetsDir, knownActions
|
|
|
1274
1373
|
type: a.type,
|
|
1275
1374
|
reason: a.reason,
|
|
1276
1375
|
warning: a.warning ?? null,
|
|
1277
|
-
})), audited));
|
|
1376
|
+
})), audited, reactCache, clientLibraryImports));
|
|
1278
1377
|
const note = notes(results);
|
|
1279
1378
|
const counted = [...results, ...apis];
|
|
1280
1379
|
console.log(`
|
|
@@ -1441,6 +1540,23 @@ function renderRouteTypes(manifest) {
|
|
|
1441
1540
|
const target = relative(typesDir, join(sourceDir, route.component)).replace(/\\/g, "/");
|
|
1442
1541
|
search.set(pattern, target.startsWith(".") ? target : "./" + target);
|
|
1443
1542
|
}
|
|
1543
|
+
// Section names are read from the source - section('orders', …) - the way
|
|
1544
|
+
// generateStaticParams is detected, because this runs before any bundle
|
|
1545
|
+
// exists. A name computed at runtime is not seen and revalidate() falls
|
|
1546
|
+
// back to refusing it at the renderer, as before.
|
|
1547
|
+
const regions = [
|
|
1548
|
+
...new Set([
|
|
1549
|
+
...[...components.values()]
|
|
1550
|
+
.filter((c) => SECTION_FILE.test(c.absPath))
|
|
1551
|
+
.flatMap((c) => [
|
|
1552
|
+
...readFileSync(c.absPath, "utf-8").matchAll(/\bsection\(\s*['"`]([^'"`]+)['"`]/g),
|
|
1553
|
+
].map((m) => m[1])),
|
|
1554
|
+
...[...components.keys()].flatMap((name) => {
|
|
1555
|
+
const slot = name.split("/").find((part) => part.startsWith("@"));
|
|
1556
|
+
return slot ? [slot.slice(1)] : [];
|
|
1557
|
+
}),
|
|
1558
|
+
]),
|
|
1559
|
+
].sort();
|
|
1444
1560
|
return [
|
|
1445
1561
|
"// @generated — do not edit. Written by the RSC build from the route tree.",
|
|
1446
1562
|
"//",
|
|
@@ -1474,6 +1590,14 @@ function renderRouteTypes(manifest) {
|
|
|
1474
1590
|
// Api routes are a separate union, so Link refuses an api url and apiUrl()
|
|
1475
1591
|
// refuses a page. Linking to an api route navigates the browser away to a
|
|
1476
1592
|
// json document, which is the mistake worth catching.
|
|
1593
|
+
// Regions: every section('name', …) the build read, and every @slot
|
|
1594
|
+
// directory. This is what types revalidate().
|
|
1595
|
+
" interface RegisterRegions {",
|
|
1596
|
+
regions.length > 0
|
|
1597
|
+
? " regions:\n" +
|
|
1598
|
+
regions.map((r) => " | " + JSON.stringify(r)).join("\n")
|
|
1599
|
+
: " // No sections or slots found under the source directory.\n regions: never",
|
|
1600
|
+
" }",
|
|
1477
1601
|
" interface RegisterApi {",
|
|
1478
1602
|
apis.length > 0
|
|
1479
1603
|
? " apis:\n" +
|
|
@@ -1571,6 +1695,76 @@ const components = new Map();
|
|
|
1571
1695
|
* a default component.
|
|
1572
1696
|
*/
|
|
1573
1697
|
const apiRoutes = new Map();
|
|
1698
|
+
/** Files beside the root layout served at the root as they are: robots.txt, a hand-written sitemap.xml, humans.txt. */
|
|
1699
|
+
let rootFiles = [];
|
|
1700
|
+
const METADATA_ROUTE_ID = "virtual:rsc-kit/metadata-route/";
|
|
1701
|
+
/**
|
|
1702
|
+
* robots.ts, sitemap.ts, llms.ts beside the root layout, each registered as
|
|
1703
|
+
* an api route at the file it stands for. The module the entry imports is
|
|
1704
|
+
* synthesised (metadataRoutesPlugin): the app's default export, formatted by
|
|
1705
|
+
* the engine, with the root layout's metadataBase for relative urls.
|
|
1706
|
+
*
|
|
1707
|
+
* Api routes, so nothing else is new: the build stores the ones that read
|
|
1708
|
+
* nothing per request and names the ones that do. Without middleware, on
|
|
1709
|
+
* purpose - these exist to be read by anyone, and a guard on the root would
|
|
1710
|
+
* otherwise 401 the crawler asking for robots.txt.
|
|
1711
|
+
*/
|
|
1712
|
+
function registerMetadataRoutes(appDir) {
|
|
1713
|
+
rootFiles = existsSync(appDir)
|
|
1714
|
+
? readdirSync(appDir)
|
|
1715
|
+
.filter((name) => ROOT_FILES.test(name))
|
|
1716
|
+
.sort()
|
|
1717
|
+
: [];
|
|
1718
|
+
for (const kind of Object.keys(METADATA_ROUTES)) {
|
|
1719
|
+
const source = ["ts", "tsx", "js", "mjs"]
|
|
1720
|
+
.map((ext) => join(appDir, `${kind}.${ext}`))
|
|
1721
|
+
.find((file) => existsSync(file));
|
|
1722
|
+
if (!source)
|
|
1723
|
+
continue;
|
|
1724
|
+
const { file } = METADATA_ROUTES[kind];
|
|
1725
|
+
if (rootFiles.includes(file)) {
|
|
1726
|
+
throw new Error(`[rsc-kit] app/${basename(source)} and app/${file} both answer /${file}. Keep one: the function, or the file as written.`);
|
|
1727
|
+
}
|
|
1728
|
+
const name = `app/${file}/route`;
|
|
1729
|
+
if (apiRoutes.has(name)) {
|
|
1730
|
+
throw new Error(`[rsc-kit] app/${basename(source)} and app/${file}/route.ts both answer /${file}. Keep one.`);
|
|
1731
|
+
}
|
|
1732
|
+
apiRoutes.set(name, {
|
|
1733
|
+
name,
|
|
1734
|
+
absPath: METADATA_ROUTE_ID + kind,
|
|
1735
|
+
methods: ["GET"],
|
|
1736
|
+
generated: { kind, file: source },
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
/** The module behind a synthesised metadata route. */
|
|
1741
|
+
function metadataRoutesPlugin() {
|
|
1742
|
+
return {
|
|
1743
|
+
name: "rsc-kit:metadata-routes",
|
|
1744
|
+
resolveId(id) {
|
|
1745
|
+
if (id.startsWith(METADATA_ROUTE_ID))
|
|
1746
|
+
return "\0" + id;
|
|
1747
|
+
},
|
|
1748
|
+
load(id) {
|
|
1749
|
+
if (!id.startsWith("\0" + METADATA_ROUTE_ID))
|
|
1750
|
+
return;
|
|
1751
|
+
const kind = id.slice(("\0" + METADATA_ROUTE_ID).length);
|
|
1752
|
+
const route = [...apiRoutes.values()].find((r) => r.generated?.kind === kind);
|
|
1753
|
+
if (!route?.generated)
|
|
1754
|
+
return;
|
|
1755
|
+
const rootLayout = components.get("app/layout")?.absPath ?? null;
|
|
1756
|
+
return [
|
|
1757
|
+
`import produce from ${JSON.stringify(route.generated.file)}`,
|
|
1758
|
+
rootLayout
|
|
1759
|
+
? `import * as __root from ${JSON.stringify(rootLayout)}`
|
|
1760
|
+
: "const __root = {}",
|
|
1761
|
+
`import { metadataResponse } from ${JSON.stringify(join(packageDir, "metadataRoutes"))}`,
|
|
1762
|
+
`export const GET = () => metadataResponse(${JSON.stringify(kind)}, produce, __root.metadata?.metadataBase ?? null)`,
|
|
1763
|
+
"",
|
|
1764
|
+
].join("\n");
|
|
1765
|
+
},
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1574
1768
|
function register(absPath) {
|
|
1575
1769
|
const name = componentName(absPath);
|
|
1576
1770
|
const existing = components.get(name);
|
|
@@ -1605,10 +1799,20 @@ function registerApiRoute(absPath) {
|
|
|
1605
1799
|
}
|
|
1606
1800
|
apiRoutes.set(name, { name, absPath, methods });
|
|
1607
1801
|
}
|
|
1802
|
+
/**
|
|
1803
|
+
* Whether a middleware.ts is a guard the engine runs, or only names guards
|
|
1804
|
+
* the host runs. The engine imports a guard's default export; a file with
|
|
1805
|
+
* none - `export const middleware = ['auth']` and nothing else - has nothing
|
|
1806
|
+
* to import, and registering it would make the build fail on an export that
|
|
1807
|
+
* was never meant to exist.
|
|
1808
|
+
*/
|
|
1809
|
+
function isEngineGuard(absPath) {
|
|
1810
|
+
return /export\s+default\b/.test(readFileSync(absPath, "utf-8"));
|
|
1811
|
+
}
|
|
1608
1812
|
function discover(dir) {
|
|
1609
1813
|
for (const base of ROUTE_FILES) {
|
|
1610
1814
|
const p = findRouteFile(dir, base);
|
|
1611
|
-
if (p)
|
|
1815
|
+
if (p && (base !== "middleware" || isEngineGuard(p)))
|
|
1612
1816
|
register(p);
|
|
1613
1817
|
}
|
|
1614
1818
|
// route.ts — an api endpoint, colocated with the pages it sits among. Read
|
|
@@ -1639,6 +1843,21 @@ function discover(dir) {
|
|
|
1639
1843
|
* pages export only the static object, so referencing both meant that warning
|
|
1640
1844
|
* for almost every route in an app.
|
|
1641
1845
|
*/
|
|
1846
|
+
/**
|
|
1847
|
+
* The root layout's metadataBase, read from the source. Wanted while the
|
|
1848
|
+
* manifest is built, before there is a bundle to execute - the same reason
|
|
1849
|
+
* generateStaticParams is detected by reading. A `new URL('…')` or a string
|
|
1850
|
+
* literal; anything computed is not seen, and rscKit({ hosts }) says it.
|
|
1851
|
+
*/
|
|
1852
|
+
function rootMetadataBase(appDir) {
|
|
1853
|
+
const layout = ["layout.tsx", "layout.jsx", "layout.ts", "layout.js"]
|
|
1854
|
+
.map((name) => join(appDir, name))
|
|
1855
|
+
.find((file) => existsSync(file));
|
|
1856
|
+
if (!layout)
|
|
1857
|
+
return null;
|
|
1858
|
+
const match = /metadataBase\s*:\s*(?:new\s+URL\(\s*)?["'`](https?:\/\/[^"'`]+)["'`]/.exec(readFileSync(layout, "utf-8"));
|
|
1859
|
+
return match ? match[1] : null;
|
|
1860
|
+
}
|
|
1642
1861
|
function metadataExports(absPath) {
|
|
1643
1862
|
const src = readFileSync(absPath, "utf-8");
|
|
1644
1863
|
return {
|
|
@@ -1833,7 +2052,29 @@ function installHostCallsOnce(): void {
|
|
|
1833
2052
|
}
|
|
1834
2053
|
|
|
1835
2054
|
`;
|
|
2055
|
+
/**
|
|
2056
|
+
* The app's process bootstrap, if it has one: `instrumentation.ts` beside
|
|
2057
|
+
* `app/`, the name Next uses so a reader arriving from there knows what it
|
|
2058
|
+
* is.
|
|
2059
|
+
*
|
|
2060
|
+
* Two things make it a framework concern rather than an import the app
|
|
2061
|
+
* adds. Module evaluation order: a page that configures a shared package at
|
|
2062
|
+
* import time runs before any module the app could put first, and only the
|
|
2063
|
+
* generated entry can import something before the pages. And "before the
|
|
2064
|
+
* first request": an async `register()` — connecting, validating env — has
|
|
2065
|
+
* to be awaited by every entry point a host or a prerender can call, which
|
|
2066
|
+
* is a list the app cannot see.
|
|
2067
|
+
*/
|
|
2068
|
+
function instrumentationFile() {
|
|
2069
|
+
for (const ext of ["ts", "tsx", "mts", "js", "mjs"]) {
|
|
2070
|
+
const file = join(sourceDir, `instrumentation.${ext}`);
|
|
2071
|
+
if (existsSync(file))
|
|
2072
|
+
return file;
|
|
2073
|
+
}
|
|
2074
|
+
return null;
|
|
2075
|
+
}
|
|
1836
2076
|
function generateEntryRsc(fallbackOrigin = "") {
|
|
2077
|
+
const instrumentation = instrumentationFile();
|
|
1837
2078
|
// The 404 page, if the app has one, and the layouts it renders inside.
|
|
1838
2079
|
// Computed here rather than looked up at runtime: not-found is not a route,
|
|
1839
2080
|
// so the manifest has no entry to read its chain from.
|
|
@@ -1890,12 +2131,20 @@ function generateEntryRsc(fallbackOrigin = "") {
|
|
|
1890
2131
|
// from src/ in its own repo and from dist/ once published, and Vite resolves
|
|
1891
2132
|
// either. Naming .tsx here builds fine from source and fails after publish.
|
|
1892
2133
|
return `// GENERATED by rscKit() — do not edit.
|
|
2134
|
+
${
|
|
2135
|
+
// First, before any page: an import's side effects run in import order,
|
|
2136
|
+
// and a package configured here has to be configured before a page module
|
|
2137
|
+
// that reads it at evaluation time.
|
|
2138
|
+
instrumentation
|
|
2139
|
+
? `import * as __instrumentation from ${JSON.stringify(instrumentation)}`
|
|
2140
|
+
: "const __instrumentation: { register?: () => unknown } = {}"}
|
|
1893
2141
|
import { SegmentBoundary } from ${JSON.stringify(join(packageDir, "js/SegmentBoundary"))}
|
|
1894
2142
|
import { DocumentTitle } from ${JSON.stringify(join(packageDir, "js/DocumentTitle"))}
|
|
1895
2143
|
import { SlotBoundary } from ${JSON.stringify(join(packageDir, "js/SlotBoundary"))}
|
|
1896
2144
|
import { RouteErrorBoundary } from ${JSON.stringify(join(packageDir, "js/RouteErrorBoundary"))}
|
|
1897
2145
|
import { sectionComponent } from ${JSON.stringify(join(packageDir, "js/section"))}
|
|
1898
2146
|
import { PathnameProvider } from ${JSON.stringify(join(packageDir, "js/PathnameProvider"))}
|
|
2147
|
+
import { DefaultRouteError } from ${JSON.stringify(join(packageDir, "js/DefaultRouteError"))}
|
|
1899
2148
|
import { searchParams as requestSearchParams } from ${JSON.stringify(join(packageDir, "request"))}
|
|
1900
2149
|
import { parseParams, parseSearchParams, parseBody, isSearchParamsError, isBodyError } from ${JSON.stringify(join(packageDir, "routeSchema"))}
|
|
1901
2150
|
import { notFoundDigest, isNotFoundSignal } from ${JSON.stringify(join(packageDir, "notFound"))}
|
|
@@ -1907,6 +2156,9 @@ import { prerenderedBeside } from ${JSON.stringify(join(packageDir, "files"))}
|
|
|
1907
2156
|
import { renderToReadableStream, decodeReply, loadServerAction } from '@vitejs/plugin-rsc/rsc'
|
|
1908
2157
|
import { isQuery, queryCacheControl, isQueryValidationError } from ${JSON.stringify(join(packageDir, "query"))}
|
|
1909
2158
|
import { isActionValidationError, isClientBuilt } from ${JSON.stringify(join(packageDir, "action"))}
|
|
2159
|
+
import { noteFallback as noteCaughtRead } from ${JSON.stringify(join(packageDir, "request"))}
|
|
2160
|
+
import { isOutdatedOptimizedDep, outdatedDepResponse } from ${JSON.stringify(join(packageDir, "devReload"))}
|
|
2161
|
+
import { sharedDepth } from ${JSON.stringify(join(packageDir, "routing"))}
|
|
1910
2162
|
import { Suspense, createElement, Fragment } from 'react'
|
|
1911
2163
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
1912
2164
|
${imports.join("\n")}
|
|
@@ -2032,6 +2284,7 @@ export async function handleApiRoute(
|
|
|
2032
2284
|
params: Record<string, string>,
|
|
2033
2285
|
allow: string,
|
|
2034
2286
|
): Promise<Response> {
|
|
2287
|
+
await instrumented()
|
|
2035
2288
|
applyHost()
|
|
2036
2289
|
|
|
2037
2290
|
const mod = apiRoutes[name]
|
|
@@ -2139,6 +2392,8 @@ export async function auditActions(ids: string[]): Promise<{ id: string; client:
|
|
|
2139
2392
|
* that asked for everything, or the reverse.
|
|
2140
2393
|
*/
|
|
2141
2394
|
export async function getStaticParams(component: string): Promise<Record<string, string>[] | null> {
|
|
2395
|
+
await instrumented()
|
|
2396
|
+
|
|
2142
2397
|
const generate = staticParamsMap[component]
|
|
2143
2398
|
|
|
2144
2399
|
if (!generate) return null
|
|
@@ -2162,6 +2417,57 @@ const HOST_GLOBAL = ${JSON.stringify(hostGlobal)}
|
|
|
2162
2417
|
*/
|
|
2163
2418
|
const HOST_MIDDLEWARE_FN = '__rsc.middleware'
|
|
2164
2419
|
|
|
2420
|
+
/**
|
|
2421
|
+
* instrumentation.ts's register(), once.
|
|
2422
|
+
*
|
|
2423
|
+
* On a long-lived server it runs at startup: the entry is evaluated when the
|
|
2424
|
+
* process starts, and this begins then, so env that fails validation fails
|
|
2425
|
+
* the boot rather than the first visitor, and the first request does not
|
|
2426
|
+
* pay for it. On a Worker there is no startup — an isolate is created for a
|
|
2427
|
+
* request, and a binding is only readable once one has arrived — so it runs
|
|
2428
|
+
* at the first request of each isolate instead. Every entry point awaits it
|
|
2429
|
+
* either way, which is what makes "before the first render" hold whether the
|
|
2430
|
+
* caller is the built server, the dev server or the prerender.
|
|
2431
|
+
*
|
|
2432
|
+
* A rejection is not memoised. Env that fails validation fails the same way
|
|
2433
|
+
* on every request, which is right; a database that was not up yet gets
|
|
2434
|
+
* asked again rather than leaving the process permanently refusing. In
|
|
2435
|
+
* production a startup failure is reported and the process exits, because a
|
|
2436
|
+
* server that could not bootstrap has nothing correct to serve; the dev
|
|
2437
|
+
* server stays up and reports it on the page instead.
|
|
2438
|
+
*/
|
|
2439
|
+
let __instrumented: Promise<void> | null = null
|
|
2440
|
+
|
|
2441
|
+
function instrumented(): Promise<void> {
|
|
2442
|
+
if (!__instrumented) {
|
|
2443
|
+
__instrumented = Promise.resolve()
|
|
2444
|
+
.then(() => __instrumentation.register?.())
|
|
2445
|
+
.then(
|
|
2446
|
+
() => undefined,
|
|
2447
|
+
(error) => {
|
|
2448
|
+
__instrumented = null
|
|
2449
|
+
throw error
|
|
2450
|
+
},
|
|
2451
|
+
)
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
return __instrumented
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
// Workers name themselves; nothing else does. Where there is a process to
|
|
2458
|
+
// start, start now.
|
|
2459
|
+
const isolateRuntime = typeof navigator !== 'undefined' && navigator.userAgent === 'Cloudflare-Workers'
|
|
2460
|
+
|
|
2461
|
+
if (__instrumentation.register && !isolateRuntime) {
|
|
2462
|
+
instrumented().catch((error) => {
|
|
2463
|
+
console.error('[rsc-kit] instrumentation.ts register() failed:', error)
|
|
2464
|
+
|
|
2465
|
+
if (!import.meta.env.DEV && typeof process !== 'undefined' && typeof process.exit === 'function') {
|
|
2466
|
+
process.exit(1)
|
|
2467
|
+
}
|
|
2468
|
+
})
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2165
2471
|
let currentHost: HostFn | null = null
|
|
2166
2472
|
|
|
2167
2473
|
export function installHostFn(fn: HostFn) {
|
|
@@ -2395,6 +2701,19 @@ function buildElement(
|
|
|
2395
2701
|
)
|
|
2396
2702
|
}
|
|
2397
2703
|
|
|
2704
|
+
// Outermost, for a throw no error.tsx covers - including one in a layout,
|
|
2705
|
+
// which the boundary inside that layout cannot see. Without it React
|
|
2706
|
+
// unmounted the document on hydration: a black page with the cause nowhere
|
|
2707
|
+
// near it. Only with the runtime: it is a client component, and a route
|
|
2708
|
+
// shipping none has nothing to catch with.
|
|
2709
|
+
if (bootstrap) {
|
|
2710
|
+
element = createElement(
|
|
2711
|
+
RouteErrorBoundary,
|
|
2712
|
+
{ fallback: DefaultRouteError as never, resetKey: pageKey || component },
|
|
2713
|
+
element,
|
|
2714
|
+
)
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2398
2717
|
// <title>/<meta> go OUTSIDE the Suspense boundaries so they reach the shell
|
|
2399
2718
|
// immediately — inside, they would be withheld until the page's data
|
|
2400
2719
|
// resolves, delaying the whole document on a slow page.
|
|
@@ -2548,6 +2867,40 @@ async function renderTree(
|
|
|
2548
2867
|
if (bootstrap) head.push(createElement(DocumentTitle, { key: '__ts', title: String(md.title) }))
|
|
2549
2868
|
}
|
|
2550
2869
|
if (md.description != null) head.push(createElement('meta', { key: '__d', name: 'description', content: String(md.description) }))
|
|
2870
|
+
// The name only, never the version - see the identify option.
|
|
2871
|
+
if (manifest().build?.identify) head.push(createElement('meta', { key: '__g', name: 'generator', content: 'rsc-kit' }))
|
|
2872
|
+
|
|
2873
|
+
// robots is a string, or the object Next takes: index and follow as
|
|
2874
|
+
// their no- forms, the flags by name, the limits as name:value. googleBot
|
|
2875
|
+
// is the same shape for the googlebot tag. The object used to fall
|
|
2876
|
+
// through to the catch-all below as "[object Object]" - which no crawler
|
|
2877
|
+
// reads, on the one page that asked not to be indexed.
|
|
2878
|
+
const robotsContent = (value: unknown): string => {
|
|
2879
|
+
if (typeof value !== 'object' || value === null) return String(value)
|
|
2880
|
+
|
|
2881
|
+
const r = value as Record<string, unknown>
|
|
2882
|
+
const parts: string[] = []
|
|
2883
|
+
|
|
2884
|
+
if (r.index != null) parts.push(r.index ? 'index' : 'noindex')
|
|
2885
|
+
if (r.follow != null) parts.push(r.follow ? 'follow' : 'nofollow')
|
|
2886
|
+
for (const flag of ['noarchive', 'nosnippet', 'noimageindex', 'nocache', 'notranslate', 'indexifembedded', 'nositelinkssearchbox']) {
|
|
2887
|
+
if (r[flag]) parts.push(flag)
|
|
2888
|
+
}
|
|
2889
|
+
if (r.unavailable_after != null) parts.push('unavailable_after: ' + String(r.unavailable_after))
|
|
2890
|
+
for (const limit of ['max-video-preview', 'max-image-preview', 'max-snippet']) {
|
|
2891
|
+
if (r[limit] != null) parts.push(limit + ':' + String(r[limit]))
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
return parts.join(', ')
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
if (md.robots != null) {
|
|
2898
|
+
head.push(createElement('meta', { key: '__r', name: 'robots', content: robotsContent(md.robots) }))
|
|
2899
|
+
|
|
2900
|
+
const bot = typeof md.robots === 'object' ? (md.robots as { googleBot?: unknown }).googleBot : null
|
|
2901
|
+
|
|
2902
|
+
if (bot != null) head.push(createElement('meta', { key: '__rg', name: 'googlebot', content: robotsContent(bot) }))
|
|
2903
|
+
}
|
|
2551
2904
|
|
|
2552
2905
|
// og: and its relatives are PROPERTY, not name. Facebook's scraper - and
|
|
2553
2906
|
// Slack's, and LinkedIn's - reads only property=, so every og tag this
|
|
@@ -2653,7 +3006,7 @@ async function renderTree(
|
|
|
2653
3006
|
// meta tags rather than a meta tag by that name. A key at the top level
|
|
2654
3007
|
// still renders - the type no longer invites one, but an app written
|
|
2655
3008
|
// against the old shape must not silently lose its tags.
|
|
2656
|
-
const structured = new Set(['title', 'description', 'metadataBase', 'openGraph', 'twitter', 'icons', 'other'])
|
|
3009
|
+
const structured = new Set(['title', 'description', 'robots', 'metadataBase', 'openGraph', 'twitter', 'icons', 'other'])
|
|
2657
3010
|
const named = Object.entries(md).filter(([k]) => !structured.has(k))
|
|
2658
3011
|
const extra = Object.entries((md.other ?? {}) as Record<string, unknown>)
|
|
2659
3012
|
|
|
@@ -2817,6 +3170,7 @@ async function runMiddleware(component: string, props: Record<string, unknown> =
|
|
|
2817
3170
|
* throws, exactly as it does mid-render.
|
|
2818
3171
|
*/
|
|
2819
3172
|
export async function runRouteMiddleware(component: string, props: Record<string, unknown> = {}): Promise<void> {
|
|
3173
|
+
await instrumented()
|
|
2820
3174
|
applyHost()
|
|
2821
3175
|
|
|
2822
3176
|
return runMiddleware(component, props)
|
|
@@ -2832,6 +3186,7 @@ export async function handleRscStream(
|
|
|
2832
3186
|
from = 0,
|
|
2833
3187
|
pageKey = '',
|
|
2834
3188
|
): Promise<{ stream: ReadableStream; clientChunks: unknown; segmentDepth: number }> {
|
|
3189
|
+
await instrumented()
|
|
2835
3190
|
applyHost()
|
|
2836
3191
|
|
|
2837
3192
|
// The host proposes how much the client already has; the engine decides what
|
|
@@ -2887,6 +3242,7 @@ export async function handleRscHtmlStream(
|
|
|
2887
3242
|
pageKey = '',
|
|
2888
3243
|
bootstrap = true,
|
|
2889
3244
|
): Promise<{ htmlStream: ReadableStream; rscPayloadPromise: Promise<string>; clientChunks: unknown }> {
|
|
3245
|
+
await instrumented()
|
|
2890
3246
|
applyHost()
|
|
2891
3247
|
await runMiddleware(component, props)
|
|
2892
3248
|
const flight = renderToReadableStream(
|
|
@@ -2922,6 +3278,7 @@ export async function handleRscResume(
|
|
|
2922
3278
|
nonce?: string,
|
|
2923
3279
|
pageKey = '',
|
|
2924
3280
|
): Promise<{ htmlStream: ReadableStream }> {
|
|
3281
|
+
await instrumented()
|
|
2925
3282
|
applyHost()
|
|
2926
3283
|
await runMiddleware(component, props)
|
|
2927
3284
|
|
|
@@ -3072,6 +3429,7 @@ export async function handleQuery(
|
|
|
3072
3429
|
| { status: number; message: string; errors?: Record<string, string[]> }
|
|
3073
3430
|
| null
|
|
3074
3431
|
> {
|
|
3432
|
+
await instrumented()
|
|
3075
3433
|
applyHost()
|
|
3076
3434
|
|
|
3077
3435
|
let fn: unknown
|
|
@@ -3120,6 +3478,7 @@ export async function handleAction(
|
|
|
3120
3478
|
page?: PageContext,
|
|
3121
3479
|
takeRevalidated?: () => string[],
|
|
3122
3480
|
): Promise<{ stream: ReadableStream }> {
|
|
3481
|
+
await instrumented()
|
|
3123
3482
|
applyHost()
|
|
3124
3483
|
|
|
3125
3484
|
// Every body arrives as bytes on its own socket frame — an upload because it
|
|
@@ -3212,6 +3571,8 @@ export async function resolveMetadata(
|
|
|
3212
3571
|
props: Record<string, unknown> = {},
|
|
3213
3572
|
layouts: LayoutEntry[] = [],
|
|
3214
3573
|
): Promise<Record<string, unknown> | null> {
|
|
3574
|
+
await instrumented()
|
|
3575
|
+
|
|
3215
3576
|
const pageEntry = metadataMap[component]
|
|
3216
3577
|
const page: Record<string, unknown> = pageEntry
|
|
3217
3578
|
? (pageEntry.generate
|
|
@@ -3281,6 +3642,7 @@ export async function handleRsc(
|
|
|
3281
3642
|
bootstrap = true,
|
|
3282
3643
|
canReachHost = true,
|
|
3283
3644
|
): Promise<{ body: string; rscPayload: string; clientChunks: unknown; usedDynamicApis: boolean; dynamicBecause: string[]; clientComponents: string[]; serverReferences: boolean }> {
|
|
3645
|
+
await instrumented()
|
|
3284
3646
|
applyHost()
|
|
3285
3647
|
|
|
3286
3648
|
// A build renders this with no host installed, so every rpc() has to suspend
|
|
@@ -3394,6 +3756,7 @@ export async function handleRscRevalidate(
|
|
|
3394
3756
|
target: string,
|
|
3395
3757
|
page: PageContext,
|
|
3396
3758
|
): Promise<{ rscPayload: string }> {
|
|
3759
|
+
await instrumented()
|
|
3397
3760
|
applyHost()
|
|
3398
3761
|
|
|
3399
3762
|
const flight = renderToReadableStream(await renderRevalidated(target, page))
|
|
@@ -3410,6 +3773,7 @@ export async function handleRscPayload(
|
|
|
3410
3773
|
from = 0,
|
|
3411
3774
|
pageKey = '',
|
|
3412
3775
|
): Promise<{ rscPayload: string }> {
|
|
3776
|
+
await instrumented()
|
|
3413
3777
|
applyHost()
|
|
3414
3778
|
|
|
3415
3779
|
const flight = renderToReadableStream(
|
|
@@ -3477,6 +3841,7 @@ export async function handleRscPprShell(
|
|
|
3477
3841
|
// and make every guarded route dynamic for the wrong reason.
|
|
3478
3842
|
let usedDynamicApis = false
|
|
3479
3843
|
|
|
3844
|
+
await instrumented()
|
|
3480
3845
|
applyHost()
|
|
3481
3846
|
|
|
3482
3847
|
const probe = (..._args: unknown[]) => {
|
|
@@ -3506,12 +3871,22 @@ export async function handleRscPprShell(
|
|
|
3506
3871
|
// page, stored, with the build reporting success.
|
|
3507
3872
|
let renderFailure: string | undefined
|
|
3508
3873
|
|
|
3509
|
-
const noteFailure = (e: unknown): string | undefined => {
|
|
3874
|
+
const noteFailure = (e: unknown, info?: { componentStack?: string }): string | undefined => {
|
|
3510
3875
|
const digest = redirectDigest(e)
|
|
3511
3876
|
|
|
3512
3877
|
// A redirect is a classification here, not a failure.
|
|
3513
3878
|
if (digest) return digest
|
|
3514
3879
|
|
|
3880
|
+
// A read the server could not answer, caught at a boundary: the fallback
|
|
3881
|
+
// there is what gets stored. Noted with its component so the build can
|
|
3882
|
+
// say so on the route's line; the digest goes into the document for the
|
|
3883
|
+
// browser to recognise.
|
|
3884
|
+
if ((e as { digest?: string } | null)?.digest === 'rsc-kit:search-params-fallback') {
|
|
3885
|
+
const where = /at ([A-Z][\\w$]*)/.exec(info?.componentStack ?? '')?.[1]
|
|
3886
|
+
noteCaughtRead('useSearchParams()' + (where ? ' in ' + where : ''))
|
|
3887
|
+
return 'rsc-kit:search-params-fallback'
|
|
3888
|
+
}
|
|
3889
|
+
|
|
3515
3890
|
// Every probe ends by aborting, so React reports that abort. Asked of the
|
|
3516
3891
|
// signal rather than matched against the message: a string test would be a
|
|
3517
3892
|
// guess about wording, and would quietly stop working when React changed it.
|
|
@@ -3624,6 +3999,29 @@ ${fallbackOrigin ? FALLBACK_CONSTS.replace("__ORIGIN__", JSON.stringify(fallback
|
|
|
3624
3999
|
let devHandler: ((request: Request) => Promise<Response | null>) | null = null
|
|
3625
4000
|
|
|
3626
4001
|
export default async function handler(request: Request): Promise<Response> {
|
|
4002
|
+
try {
|
|
4003
|
+
return await serve(request)
|
|
4004
|
+
} catch (error) {
|
|
4005
|
+
// Vite re-optimised a server pre-bundle under this render. The condition
|
|
4006
|
+
// is over already; the page is asked to load again.
|
|
4007
|
+
if (import.meta.env.DEV && isOutdatedOptimizedDep(error)) return outdatedDepResponse(request)
|
|
4008
|
+
|
|
4009
|
+
throw error
|
|
4010
|
+
}
|
|
4011
|
+
}
|
|
4012
|
+
|
|
4013
|
+
async function serve(request: Request): Promise<Response> {
|
|
4014
|
+
// The startup plugin's probe. Nitro loads this module at the first request
|
|
4015
|
+
// rather than when the server starts, so a plugin that runs at startup
|
|
4016
|
+
// sends one request to load it; the module's evaluation began register(),
|
|
4017
|
+
// and this waits for it so a failure is reported as the startup failure it
|
|
4018
|
+
// is. No page is rendered and nothing is disclosed.
|
|
4019
|
+
if (request.headers.get('x-rsc-kit-startup') !== null) {
|
|
4020
|
+
await instrumented()
|
|
4021
|
+
|
|
4022
|
+
return new Response(null, { status: 204 })
|
|
4023
|
+
}
|
|
4024
|
+
|
|
3627
4025
|
installHostCallsOnce()
|
|
3628
4026
|
|
|
3629
4027
|
devHandler ??= createRscHandler({
|
|
@@ -3647,7 +4045,7 @@ export default async function handler(request: Request): Promise<Response> {
|
|
|
3647
4045
|
${NITRO_HANDLER_OPTIONS}${NITRO_PRERENDERED} maxActionBody: ${maxActionBody === undefined ? "undefined" : String(maxActionBody)},
|
|
3648
4046
|
})
|
|
3649
4047
|
|
|
3650
|
-
${fallbackOrigin ? FALLBACK_BODY : " return (await devHandler(request)) ?? (await notFound())\n"}}
|
|
4048
|
+
${fallbackOrigin ? FALLBACK_BODY : " return (await devHandler(request)) ?? (await notFound(request))\n"}}
|
|
3651
4049
|
|
|
3652
4050
|
/**
|
|
3653
4051
|
* The page for a url nothing answers.
|
|
@@ -3658,9 +4056,42 @@ ${fallbackOrigin ? FALLBACK_BODY : " return (await devHandler(request)) ?? (awa
|
|
|
3658
4056
|
*
|
|
3659
4057
|
* Without a not-found.tsx this is the string it always was.
|
|
3660
4058
|
*/
|
|
3661
|
-
async function notFound(): Promise<Response> {
|
|
4059
|
+
async function notFound(request: Request): Promise<Response> {
|
|
3662
4060
|
${notFoundComponent
|
|
3663
4061
|
? `
|
|
4062
|
+
// A navigation, not a document: answer with the not-found tree as a
|
|
4063
|
+
// payload, at the depth the client already holds, so the router renders
|
|
4064
|
+
// it in place - the layout stays, the url changes, nothing reloads. The
|
|
4065
|
+
// status is still 404; a payload is a payload whatever it says.
|
|
4066
|
+
if (request.headers.get('X-RSC')) {
|
|
4067
|
+
try {
|
|
4068
|
+
const chain = ${JSON.stringify(notFoundLayouts)}
|
|
4069
|
+
const from = sharedDepth(request.headers.get('X-RSC-Segments'), chain)
|
|
4070
|
+
const { rscPayload } = await handleRscPayload(
|
|
4071
|
+
${JSON.stringify(notFoundComponent)},
|
|
4072
|
+
{},
|
|
4073
|
+
${JSON.stringify(notFoundLayouts.map((component) => ({ component, props: {} })))},
|
|
4074
|
+
[],
|
|
4075
|
+
{},
|
|
4076
|
+
from,
|
|
4077
|
+
'/404',
|
|
4078
|
+
)
|
|
4079
|
+
|
|
4080
|
+
return new Response(rscPayload, {
|
|
4081
|
+
status: 404,
|
|
4082
|
+
headers: {
|
|
4083
|
+
'Content-Type': 'text/x-component; charset=utf-8',
|
|
4084
|
+
'X-RSC-Segment-Depth': String(from),
|
|
4085
|
+
'X-RSC-Layouts': chain.join(','),
|
|
4086
|
+
'Cache-Control': 'no-store',
|
|
4087
|
+
Vary: 'X-RSC',
|
|
4088
|
+
},
|
|
4089
|
+
})
|
|
4090
|
+
} catch {
|
|
4091
|
+
// Fall through to the document answer below.
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
4094
|
+
|
|
3664
4095
|
try {
|
|
3665
4096
|
const { htmlStream } = await handleRscHtmlStream(
|
|
3666
4097
|
${JSON.stringify(notFoundComponent)},
|
|
@@ -3689,11 +4120,15 @@ async function notFound(): Promise<Response> {
|
|
|
3689
4120
|
}
|
|
3690
4121
|
function generateEntrySsr() {
|
|
3691
4122
|
const devUrls = join(packageDir, "devUrls");
|
|
4123
|
+
const request = join(packageDir, "request");
|
|
4124
|
+
const fallbackReport = join(packageDir, "js/fallbackReport");
|
|
3692
4125
|
return `// GENERATED by rscKit() — do not edit.
|
|
3693
4126
|
import { createFromReadableStream } from '@vitejs/plugin-rsc/ssr'
|
|
3694
4127
|
import { renderToReadableStream, resume } from 'react-dom/server.edge'
|
|
3695
4128
|
import { prerender } from 'react-dom/static.edge'
|
|
3696
4129
|
import { rewriteViteDevUrlStream } from ${JSON.stringify(devUrls)}
|
|
4130
|
+
import { noteFallback } from ${JSON.stringify(request)}
|
|
4131
|
+
import { cancelledByConsumer, caughtByLoading } from ${JSON.stringify(fallbackReport)}
|
|
3697
4132
|
|
|
3698
4133
|
// Set only by the dev server. @vitejs/plugin-rsc emits its bootstrap and CSS
|
|
3699
4134
|
// links root-relative in dev, which would send the browser to the host for
|
|
@@ -3724,12 +4159,28 @@ export async function handleSsr(
|
|
|
3724
4159
|
// The query-string fallback is the designed path for a stored page, and
|
|
3725
4160
|
// its digest is what lets the client tell it from a fault on hydration;
|
|
3726
4161
|
// returned here so React writes it into the document.
|
|
3727
|
-
onError: onError ?? ((error: unknown) => {
|
|
4162
|
+
onError: onError ?? ((error: unknown, info?: { componentStack?: string }) => {
|
|
4163
|
+
// The consumer cancelled - a browser that left mid-stream, a prefetch
|
|
4164
|
+
// abandoned. React reports it as an error; the page had none.
|
|
4165
|
+
if (cancelledByConsumer(error)) return
|
|
3728
4166
|
const digest = (error as { digest?: string } | null)?.digest
|
|
3729
4167
|
if (digest === 'rsc-kit:search-params-fallback') {
|
|
3730
|
-
//
|
|
3731
|
-
//
|
|
3732
|
-
|
|
4168
|
+
// The component is the first frame of React's stack. Noted on the
|
|
4169
|
+
// request so the build attaches it to the route; printed as one line,
|
|
4170
|
+
// not a stack, so the dev server says which boundary the build wants.
|
|
4171
|
+
const where = /at ([A-Z][\\w$]*)/.exec(info?.componentStack ?? '')?.[1]
|
|
4172
|
+
noteFallback('useSearchParams()' + (where ? ' in ' + where : ''))
|
|
4173
|
+
// Under a boundary the developer wrote, nothing to say. With nothing
|
|
4174
|
+
// closer than a loading.tsx, one line: the whole segment is the
|
|
4175
|
+
// fallback until the query arrives.
|
|
4176
|
+
if (caughtByLoading(info?.componentStack)) {
|
|
4177
|
+
console.error(
|
|
4178
|
+
'[rsc-kit] ' + (where ? where + ': ' : '') +
|
|
4179
|
+
'useSearchParams() was read on the server with nothing closer than a loading.tsx, so the whole ' +
|
|
4180
|
+
'segment shows that fallback until the query arrives. A <Suspense> around the component that reads ' +
|
|
4181
|
+
'keeps the rest of the page painted.',
|
|
4182
|
+
)
|
|
4183
|
+
}
|
|
3733
4184
|
return digest
|
|
3734
4185
|
}
|
|
3735
4186
|
console.error('[rsc-kit:ssr]', error)
|
|
@@ -3769,8 +4220,14 @@ export async function handleSsrPrerender(
|
|
|
3769
4220
|
bootstrapScriptContent,
|
|
3770
4221
|
nonce: options.nonce,
|
|
3771
4222
|
signal: options.signal,
|
|
3772
|
-
// Aborting is how this ends, so React's report of it is not news.
|
|
3773
|
-
|
|
4223
|
+
// Aborting is how this ends, so React's report of it is not news. A read
|
|
4224
|
+
// caught at a boundary is: the build attaches it to the route.
|
|
4225
|
+
onError: (error: unknown, info?: { componentStack?: string }) => {
|
|
4226
|
+
if ((error as { digest?: string } | null)?.digest !== 'rsc-kit:search-params-fallback') return
|
|
4227
|
+
const where = /at ([A-Z][\\w$]*)/.exec(info?.componentStack ?? '')?.[1]
|
|
4228
|
+
noteFallback('useSearchParams()' + (where ? ' in ' + where : ''))
|
|
4229
|
+
return 'rsc-kit:search-params-fallback'
|
|
4230
|
+
},
|
|
3774
4231
|
})
|
|
3775
4232
|
|
|
3776
4233
|
return {
|
|
@@ -4062,16 +4519,307 @@ function extendableClientReferences() {
|
|
|
4062
4519
|
},
|
|
4063
4520
|
};
|
|
4064
4521
|
}
|
|
4522
|
+
/**
|
|
4523
|
+
* The worker the dev server serves at /sw.js: it removes the one a production
|
|
4524
|
+
* run left on this origin, and the caches with it, then reloads each page it
|
|
4525
|
+
* was controlling so they load uncontrolled. Nothing else runs in development.
|
|
4526
|
+
*/
|
|
4527
|
+
export const DEV_WORKER = `self.addEventListener('install', () => self.skipWaiting())
|
|
4528
|
+
self.addEventListener('activate', (event) => {
|
|
4529
|
+
event.waitUntil((async () => {
|
|
4530
|
+
const keys = await caches.keys()
|
|
4531
|
+
await Promise.all(keys.filter((k) => k.startsWith('rsc-kit-')).map((k) => caches.delete(k)))
|
|
4532
|
+
await self.clients.claim()
|
|
4533
|
+
const pages = await self.clients.matchAll({ type: 'window' })
|
|
4534
|
+
await self.registration.unregister()
|
|
4535
|
+
for (const page of pages) page.navigate(page.url).catch(() => {})
|
|
4536
|
+
})())
|
|
4537
|
+
})
|
|
4538
|
+
`;
|
|
4539
|
+
/**
|
|
4540
|
+
* Named imports from a barrel package, unrolled on the server environments.
|
|
4541
|
+
*
|
|
4542
|
+
* The browser gets lucide pre-bundled; the server environments do not, so
|
|
4543
|
+
* `import { ArrowRight } from 'lucide-react'` loaded and transformed every
|
|
4544
|
+
* icon module - 3,700 transforms, twenty-five seconds before the first
|
|
4545
|
+
* document, for thirteen icons. See barrelImports. Development only: a
|
|
4546
|
+
* build tree-shakes the barrel itself.
|
|
4547
|
+
*/
|
|
4548
|
+
function barrelImportsPlugin() {
|
|
4549
|
+
const entries = new Map();
|
|
4550
|
+
return {
|
|
4551
|
+
name: "rsc-kit:barrel-imports",
|
|
4552
|
+
apply: () => barrelImports && process.env.NODE_ENV !== "production",
|
|
4553
|
+
enforce: "pre",
|
|
4554
|
+
applyToEnvironment: (environment) => environment.name === "rsc" || environment.name === "ssr",
|
|
4555
|
+
async transform(code, id) {
|
|
4556
|
+
if (id.includes("/node_modules/") ||
|
|
4557
|
+
!/\.[cm]?[jt]sx?$/.test(id.split("?")[0]))
|
|
4558
|
+
return;
|
|
4559
|
+
if (!code.includes("from"))
|
|
4560
|
+
return;
|
|
4561
|
+
const pending = [];
|
|
4562
|
+
const wanted = new Set();
|
|
4563
|
+
for (const m of code.matchAll(/import\s*(?:type\s+)?\{[^}]*\}\s*from\s*['"]([^'"./][^'"]*)['"]/g)) {
|
|
4564
|
+
wanted.add(m[1]);
|
|
4565
|
+
}
|
|
4566
|
+
for (const specifier of wanted) {
|
|
4567
|
+
if (entries.has(specifier))
|
|
4568
|
+
continue;
|
|
4569
|
+
pending.push(this.resolve(specifier, id).then((resolved) => {
|
|
4570
|
+
entries.set(specifier, resolved && !resolved.external ? resolved.id.split("?")[0] : null);
|
|
4571
|
+
}));
|
|
4572
|
+
}
|
|
4573
|
+
await Promise.all(pending);
|
|
4574
|
+
const out = unrollBarrelImports(code, (specifier) => entries.get(specifier) ?? null);
|
|
4575
|
+
return out === null ? undefined : { code: out, map: null };
|
|
4576
|
+
},
|
|
4577
|
+
};
|
|
4578
|
+
}
|
|
4579
|
+
/**
|
|
4580
|
+
* "use ssr" modules, rewritten for the server-components environment into
|
|
4581
|
+
* proxies that call the real module in the ssr environment. See useSsr.ts.
|
|
4582
|
+
*
|
|
4583
|
+
* Before every other transform, so the cross-environment import this emits
|
|
4584
|
+
* is still ahead of plugin-rsc's handling of it - and so the module's own
|
|
4585
|
+
* imports, react-dom/server among them, are never resolved here at all.
|
|
4586
|
+
*/
|
|
4587
|
+
function useSsrModules() {
|
|
4588
|
+
return {
|
|
4589
|
+
name: "rsc-kit:use-ssr",
|
|
4590
|
+
enforce: "pre",
|
|
4591
|
+
applyToEnvironment: (environment) => environment.name === "rsc",
|
|
4592
|
+
transform(code, id) {
|
|
4593
|
+
if (!code.includes("use ssr"))
|
|
4594
|
+
return;
|
|
4595
|
+
try {
|
|
4596
|
+
const proxy = ssrProxyModule(code, id);
|
|
4597
|
+
return proxy === null ? undefined : { code: proxy, map: null };
|
|
4598
|
+
}
|
|
4599
|
+
catch (error) {
|
|
4600
|
+
if (error instanceof UseSsrError)
|
|
4601
|
+
this.error(error.message);
|
|
4602
|
+
throw error;
|
|
4603
|
+
}
|
|
4604
|
+
},
|
|
4605
|
+
};
|
|
4606
|
+
}
|
|
4607
|
+
/**
|
|
4608
|
+
* react-dom/server, imported where server components render, becomes a
|
|
4609
|
+
* module that throws the fix instead of React's refusal - naming the app
|
|
4610
|
+
* file that imported it (through @react-email/render, say) and the
|
|
4611
|
+
* directive that moves it. The build says the same once, as a warning.
|
|
4612
|
+
*/
|
|
4613
|
+
const RENDERER_STUB = "\0rsc-kit:react-dom-server?from=";
|
|
4614
|
+
function serverRendererInRsc() {
|
|
4615
|
+
const warned = new Set();
|
|
4616
|
+
const appImporter = (ctx, importer) => {
|
|
4617
|
+
let current = importer;
|
|
4618
|
+
for (let hop = 0; current && hop < 12; hop++) {
|
|
4619
|
+
if (!current.includes("/node_modules/"))
|
|
4620
|
+
return relative(process.cwd(), current.split("?")[0]);
|
|
4621
|
+
const next = ctx.environment.mode === "build"
|
|
4622
|
+
? ctx.getModuleInfo(current)?.importers[0]
|
|
4623
|
+
: (ctx.environment.moduleGraph
|
|
4624
|
+
?.getModuleById(current)
|
|
4625
|
+
?.importers.values()
|
|
4626
|
+
.next().value?.id ?? undefined);
|
|
4627
|
+
if (!next)
|
|
4628
|
+
break;
|
|
4629
|
+
current = next;
|
|
4630
|
+
}
|
|
4631
|
+
return importer ? relative(process.cwd(), importer.split("?")[0]) : null;
|
|
4632
|
+
};
|
|
4633
|
+
return {
|
|
4634
|
+
name: "rsc-kit:server-renderer",
|
|
4635
|
+
applyToEnvironment: (environment) => environment.name === "rsc",
|
|
4636
|
+
resolveId(source, importer) {
|
|
4637
|
+
if (!SERVER_RENDERER.test(source))
|
|
4638
|
+
return;
|
|
4639
|
+
return RENDERER_STUB + encodeURIComponent(importer ?? "");
|
|
4640
|
+
},
|
|
4641
|
+
load(id) {
|
|
4642
|
+
if (!id.startsWith(RENDERER_STUB))
|
|
4643
|
+
return;
|
|
4644
|
+
const importer = decodeURIComponent(id.slice(RENDERER_STUB.length)) || null;
|
|
4645
|
+
const message = serverRendererMessage(appImporter(this, importer));
|
|
4646
|
+
if (this.environment.mode === "build" && !warned.has(message)) {
|
|
4647
|
+
warned.add(message);
|
|
4648
|
+
this.warn(message);
|
|
4649
|
+
}
|
|
4650
|
+
return `throw new Error(${JSON.stringify(message)});\n`;
|
|
4651
|
+
},
|
|
4652
|
+
};
|
|
4653
|
+
}
|
|
4654
|
+
/**
|
|
4655
|
+
* `tsc --noEmit` on the project's own tsconfig, the way `check` runs it.
|
|
4656
|
+
*
|
|
4657
|
+
* Through package.json rather than a subpath: TypeScript's exports map does
|
|
4658
|
+
* not expose bin/tsc, and the bin field is where the name lives. Its output
|
|
4659
|
+
* is the message, because that is what the developer would have read.
|
|
4660
|
+
*/
|
|
4661
|
+
export function typecheckProject(root) {
|
|
4662
|
+
const tsconfig = join(root, "tsconfig.json");
|
|
4663
|
+
if (!existsSync(tsconfig))
|
|
4664
|
+
return { ran: false, because: "no tsconfig" };
|
|
4665
|
+
let tsc;
|
|
4666
|
+
try {
|
|
4667
|
+
const fromApp = createRequire(join(root, "package.json"));
|
|
4668
|
+
const manifest = fromApp.resolve("typescript/package.json");
|
|
4669
|
+
const bin = fromApp(manifest)
|
|
4670
|
+
.bin;
|
|
4671
|
+
const relative = typeof bin === "string" ? bin : bin?.tsc;
|
|
4672
|
+
if (!relative)
|
|
4673
|
+
return { ran: false, because: "no typescript" };
|
|
4674
|
+
tsc = join(dirname(manifest), relative);
|
|
4675
|
+
}
|
|
4676
|
+
catch {
|
|
4677
|
+
return { ran: false, because: "no typescript" };
|
|
4678
|
+
}
|
|
4679
|
+
const started = Date.now();
|
|
4680
|
+
const script = /\.[cm]?js$/.test(tsc) || !/\.\w+$/.test(tsc);
|
|
4681
|
+
const result = spawnSync(script ? process.execPath : tsc,
|
|
4682
|
+
// Plain lines, not coloured ones: they go into a build error, and the
|
|
4683
|
+
// test that reads them is a reader too.
|
|
4684
|
+
[...(script ? [tsc] : []), "--noEmit", "--pretty", "false", "-p", tsconfig], { cwd: root, encoding: "utf-8", maxBuffer: 64 * 1024 * 1024 });
|
|
4685
|
+
if (result.status === 0)
|
|
4686
|
+
return { ran: true, ok: true, seconds: (Date.now() - started) / 1000 };
|
|
4687
|
+
return {
|
|
4688
|
+
ran: true,
|
|
4689
|
+
ok: false,
|
|
4690
|
+
output: `${result.stdout ?? ""}${result.stderr ?? ""}`.trim(),
|
|
4691
|
+
};
|
|
4692
|
+
}
|
|
4693
|
+
/**
|
|
4694
|
+
* The project's typecheck, as part of the build.
|
|
4695
|
+
*
|
|
4696
|
+
* Runs once, in the first environment to start, after the route types have
|
|
4697
|
+
* been written by the config hook - so a link to a route that does not exist
|
|
4698
|
+
* is an error here and not a 404 found after deploying. Skipped where the
|
|
4699
|
+
* project is not doing this checking at all: no tsconfig.json, or no
|
|
4700
|
+
* typescript to run.
|
|
4701
|
+
*/
|
|
4702
|
+
function typecheckPlugin() {
|
|
4703
|
+
let ran = false;
|
|
4704
|
+
return {
|
|
4705
|
+
name: "rsc-kit:typecheck",
|
|
4706
|
+
apply: "build",
|
|
4707
|
+
buildStart() {
|
|
4708
|
+
if (ran || !typecheck || isWatch)
|
|
4709
|
+
return;
|
|
4710
|
+
ran = true;
|
|
4711
|
+
const outcome = typecheckProject(projectRoot);
|
|
4712
|
+
if (!outcome.ran)
|
|
4713
|
+
return;
|
|
4714
|
+
if (outcome.ok) {
|
|
4715
|
+
log(`typecheck: ok (${outcome.seconds.toFixed(1)}s)`);
|
|
4716
|
+
return;
|
|
4717
|
+
}
|
|
4718
|
+
throw new Error("[rsc-kit] The typecheck failed, so the build stops here.\n\n" +
|
|
4719
|
+
outcome.output +
|
|
4720
|
+
"\n\nrscKit({ typecheck: false }) builds without it.");
|
|
4721
|
+
},
|
|
4722
|
+
};
|
|
4723
|
+
}
|
|
4724
|
+
/**
|
|
4725
|
+
* Which server files import a client library, read off the rsc environment's
|
|
4726
|
+
* module graph once it is built. plugin-rsc classifies the client packages
|
|
4727
|
+
* (every package with react among its peers) and excludes them from the
|
|
4728
|
+
* server optimizers; that list is the one used here, minus this package -
|
|
4729
|
+
* a server component importing Link is the norm - and plugin-rsc's own.
|
|
4730
|
+
*/
|
|
4731
|
+
function clientImportsAudit() {
|
|
4732
|
+
return {
|
|
4733
|
+
name: "rsc-kit:client-imports",
|
|
4734
|
+
apply: "build",
|
|
4735
|
+
applyToEnvironment: (environment) => environment.name === "rsc",
|
|
4736
|
+
buildEnd() {
|
|
4737
|
+
const excluded = resolvedConfig?.environments?.rsc?.optimizeDeps?.exclude ?? [];
|
|
4738
|
+
const clientPackages = excluded.filter((name) => !name.startsWith("@vitejs/plugin-rsc"));
|
|
4739
|
+
if (clientPackages.length === 0)
|
|
4740
|
+
return;
|
|
4741
|
+
clientLibraryImports = serverImportsOfClientPackages({
|
|
4742
|
+
moduleIds: () => this.getModuleIds(),
|
|
4743
|
+
importedIds: (id) => this.getModuleInfo(id)?.importedIds ?? [],
|
|
4744
|
+
importers: (id) => this.getModuleInfo(id)?.importers ?? [],
|
|
4745
|
+
}, {
|
|
4746
|
+
sourceDir,
|
|
4747
|
+
clientPackages,
|
|
4748
|
+
ignore: [PACKAGE_NAME, "server-only", "client-only"],
|
|
4749
|
+
});
|
|
4750
|
+
},
|
|
4751
|
+
};
|
|
4752
|
+
}
|
|
4753
|
+
/**
|
|
4754
|
+
* A Nitro runtime plugin, so instrumentation.ts runs when the server starts.
|
|
4755
|
+
*
|
|
4756
|
+
* Nitro loads the rsc service at the first request, not at startup - so
|
|
4757
|
+
* without this, register() would run when the first visitor arrived, and a
|
|
4758
|
+
* bad environment would take down a server that had already reported itself
|
|
4759
|
+
* up. The plugin runs at app init and sends the service one request, which
|
|
4760
|
+
* loads the module; loading it begins register(), and the request waits for
|
|
4761
|
+
* it. Not on a Worker: there is no startup, and the first request is the
|
|
4762
|
+
* first chance to read a binding.
|
|
4763
|
+
*/
|
|
4764
|
+
const STARTUP_PLUGIN = `import { viteServices } from "#nitro/virtual/vite-services"
|
|
4765
|
+
|
|
4766
|
+
export default function rscKitStartup() {
|
|
4767
|
+
if (typeof navigator !== 'undefined' && navigator.userAgent === 'Cloudflare-Workers') return
|
|
4768
|
+
|
|
4769
|
+
// The ssr service is the one Nitro's renderer asks; its fetch imports the
|
|
4770
|
+
// rsc entry and hands the request on. The rsc service's own export is the
|
|
4771
|
+
// handler function, not a { fetch }, so it cannot be probed directly.
|
|
4772
|
+
const ssr = viteServices.ssr
|
|
4773
|
+
if (!ssr) return
|
|
4774
|
+
|
|
4775
|
+
// Two ways to fail, one outcome. register() rejecting is reported by the
|
|
4776
|
+
// module itself. instrumentation.ts throwing at import - an env schema
|
|
4777
|
+
// refusing - means the module never evaluated, so nothing in it can
|
|
4778
|
+
// report; the load rejects here instead. Either way a server that could
|
|
4779
|
+
// not bootstrap has nothing correct to serve, and says so and stops
|
|
4780
|
+
// rather than reporting itself up.
|
|
4781
|
+
ssr
|
|
4782
|
+
.fetch(new Request('http://rsc-kit.internal/_rsc/startup', { headers: { 'x-rsc-kit-startup': '1' } }))
|
|
4783
|
+
.then((response) => {
|
|
4784
|
+
if (!response.ok) throw new Error('the startup probe answered ' + response.status)
|
|
4785
|
+
})
|
|
4786
|
+
.catch((error) => {
|
|
4787
|
+
console.error('[rsc-kit] instrumentation.ts failed at startup:', error)
|
|
4788
|
+
|
|
4789
|
+
if (typeof process !== 'undefined' && typeof process.exit === 'function') process.exit(1)
|
|
4790
|
+
})
|
|
4791
|
+
}
|
|
4792
|
+
`;
|
|
4065
4793
|
export function rscKit(options = {}) {
|
|
4066
4794
|
resolvePaths(options);
|
|
4067
4795
|
const routesPlugin = {
|
|
4068
4796
|
name: "rsc-kit",
|
|
4797
|
+
// A Nitro module, which Nitro's Vite plugin collects from any plugin
|
|
4798
|
+
// that carries one. Only for a built server: the dev server evaluates
|
|
4799
|
+
// the entry through Vite's runner when it is first asked for, and there
|
|
4800
|
+
// is no earlier moment to offer.
|
|
4801
|
+
nitro: {
|
|
4802
|
+
name: "rsc-kit",
|
|
4803
|
+
setup(nitro) {
|
|
4804
|
+
if (nitro.options.dev || !instrumentationFile())
|
|
4805
|
+
return;
|
|
4806
|
+
nitro.options.virtual ??= {};
|
|
4807
|
+
nitro.options.virtual["#rsc-kit/startup"] = STARTUP_PLUGIN;
|
|
4808
|
+
nitro.options.plugins = [...(nitro.options.plugins ?? []), "#rsc-kit/startup"];
|
|
4809
|
+
},
|
|
4810
|
+
},
|
|
4069
4811
|
config(_config, env) {
|
|
4070
4812
|
if (!existsSync(appDir)) {
|
|
4071
4813
|
throw new Error(`[rsc-kit] No app directory at ${appDir} — nothing to build.`);
|
|
4072
4814
|
}
|
|
4815
|
+
// Both maps, not one: the config hook runs again when the config
|
|
4816
|
+
// changes under a dev server, and a route.ts or a synthesised
|
|
4817
|
+
// robots.txt left over from the previous run read as a duplicate.
|
|
4073
4818
|
components.clear();
|
|
4819
|
+
apiRoutes.clear();
|
|
4074
4820
|
discover(appDir);
|
|
4821
|
+
registerMetadataRoutes(appDir);
|
|
4822
|
+
siteHosts = ownHosts(rootMetadataBase(appDir), hostsOption);
|
|
4075
4823
|
// Silent when it worked. The names were printed on every dev start and
|
|
4076
4824
|
// every build — thirty of them for a middling app, above the output that
|
|
4077
4825
|
// actually says something, and the build's classification table lists
|
|
@@ -4160,9 +4908,6 @@ export function rscKit(options = {}) {
|
|
|
4160
4908
|
*/
|
|
4161
4909
|
define: {
|
|
4162
4910
|
"process.env.NODE_ENV": JSON.stringify(env.mode === "development" ? "development" : "production"),
|
|
4163
|
-
// A constant, so the boundary and its import fall out of the bundle
|
|
4164
|
-
// entirely when this is off rather than shipping a branch nobody takes.
|
|
4165
|
-
__RSC_VIEW_TRANSITIONS__: JSON.stringify(viewTransitions),
|
|
4166
4911
|
__RSC_OFFLINE__: JSON.stringify(offline),
|
|
4167
4912
|
},
|
|
4168
4913
|
/*
|
|
@@ -4187,6 +4932,36 @@ export function rscKit(options = {}) {
|
|
|
4187
4932
|
*/
|
|
4188
4933
|
optimizeDeps: {
|
|
4189
4934
|
exclude: [PACKAGE_NAME, ...(_config.optimizeDeps?.exclude ?? [])],
|
|
4935
|
+
// Every "use client" file, so the browser's dependencies are all
|
|
4936
|
+
// found at startup. The client entry reaches only the engine and
|
|
4937
|
+
// React; the app's client components arrive through payloads, page
|
|
4938
|
+
// by page, and a dependency first seen on the third page visited
|
|
4939
|
+
// re-optimised every pre-bundle under a running page - two Reacts,
|
|
4940
|
+
// a blank document, then Vite's own reload. See clientEntries.
|
|
4941
|
+
// Naming entries replaces the input, so the browser entry is named
|
|
4942
|
+
// too, or the engine and React would be the ones left out.
|
|
4943
|
+
entries: [
|
|
4944
|
+
...(Array.isArray(_config.optimizeDeps?.entries)
|
|
4945
|
+
? _config.optimizeDeps.entries
|
|
4946
|
+
: _config.optimizeDeps?.entries
|
|
4947
|
+
? [_config.optimizeDeps.entries]
|
|
4948
|
+
: []),
|
|
4949
|
+
// Entries are globs: a route group's parentheses, or a bracket
|
|
4950
|
+
// in a dynamic segment, would otherwise read as pattern syntax
|
|
4951
|
+
// and match nothing - and the scanner would crawl nothing, quietly.
|
|
4952
|
+
...[
|
|
4953
|
+
join(genDir, "entry.browser.tsx"),
|
|
4954
|
+
...engineClientEntries(),
|
|
4955
|
+
...clientEntries(sourceDir),
|
|
4956
|
+
].map((file) => file.replace(/[()[\]{}*?!+@]/g, "\\$&")),
|
|
4957
|
+
],
|
|
4958
|
+
rolldownOptions: {
|
|
4959
|
+
...(_config.optimizeDeps?.rolldownOptions ?? {}),
|
|
4960
|
+
plugins: [
|
|
4961
|
+
clientScanPlugin(),
|
|
4962
|
+
...arrayOf(_config.optimizeDeps?.rolldownOptions?.plugins),
|
|
4963
|
+
],
|
|
4964
|
+
},
|
|
4190
4965
|
},
|
|
4191
4966
|
// Public URL for browser-facing client assets, and a BUILD concern
|
|
4192
4967
|
// only: it says where the built files will be served from.
|
|
@@ -4279,6 +5054,28 @@ export function rscKit(options = {}) {
|
|
|
4279
5054
|
// and the console a 404 for a file that is right there.
|
|
4280
5055
|
server.middlewares.use((req, res, next) => {
|
|
4281
5056
|
const url = (req.url ?? "").split("?")[0];
|
|
5057
|
+
// A worker registered by a production run on this origin - `bun run
|
|
5058
|
+
// start` on the port the dev server uses next - outlives that run and
|
|
5059
|
+
// answers the dev server's documents from its cache: a stored page is
|
|
5060
|
+
// cache-first, so a refresh brings back yesterday's document with
|
|
5061
|
+
// yesterday's module hashes, and a 504 for each. The browser fetches
|
|
5062
|
+
// /sw.js again on every navigation to look for an update; in
|
|
5063
|
+
// development that fetch gets a worker whose only job is to remove
|
|
5064
|
+
// itself, its caches, and reload the pages it controlled.
|
|
5065
|
+
// robots.txt, a hand-written sitemap.xml, humans.txt: beside the root
|
|
5066
|
+
// layout, served at the root. The build copies them beside the client
|
|
5067
|
+
// output; here they are read from app/.
|
|
5068
|
+
if (url.startsWith("/") && rootFiles.includes(url.slice(1))) {
|
|
5069
|
+
res.setHeader("Content-Type", rootFileType(url.slice(1)));
|
|
5070
|
+
res.end(readFileSync(join(sourceDir, "app", url.slice(1))));
|
|
5071
|
+
return;
|
|
5072
|
+
}
|
|
5073
|
+
if (url === "/sw.js") {
|
|
5074
|
+
res.setHeader("Content-Type", "text/javascript");
|
|
5075
|
+
res.setHeader("Cache-Control", "no-store");
|
|
5076
|
+
res.end(DEV_WORKER);
|
|
5077
|
+
return;
|
|
5078
|
+
}
|
|
4282
5079
|
if (webManifestOptions && url === MANIFEST_PATH) {
|
|
4283
5080
|
res.setHeader("Content-Type", "application/manifest+json");
|
|
4284
5081
|
res.end(webManifest(webManifestOptions));
|
|
@@ -4506,7 +5303,13 @@ export function rscKit(options = {}) {
|
|
|
4506
5303
|
clientChunks: (meta) => meta.normalizedId,
|
|
4507
5304
|
...actionEncryptionKey(),
|
|
4508
5305
|
}),
|
|
5306
|
+
barrelImportsPlugin(),
|
|
5307
|
+
useSsrModules(),
|
|
5308
|
+
serverRendererInRsc(),
|
|
5309
|
+
metadataRoutesPlugin(),
|
|
4509
5310
|
extendableClientReferences(),
|
|
5311
|
+
typecheckPlugin(),
|
|
5312
|
+
clientImportsAudit(),
|
|
4510
5313
|
routesPlugin,
|
|
4511
5314
|
];
|
|
4512
5315
|
}
|