@rsc-kit/core 0.17.0 → 0.18.1
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/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/LoadingBoundary.d.ts +5 -0
- package/dist/js/LoadingBoundary.js +19 -0
- package/dist/js/LoadingBoundary.js.map +1 -0
- package/dist/js/fallbackReport.js +10 -8
- package/dist/js/fallbackReport.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/useEvents.d.ts +25 -0
- package/dist/js/useEvents.js +80 -0
- package/dist/js/useEvents.js.map +1 -0
- package/dist/js/usePolling.d.ts +29 -0
- package/dist/js/usePolling.js +142 -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/metadataRoutes.d.ts +20 -0
- package/dist/metadataRoutes.js +35 -0
- package/dist/metadataRoutes.js.map +1 -1
- package/dist/prerender.js +2 -0
- package/dist/prerender.js.map +1 -1
- package/dist/redirect.d.ts +16 -2
- package/dist/redirect.js +10 -19
- package/dist/redirect.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/routeSchema.d.ts +49 -0
- package/dist/routeSchema.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/testing.d.ts +8 -0
- package/dist/testing.js +27 -3
- package/dist/testing.js.map +1 -1
- package/dist/vite.d.ts +53 -2
- package/dist/vite.js +503 -38
- package/dist/vite.js.map +1 -1
- package/package.json +15 -3
package/dist/vite.js
CHANGED
|
@@ -27,7 +27,9 @@ import { ASSET_BASE, allAppAssets, appAssets, headTags, typeOf, } from "./appAss
|
|
|
27
27
|
import { reactCacheImports } from "./reactCache.js";
|
|
28
28
|
import { clientEntries, clientScanPlugin, engineClientEntries, } from "./clientEntries.js";
|
|
29
29
|
import { serverImportsOfClientPackages } from "./clientImports.js";
|
|
30
|
-
import { METADATA_ROUTES, ROOT_FILES, rootFileType } from "./metadataRoutes.js";
|
|
30
|
+
import { automaticSitemap, METADATA_ROUTES, ROOT_FILES, rootFileType, } from "./metadataRoutes.js";
|
|
31
|
+
import { ownHosts } from "./hostRouting.js";
|
|
32
|
+
import { unrollBarrelImports } from "./barrelImports.js";
|
|
31
33
|
import { serverRendererMessage, SERVER_RENDERER, ssrProxyModule, UseSsrError, } from "./useSsr.js";
|
|
32
34
|
import { httpHostCalls } from "./hostCalls.js";
|
|
33
35
|
// Resolved once per rscKit() call. One build runs in one process, so these are
|
|
@@ -40,6 +42,47 @@ let resolvedConfig = null;
|
|
|
40
42
|
let clientLibraryImports = [];
|
|
41
43
|
/** Modules a runtime provides and no bundle should try to carry. */
|
|
42
44
|
const RUNTIME_BUILTINS = ["bun", /^bun:/];
|
|
45
|
+
/**
|
|
46
|
+
* Packages the server bundles import rather than inline, by default.
|
|
47
|
+
*
|
|
48
|
+
* Each of these ships a native binary, spawns one, or reads files relative to
|
|
49
|
+
* its own location - none of which survives being rolled into a bundle. Left
|
|
50
|
+
* external, Nitro traces them into .output/server/node_modules with their
|
|
51
|
+
* binaries, and the built server imports them the way the package expects.
|
|
52
|
+
* Next keeps the same list under serverExternalPackages, for the same reason.
|
|
53
|
+
* `rscKit({ serverExternalPackages })` adds to it.
|
|
54
|
+
*/
|
|
55
|
+
const DEFAULT_SERVER_EXTERNALS = [
|
|
56
|
+
"sharp",
|
|
57
|
+
"canvas",
|
|
58
|
+
"bcrypt",
|
|
59
|
+
"argon2",
|
|
60
|
+
"@node-rs/argon2",
|
|
61
|
+
"@node-rs/bcrypt",
|
|
62
|
+
"better-sqlite3",
|
|
63
|
+
"sqlite3",
|
|
64
|
+
"libsql",
|
|
65
|
+
"@libsql/client",
|
|
66
|
+
"@prisma/client",
|
|
67
|
+
"prisma",
|
|
68
|
+
"puppeteer",
|
|
69
|
+
"puppeteer-core",
|
|
70
|
+
"playwright",
|
|
71
|
+
"playwright-core",
|
|
72
|
+
"jsdom",
|
|
73
|
+
"node-pty",
|
|
74
|
+
"onnxruntime-node",
|
|
75
|
+
"@sentry/profiling-node",
|
|
76
|
+
"pdfkit",
|
|
77
|
+
"mongodb",
|
|
78
|
+
"oslo",
|
|
79
|
+
"@resvg/resvg-js",
|
|
80
|
+
"@napi-rs/canvas",
|
|
81
|
+
];
|
|
82
|
+
/** A package name as a rollup external: the package and every subpath of it. */
|
|
83
|
+
function externalPackage(name) {
|
|
84
|
+
return new RegExp("^" + name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "(?:/|$)");
|
|
85
|
+
}
|
|
43
86
|
function arrayOf(value) {
|
|
44
87
|
return value == null ? [] : Array.isArray(value) ? value : [value];
|
|
45
88
|
}
|
|
@@ -108,6 +151,11 @@ let isWatch = false;
|
|
|
108
151
|
let offline = false;
|
|
109
152
|
let typecheck = true;
|
|
110
153
|
let webManifestOptions = null;
|
|
154
|
+
/** The site's own hosts, from the root layout's metadataBase and rscKit({ hosts }). */
|
|
155
|
+
let siteHosts = [];
|
|
156
|
+
let hostsOption = [];
|
|
157
|
+
let barrelImports = true;
|
|
158
|
+
let identify = true;
|
|
111
159
|
let foundAssets = {
|
|
112
160
|
favicon: null,
|
|
113
161
|
icons: [],
|
|
@@ -270,6 +318,9 @@ function resolvePaths(options) {
|
|
|
270
318
|
prerenderAfterBuild = process.env.RSC_PRERENDER !== "0";
|
|
271
319
|
offline = options.offline === true;
|
|
272
320
|
typecheck = options.typecheck !== false;
|
|
321
|
+
hostsOption = options.hosts ?? [];
|
|
322
|
+
barrelImports = options.barrelImports !== false;
|
|
323
|
+
identify = options.identify !== false;
|
|
273
324
|
inlineStylesheets = options.inlineStylesheets ?? "auto";
|
|
274
325
|
maxActionBody = options.maxActionBody;
|
|
275
326
|
// One place, and it is the file. A plugin option as well would be the same
|
|
@@ -323,7 +374,12 @@ function urlSegments(componentName) {
|
|
|
323
374
|
continue;
|
|
324
375
|
}
|
|
325
376
|
if (part.startsWith("[") && part.endsWith("]")) {
|
|
326
|
-
|
|
377
|
+
// At the top of app/ - the first segment the url has - a parameter is
|
|
378
|
+
// the host's: bound from acme.example.com, never from example.com/acme.
|
|
379
|
+
segments.push({
|
|
380
|
+
type: segments.length === 0 ? "host" : "param",
|
|
381
|
+
value: part.slice(1, -1),
|
|
382
|
+
});
|
|
327
383
|
continue;
|
|
328
384
|
}
|
|
329
385
|
// An interception marker says which url this replaces, not what it is
|
|
@@ -395,7 +451,11 @@ function routeManifest() {
|
|
|
395
451
|
* to whoever knows what they mean.
|
|
396
452
|
*/
|
|
397
453
|
const middlewareIn = (absDir) => {
|
|
398
|
-
|
|
454
|
+
// middleware.ts first: it is the file named for what this is. A guard
|
|
455
|
+
// the engine runs is its default export; the names a host runs are its
|
|
456
|
+
// `middleware` export, and a file may carry either or both. route.ts is
|
|
457
|
+
// read too, for the apps written before middleware.ts could.
|
|
458
|
+
for (const file of ["middleware.ts", "middleware.tsx", "route.ts", "route.tsx"]) {
|
|
399
459
|
const path = join(absDir, file);
|
|
400
460
|
if (!existsSync(path))
|
|
401
461
|
continue;
|
|
@@ -481,7 +541,13 @@ function routeManifest() {
|
|
|
481
541
|
// writing the site out, and knowing which filename the client will ask for.
|
|
482
542
|
return {
|
|
483
543
|
version: 1,
|
|
484
|
-
build: {
|
|
544
|
+
build: {
|
|
545
|
+
output,
|
|
546
|
+
exportPath,
|
|
547
|
+
payloadName: staticPayloads,
|
|
548
|
+
hosts: siteHosts,
|
|
549
|
+
identify,
|
|
550
|
+
},
|
|
485
551
|
routes,
|
|
486
552
|
intercepts,
|
|
487
553
|
apis: [...apiRoutes.values()].map(({ name, methods, generated }) => ({
|
|
@@ -1236,6 +1302,41 @@ async function prerenderAfterBundles(bundle, staticDir, assetsDir, knownActions
|
|
|
1236
1302
|
// places to look for one fact.
|
|
1237
1303
|
const manifest = engine.manifest();
|
|
1238
1304
|
const apis = await prerenderApiRoutes(engine, manifest, writeTo(staticDir));
|
|
1305
|
+
// A sitemap the build writes itself, when the app wrote none: every url
|
|
1306
|
+
// it stored or was told about, minus the guarded ones. It needs the site's
|
|
1307
|
+
// host, which the root layout's metadataBase is for; without one there is
|
|
1308
|
+
// nothing to write and the report says so once.
|
|
1309
|
+
const writesSitemap = manifest.apis?.some((api) => api.name === "app/sitemap.xml/route");
|
|
1310
|
+
const hasSitemapFile = rootFiles.includes("sitemap.xml");
|
|
1311
|
+
if (!writesSitemap && !hasSitemapFile) {
|
|
1312
|
+
const base = rootMetadataBase(join(sourceDir, "app"));
|
|
1313
|
+
if (base) {
|
|
1314
|
+
const xml = automaticSitemap(results, manifest.routes, base);
|
|
1315
|
+
const stored = JSON.stringify({
|
|
1316
|
+
status: 200,
|
|
1317
|
+
headers: [["content-type", "application/xml; charset=utf-8"]],
|
|
1318
|
+
body: xml,
|
|
1319
|
+
varies: false,
|
|
1320
|
+
});
|
|
1321
|
+
await writeTo(staticDir)("sitemap.xml.api.json", stored);
|
|
1322
|
+
pending.push({
|
|
1323
|
+
line: " ○ /sitemap.xml",
|
|
1324
|
+
bytes: null,
|
|
1325
|
+
extra: [
|
|
1326
|
+
` written by the build: ${xml.split("<url>").length - 1} urls; a sitemap.ts beside the root layout replaces it`,
|
|
1327
|
+
],
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
else {
|
|
1331
|
+
pending.push({
|
|
1332
|
+
line: " - /sitemap.xml",
|
|
1333
|
+
bytes: null,
|
|
1334
|
+
extra: [
|
|
1335
|
+
" not written: the root layout has no metadataBase to make the urls absolute",
|
|
1336
|
+
],
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1239
1340
|
for (const api of apis) {
|
|
1240
1341
|
pending.push({
|
|
1241
1342
|
line: ` ${api.type === "frozen" ? "○" : "ƒ"} ${api.url}`,
|
|
@@ -1480,6 +1581,23 @@ function renderRouteTypes(manifest) {
|
|
|
1480
1581
|
const target = relative(typesDir, join(sourceDir, route.component)).replace(/\\/g, "/");
|
|
1481
1582
|
search.set(pattern, target.startsWith(".") ? target : "./" + target);
|
|
1482
1583
|
}
|
|
1584
|
+
// Section names are read from the source - section('orders', …) - the way
|
|
1585
|
+
// generateStaticParams is detected, because this runs before any bundle
|
|
1586
|
+
// exists. A name computed at runtime is not seen and revalidate() falls
|
|
1587
|
+
// back to refusing it at the renderer, as before.
|
|
1588
|
+
const regions = [
|
|
1589
|
+
...new Set([
|
|
1590
|
+
...[...components.values()]
|
|
1591
|
+
.filter((c) => SECTION_FILE.test(c.absPath))
|
|
1592
|
+
.flatMap((c) => [
|
|
1593
|
+
...readFileSync(c.absPath, "utf-8").matchAll(/\bsection\(\s*['"`]([^'"`]+)['"`]/g),
|
|
1594
|
+
].map((m) => m[1])),
|
|
1595
|
+
...[...components.keys()].flatMap((name) => {
|
|
1596
|
+
const slot = name.split("/").find((part) => part.startsWith("@"));
|
|
1597
|
+
return slot ? [slot.slice(1)] : [];
|
|
1598
|
+
}),
|
|
1599
|
+
]),
|
|
1600
|
+
].sort();
|
|
1483
1601
|
return [
|
|
1484
1602
|
"// @generated — do not edit. Written by the RSC build from the route tree.",
|
|
1485
1603
|
"//",
|
|
@@ -1513,6 +1631,14 @@ function renderRouteTypes(manifest) {
|
|
|
1513
1631
|
// Api routes are a separate union, so Link refuses an api url and apiUrl()
|
|
1514
1632
|
// refuses a page. Linking to an api route navigates the browser away to a
|
|
1515
1633
|
// json document, which is the mistake worth catching.
|
|
1634
|
+
// Regions: every section('name', …) the build read, and every @slot
|
|
1635
|
+
// directory. This is what types revalidate().
|
|
1636
|
+
" interface RegisterRegions {",
|
|
1637
|
+
regions.length > 0
|
|
1638
|
+
? " regions:\n" +
|
|
1639
|
+
regions.map((r) => " | " + JSON.stringify(r)).join("\n")
|
|
1640
|
+
: " // No sections or slots found under the source directory.\n regions: never",
|
|
1641
|
+
" }",
|
|
1516
1642
|
" interface RegisterApi {",
|
|
1517
1643
|
apis.length > 0
|
|
1518
1644
|
? " apis:\n" +
|
|
@@ -1714,10 +1840,20 @@ function registerApiRoute(absPath) {
|
|
|
1714
1840
|
}
|
|
1715
1841
|
apiRoutes.set(name, { name, absPath, methods });
|
|
1716
1842
|
}
|
|
1843
|
+
/**
|
|
1844
|
+
* Whether a middleware.ts is a guard the engine runs, or only names guards
|
|
1845
|
+
* the host runs. The engine imports a guard's default export; a file with
|
|
1846
|
+
* none - `export const middleware = ['auth']` and nothing else - has nothing
|
|
1847
|
+
* to import, and registering it would make the build fail on an export that
|
|
1848
|
+
* was never meant to exist.
|
|
1849
|
+
*/
|
|
1850
|
+
function isEngineGuard(absPath) {
|
|
1851
|
+
return /export\s+default\b/.test(readFileSync(absPath, "utf-8"));
|
|
1852
|
+
}
|
|
1717
1853
|
function discover(dir) {
|
|
1718
1854
|
for (const base of ROUTE_FILES) {
|
|
1719
1855
|
const p = findRouteFile(dir, base);
|
|
1720
|
-
if (p)
|
|
1856
|
+
if (p && (base !== "middleware" || isEngineGuard(p)))
|
|
1721
1857
|
register(p);
|
|
1722
1858
|
}
|
|
1723
1859
|
// route.ts — an api endpoint, colocated with the pages it sits among. Read
|
|
@@ -1748,6 +1884,21 @@ function discover(dir) {
|
|
|
1748
1884
|
* pages export only the static object, so referencing both meant that warning
|
|
1749
1885
|
* for almost every route in an app.
|
|
1750
1886
|
*/
|
|
1887
|
+
/**
|
|
1888
|
+
* The root layout's metadataBase, read from the source. Wanted while the
|
|
1889
|
+
* manifest is built, before there is a bundle to execute - the same reason
|
|
1890
|
+
* generateStaticParams is detected by reading. A `new URL('…')` or a string
|
|
1891
|
+
* literal; anything computed is not seen, and rscKit({ hosts }) says it.
|
|
1892
|
+
*/
|
|
1893
|
+
function rootMetadataBase(appDir) {
|
|
1894
|
+
const layout = ["layout.tsx", "layout.jsx", "layout.ts", "layout.js"]
|
|
1895
|
+
.map((name) => join(appDir, name))
|
|
1896
|
+
.find((file) => existsSync(file));
|
|
1897
|
+
if (!layout)
|
|
1898
|
+
return null;
|
|
1899
|
+
const match = /metadataBase\s*:\s*(?:new\s+URL\(\s*)?["'`](https?:\/\/[^"'`]+)["'`]/.exec(readFileSync(layout, "utf-8"));
|
|
1900
|
+
return match ? match[1] : null;
|
|
1901
|
+
}
|
|
1751
1902
|
function metadataExports(absPath) {
|
|
1752
1903
|
const src = readFileSync(absPath, "utf-8");
|
|
1753
1904
|
return {
|
|
@@ -1942,7 +2093,36 @@ function installHostCallsOnce(): void {
|
|
|
1942
2093
|
}
|
|
1943
2094
|
|
|
1944
2095
|
`;
|
|
2096
|
+
/**
|
|
2097
|
+
* The app's process bootstrap, if it has one: `instrumentation.ts` beside
|
|
2098
|
+
* `app/`, the name Next uses so a reader arriving from there knows what it
|
|
2099
|
+
* is.
|
|
2100
|
+
*
|
|
2101
|
+
* Two things make it a framework concern rather than an import the app
|
|
2102
|
+
* adds. Module evaluation order: a page that configures a shared package at
|
|
2103
|
+
* import time runs before any module the app could put first, and only the
|
|
2104
|
+
* generated entry can import something before the pages. And "before the
|
|
2105
|
+
* first request": an async `register()` — connecting, validating env — has
|
|
2106
|
+
* to be awaited by every entry point a host or a prerender can call, which
|
|
2107
|
+
* is a list the app cannot see.
|
|
2108
|
+
*/
|
|
2109
|
+
function instrumentationFile() {
|
|
2110
|
+
for (const ext of ["ts", "tsx", "mts", "js", "mjs"]) {
|
|
2111
|
+
const file = join(sourceDir, `instrumentation.${ext}`);
|
|
2112
|
+
if (!existsSync(file))
|
|
2113
|
+
continue;
|
|
2114
|
+
// Both halves are optional: a file of imports is a bootstrap, a file
|
|
2115
|
+
// with register() is a hook. The entry must only name `register` when it
|
|
2116
|
+
// exists - a namespace import's missing member is a bundler warning on
|
|
2117
|
+
// every build (IMPORT_IS_UNDEFINED), which is the app being told off for
|
|
2118
|
+
// a file written exactly as documented.
|
|
2119
|
+
const hasRegister = /export\s+(?:async\s+)?(?:function\s+register\b|const\s+register\b|let\s+register\b|\{[^}]*\bregister\b[^}]*\})/.test(readFileSync(file, "utf-8"));
|
|
2120
|
+
return { file, hasRegister };
|
|
2121
|
+
}
|
|
2122
|
+
return null;
|
|
2123
|
+
}
|
|
1945
2124
|
function generateEntryRsc(fallbackOrigin = "") {
|
|
2125
|
+
const instrumentation = instrumentationFile();
|
|
1946
2126
|
// The 404 page, if the app has one, and the layouts it renders inside.
|
|
1947
2127
|
// Computed here rather than looked up at runtime: not-found is not a route,
|
|
1948
2128
|
// so the manifest has no entry to read its chain from.
|
|
@@ -1999,12 +2179,23 @@ function generateEntryRsc(fallbackOrigin = "") {
|
|
|
1999
2179
|
// from src/ in its own repo and from dist/ once published, and Vite resolves
|
|
2000
2180
|
// either. Naming .tsx here builds fine from source and fails after publish.
|
|
2001
2181
|
return `// GENERATED by rscKit() — do not edit.
|
|
2182
|
+
${
|
|
2183
|
+
// First, before any page: an import's side effects run in import order,
|
|
2184
|
+
// and a package configured here has to be configured before a page module
|
|
2185
|
+
// that reads it at evaluation time.
|
|
2186
|
+
instrumentation?.hasRegister
|
|
2187
|
+
? `import * as __instrumentation from ${JSON.stringify(instrumentation.file)}`
|
|
2188
|
+
: instrumentation
|
|
2189
|
+
? `import ${JSON.stringify(instrumentation.file)}\nconst __instrumentation: { register?: () => unknown } = {}`
|
|
2190
|
+
: "const __instrumentation: { register?: () => unknown } = {}"}
|
|
2002
2191
|
import { SegmentBoundary } from ${JSON.stringify(join(packageDir, "js/SegmentBoundary"))}
|
|
2192
|
+
import { LoadingBoundary } from ${JSON.stringify(join(packageDir, "js/LoadingBoundary"))}
|
|
2003
2193
|
import { DocumentTitle } from ${JSON.stringify(join(packageDir, "js/DocumentTitle"))}
|
|
2004
2194
|
import { SlotBoundary } from ${JSON.stringify(join(packageDir, "js/SlotBoundary"))}
|
|
2005
2195
|
import { RouteErrorBoundary } from ${JSON.stringify(join(packageDir, "js/RouteErrorBoundary"))}
|
|
2006
2196
|
import { sectionComponent } from ${JSON.stringify(join(packageDir, "js/section"))}
|
|
2007
2197
|
import { PathnameProvider } from ${JSON.stringify(join(packageDir, "js/PathnameProvider"))}
|
|
2198
|
+
import { DefaultRouteError } from ${JSON.stringify(join(packageDir, "js/DefaultRouteError"))}
|
|
2008
2199
|
import { searchParams as requestSearchParams } from ${JSON.stringify(join(packageDir, "request"))}
|
|
2009
2200
|
import { parseParams, parseSearchParams, parseBody, isSearchParamsError, isBodyError } from ${JSON.stringify(join(packageDir, "routeSchema"))}
|
|
2010
2201
|
import { notFoundDigest, isNotFoundSignal } from ${JSON.stringify(join(packageDir, "notFound"))}
|
|
@@ -2144,6 +2335,7 @@ export async function handleApiRoute(
|
|
|
2144
2335
|
params: Record<string, string>,
|
|
2145
2336
|
allow: string,
|
|
2146
2337
|
): Promise<Response> {
|
|
2338
|
+
await instrumented()
|
|
2147
2339
|
applyHost()
|
|
2148
2340
|
|
|
2149
2341
|
const mod = apiRoutes[name]
|
|
@@ -2251,6 +2443,8 @@ export async function auditActions(ids: string[]): Promise<{ id: string; client:
|
|
|
2251
2443
|
* that asked for everything, or the reverse.
|
|
2252
2444
|
*/
|
|
2253
2445
|
export async function getStaticParams(component: string): Promise<Record<string, string>[] | null> {
|
|
2446
|
+
await instrumented()
|
|
2447
|
+
|
|
2254
2448
|
const generate = staticParamsMap[component]
|
|
2255
2449
|
|
|
2256
2450
|
if (!generate) return null
|
|
@@ -2274,6 +2468,57 @@ const HOST_GLOBAL = ${JSON.stringify(hostGlobal)}
|
|
|
2274
2468
|
*/
|
|
2275
2469
|
const HOST_MIDDLEWARE_FN = '__rsc.middleware'
|
|
2276
2470
|
|
|
2471
|
+
/**
|
|
2472
|
+
* instrumentation.ts's register(), once.
|
|
2473
|
+
*
|
|
2474
|
+
* On a long-lived server it runs at startup: the entry is evaluated when the
|
|
2475
|
+
* process starts, and this begins then, so env that fails validation fails
|
|
2476
|
+
* the boot rather than the first visitor, and the first request does not
|
|
2477
|
+
* pay for it. On a Worker there is no startup — an isolate is created for a
|
|
2478
|
+
* request, and a binding is only readable once one has arrived — so it runs
|
|
2479
|
+
* at the first request of each isolate instead. Every entry point awaits it
|
|
2480
|
+
* either way, which is what makes "before the first render" hold whether the
|
|
2481
|
+
* caller is the built server, the dev server or the prerender.
|
|
2482
|
+
*
|
|
2483
|
+
* A rejection is not memoised. Env that fails validation fails the same way
|
|
2484
|
+
* on every request, which is right; a database that was not up yet gets
|
|
2485
|
+
* asked again rather than leaving the process permanently refusing. In
|
|
2486
|
+
* production a startup failure is reported and the process exits, because a
|
|
2487
|
+
* server that could not bootstrap has nothing correct to serve; the dev
|
|
2488
|
+
* server stays up and reports it on the page instead.
|
|
2489
|
+
*/
|
|
2490
|
+
let __instrumented: Promise<void> | null = null
|
|
2491
|
+
|
|
2492
|
+
function instrumented(): Promise<void> {
|
|
2493
|
+
if (!__instrumented) {
|
|
2494
|
+
__instrumented = Promise.resolve()
|
|
2495
|
+
.then(() => __instrumentation.register?.())
|
|
2496
|
+
.then(
|
|
2497
|
+
() => undefined,
|
|
2498
|
+
(error) => {
|
|
2499
|
+
__instrumented = null
|
|
2500
|
+
throw error
|
|
2501
|
+
},
|
|
2502
|
+
)
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
return __instrumented
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
// Workers name themselves; nothing else does. Where there is a process to
|
|
2509
|
+
// start, start now.
|
|
2510
|
+
const isolateRuntime = typeof navigator !== 'undefined' && navigator.userAgent === 'Cloudflare-Workers'
|
|
2511
|
+
|
|
2512
|
+
if (__instrumentation.register && !isolateRuntime) {
|
|
2513
|
+
instrumented().catch((error) => {
|
|
2514
|
+
console.error('[rsc-kit] instrumentation.ts register() failed:', error)
|
|
2515
|
+
|
|
2516
|
+
if (!import.meta.env.DEV && typeof process !== 'undefined' && typeof process.exit === 'function') {
|
|
2517
|
+
process.exit(1)
|
|
2518
|
+
}
|
|
2519
|
+
})
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2277
2522
|
let currentHost: HostFn | null = null
|
|
2278
2523
|
|
|
2279
2524
|
export function installHostFn(fn: HostFn) {
|
|
@@ -2481,9 +2726,17 @@ function buildElement(
|
|
|
2481
2726
|
searchParams: checkedSearchParams(schemas, pageSearchParams()),
|
|
2482
2727
|
})
|
|
2483
2728
|
|
|
2729
|
+
// Through LoadingBoundary when the runtime is shipped, so a server render
|
|
2730
|
+
// can tell the engine's boundary from one the developer wrote by name -
|
|
2731
|
+
// see that file. A page shipping no runtime gets a plain Suspense: a
|
|
2732
|
+
// client component would drag React in for a wrapper nothing can use.
|
|
2484
2733
|
for (let i = loadings.length - 1; i >= 0; i--) {
|
|
2485
2734
|
const Loading = components[loadings[i]]
|
|
2486
|
-
|
|
2735
|
+
const fallback = Loading ? createElement(Loading) : null
|
|
2736
|
+
|
|
2737
|
+
element = bootstrap
|
|
2738
|
+
? createElement(LoadingBoundary, { fallback }, element)
|
|
2739
|
+
: createElement(Suspense, { fallback }, element)
|
|
2487
2740
|
}
|
|
2488
2741
|
|
|
2489
2742
|
// Outside the Suspense boundary, innermost first — the nearest error.tsx to
|
|
@@ -2507,6 +2760,19 @@ function buildElement(
|
|
|
2507
2760
|
)
|
|
2508
2761
|
}
|
|
2509
2762
|
|
|
2763
|
+
// Outermost, for a throw no error.tsx covers - including one in a layout,
|
|
2764
|
+
// which the boundary inside that layout cannot see. Without it React
|
|
2765
|
+
// unmounted the document on hydration: a black page with the cause nowhere
|
|
2766
|
+
// near it. Only with the runtime: it is a client component, and a route
|
|
2767
|
+
// shipping none has nothing to catch with.
|
|
2768
|
+
if (bootstrap) {
|
|
2769
|
+
element = createElement(
|
|
2770
|
+
RouteErrorBoundary,
|
|
2771
|
+
{ fallback: DefaultRouteError as never, resetKey: pageKey || component },
|
|
2772
|
+
element,
|
|
2773
|
+
)
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2510
2776
|
// <title>/<meta> go OUTSIDE the Suspense boundaries so they reach the shell
|
|
2511
2777
|
// immediately — inside, they would be withheld until the page's data
|
|
2512
2778
|
// resolves, delaying the whole document on a slow page.
|
|
@@ -2660,6 +2926,8 @@ async function renderTree(
|
|
|
2660
2926
|
if (bootstrap) head.push(createElement(DocumentTitle, { key: '__ts', title: String(md.title) }))
|
|
2661
2927
|
}
|
|
2662
2928
|
if (md.description != null) head.push(createElement('meta', { key: '__d', name: 'description', content: String(md.description) }))
|
|
2929
|
+
// The name only, never the version - see the identify option.
|
|
2930
|
+
if (manifest().build?.identify) head.push(createElement('meta', { key: '__g', name: 'generator', content: 'rsc-kit' }))
|
|
2663
2931
|
|
|
2664
2932
|
// robots is a string, or the object Next takes: index and follow as
|
|
2665
2933
|
// their no- forms, the flags by name, the limits as name:value. googleBot
|
|
@@ -2961,6 +3229,7 @@ async function runMiddleware(component: string, props: Record<string, unknown> =
|
|
|
2961
3229
|
* throws, exactly as it does mid-render.
|
|
2962
3230
|
*/
|
|
2963
3231
|
export async function runRouteMiddleware(component: string, props: Record<string, unknown> = {}): Promise<void> {
|
|
3232
|
+
await instrumented()
|
|
2964
3233
|
applyHost()
|
|
2965
3234
|
|
|
2966
3235
|
return runMiddleware(component, props)
|
|
@@ -2976,6 +3245,7 @@ export async function handleRscStream(
|
|
|
2976
3245
|
from = 0,
|
|
2977
3246
|
pageKey = '',
|
|
2978
3247
|
): Promise<{ stream: ReadableStream; clientChunks: unknown; segmentDepth: number }> {
|
|
3248
|
+
await instrumented()
|
|
2979
3249
|
applyHost()
|
|
2980
3250
|
|
|
2981
3251
|
// The host proposes how much the client already has; the engine decides what
|
|
@@ -3031,6 +3301,7 @@ export async function handleRscHtmlStream(
|
|
|
3031
3301
|
pageKey = '',
|
|
3032
3302
|
bootstrap = true,
|
|
3033
3303
|
): Promise<{ htmlStream: ReadableStream; rscPayloadPromise: Promise<string>; clientChunks: unknown }> {
|
|
3304
|
+
await instrumented()
|
|
3034
3305
|
applyHost()
|
|
3035
3306
|
await runMiddleware(component, props)
|
|
3036
3307
|
const flight = renderToReadableStream(
|
|
@@ -3066,6 +3337,7 @@ export async function handleRscResume(
|
|
|
3066
3337
|
nonce?: string,
|
|
3067
3338
|
pageKey = '',
|
|
3068
3339
|
): Promise<{ htmlStream: ReadableStream }> {
|
|
3340
|
+
await instrumented()
|
|
3069
3341
|
applyHost()
|
|
3070
3342
|
await runMiddleware(component, props)
|
|
3071
3343
|
|
|
@@ -3216,6 +3488,7 @@ export async function handleQuery(
|
|
|
3216
3488
|
| { status: number; message: string; errors?: Record<string, string[]> }
|
|
3217
3489
|
| null
|
|
3218
3490
|
> {
|
|
3491
|
+
await instrumented()
|
|
3219
3492
|
applyHost()
|
|
3220
3493
|
|
|
3221
3494
|
let fn: unknown
|
|
@@ -3264,6 +3537,7 @@ export async function handleAction(
|
|
|
3264
3537
|
page?: PageContext,
|
|
3265
3538
|
takeRevalidated?: () => string[],
|
|
3266
3539
|
): Promise<{ stream: ReadableStream }> {
|
|
3540
|
+
await instrumented()
|
|
3267
3541
|
applyHost()
|
|
3268
3542
|
|
|
3269
3543
|
// Every body arrives as bytes on its own socket frame — an upload because it
|
|
@@ -3356,6 +3630,8 @@ export async function resolveMetadata(
|
|
|
3356
3630
|
props: Record<string, unknown> = {},
|
|
3357
3631
|
layouts: LayoutEntry[] = [],
|
|
3358
3632
|
): Promise<Record<string, unknown> | null> {
|
|
3633
|
+
await instrumented()
|
|
3634
|
+
|
|
3359
3635
|
const pageEntry = metadataMap[component]
|
|
3360
3636
|
const page: Record<string, unknown> = pageEntry
|
|
3361
3637
|
? (pageEntry.generate
|
|
@@ -3425,6 +3701,7 @@ export async function handleRsc(
|
|
|
3425
3701
|
bootstrap = true,
|
|
3426
3702
|
canReachHost = true,
|
|
3427
3703
|
): Promise<{ body: string; rscPayload: string; clientChunks: unknown; usedDynamicApis: boolean; dynamicBecause: string[]; clientComponents: string[]; serverReferences: boolean }> {
|
|
3704
|
+
await instrumented()
|
|
3428
3705
|
applyHost()
|
|
3429
3706
|
|
|
3430
3707
|
// A build renders this with no host installed, so every rpc() has to suspend
|
|
@@ -3538,6 +3815,7 @@ export async function handleRscRevalidate(
|
|
|
3538
3815
|
target: string,
|
|
3539
3816
|
page: PageContext,
|
|
3540
3817
|
): Promise<{ rscPayload: string }> {
|
|
3818
|
+
await instrumented()
|
|
3541
3819
|
applyHost()
|
|
3542
3820
|
|
|
3543
3821
|
const flight = renderToReadableStream(await renderRevalidated(target, page))
|
|
@@ -3554,6 +3832,7 @@ export async function handleRscPayload(
|
|
|
3554
3832
|
from = 0,
|
|
3555
3833
|
pageKey = '',
|
|
3556
3834
|
): Promise<{ rscPayload: string }> {
|
|
3835
|
+
await instrumented()
|
|
3557
3836
|
applyHost()
|
|
3558
3837
|
|
|
3559
3838
|
const flight = renderToReadableStream(
|
|
@@ -3621,6 +3900,7 @@ export async function handleRscPprShell(
|
|
|
3621
3900
|
// and make every guarded route dynamic for the wrong reason.
|
|
3622
3901
|
let usedDynamicApis = false
|
|
3623
3902
|
|
|
3903
|
+
await instrumented()
|
|
3624
3904
|
applyHost()
|
|
3625
3905
|
|
|
3626
3906
|
const probe = (..._args: unknown[]) => {
|
|
@@ -3790,6 +4070,17 @@ export default async function handler(request: Request): Promise<Response> {
|
|
|
3790
4070
|
}
|
|
3791
4071
|
|
|
3792
4072
|
async function serve(request: Request): Promise<Response> {
|
|
4073
|
+
// The startup plugin's probe. Nitro loads this module at the first request
|
|
4074
|
+
// rather than when the server starts, so a plugin that runs at startup
|
|
4075
|
+
// sends one request to load it; the module's evaluation began register(),
|
|
4076
|
+
// and this waits for it so a failure is reported as the startup failure it
|
|
4077
|
+
// is. No page is rendered and nothing is disclosed.
|
|
4078
|
+
if (request.headers.get('x-rsc-kit-startup') !== null) {
|
|
4079
|
+
await instrumented()
|
|
4080
|
+
|
|
4081
|
+
return new Response(null, { status: 204 })
|
|
4082
|
+
}
|
|
4083
|
+
|
|
3793
4084
|
installHostCallsOnce()
|
|
3794
4085
|
|
|
3795
4086
|
devHandler ??= createRscHandler({
|
|
@@ -3924,35 +4215,7 @@ export async function handleSsr(
|
|
|
3924
4215
|
const html = await renderToReadableStream(root as any, {
|
|
3925
4216
|
bootstrapScriptContent,
|
|
3926
4217
|
nonce,
|
|
3927
|
-
|
|
3928
|
-
// its digest is what lets the client tell it from a fault on hydration;
|
|
3929
|
-
// returned here so React writes it into the document.
|
|
3930
|
-
onError: onError ?? ((error: unknown, info?: { componentStack?: string }) => {
|
|
3931
|
-
// The consumer cancelled - a browser that left mid-stream, a prefetch
|
|
3932
|
-
// abandoned. React reports it as an error; the page had none.
|
|
3933
|
-
if (cancelledByConsumer(error)) return
|
|
3934
|
-
const digest = (error as { digest?: string } | null)?.digest
|
|
3935
|
-
if (digest === 'rsc-kit:search-params-fallback') {
|
|
3936
|
-
// The component is the first frame of React's stack. Noted on the
|
|
3937
|
-
// request so the build attaches it to the route; printed as one line,
|
|
3938
|
-
// not a stack, so the dev server says which boundary the build wants.
|
|
3939
|
-
const where = /at ([A-Z][\\w$]*)/.exec(info?.componentStack ?? '')?.[1]
|
|
3940
|
-
noteFallback('useSearchParams()' + (where ? ' in ' + where : ''))
|
|
3941
|
-
// Under a boundary the developer wrote, nothing to say. With nothing
|
|
3942
|
-
// closer than a loading.tsx, one line: the whole segment is the
|
|
3943
|
-
// fallback until the query arrives.
|
|
3944
|
-
if (caughtByLoading(info?.componentStack)) {
|
|
3945
|
-
console.error(
|
|
3946
|
-
'[rsc-kit] ' + (where ? where + ': ' : '') +
|
|
3947
|
-
'useSearchParams() was read on the server with nothing closer than a loading.tsx, so the whole ' +
|
|
3948
|
-
'segment shows that fallback until the query arrives. A <Suspense> around the component that reads ' +
|
|
3949
|
-
'keeps the rest of the page painted.',
|
|
3950
|
-
)
|
|
3951
|
-
}
|
|
3952
|
-
return digest
|
|
3953
|
-
}
|
|
3954
|
-
console.error('[rsc-kit:ssr]', error)
|
|
3955
|
-
}),
|
|
4218
|
+
onError: onError ?? reportRenderError('ssr'),
|
|
3956
4219
|
})
|
|
3957
4220
|
|
|
3958
4221
|
return DEV_ORIGIN ? rewriteViteDevUrlStream(html, DEV_ORIGIN) : html
|
|
@@ -4004,6 +4267,46 @@ export async function handleSsrPrerender(
|
|
|
4004
4267
|
}
|
|
4005
4268
|
}
|
|
4006
4269
|
|
|
4270
|
+
/**
|
|
4271
|
+
* What a server render says about an error, first render and resume alike.
|
|
4272
|
+
*
|
|
4273
|
+
* The query-string fallback is the designed path for a stored page, and its
|
|
4274
|
+
* digest is what lets the client tell it from a fault on hydration; returned
|
|
4275
|
+
* so React writes it into the document. Under a boundary the developer wrote
|
|
4276
|
+
* there is nothing to say; with nothing closer than a loading.tsx, one line.
|
|
4277
|
+
*/
|
|
4278
|
+
function reportRenderError(phase: 'ssr' | 'resume') {
|
|
4279
|
+
return (error: unknown, info?: { componentStack?: string }) => {
|
|
4280
|
+
// The consumer cancelled - a browser that left mid-stream, a prefetch
|
|
4281
|
+
// abandoned. React reports it as an error; the page had none.
|
|
4282
|
+
if (cancelledByConsumer(error)) return
|
|
4283
|
+
|
|
4284
|
+
const digest = (error as { digest?: string } | null)?.digest
|
|
4285
|
+
|
|
4286
|
+
if (digest === 'rsc-kit:search-params-fallback') {
|
|
4287
|
+
// The component is the first frame of React's stack. Noted on the
|
|
4288
|
+
// request so the build attaches it to the route; printed as one line,
|
|
4289
|
+
// not a stack, so the server says which boundary it wants.
|
|
4290
|
+
const where = /at ([A-Z][\\w$]*)/.exec(info?.componentStack ?? '')?.[1]
|
|
4291
|
+
|
|
4292
|
+
noteFallback('useSearchParams()' + (where ? ' in ' + where : ''))
|
|
4293
|
+
|
|
4294
|
+
if (caughtByLoading(info?.componentStack)) {
|
|
4295
|
+
console.error(
|
|
4296
|
+
'[rsc-kit] ' + (where ? where + ': ' : '') +
|
|
4297
|
+
'useSearchParams() was read on the server with nothing closer than a loading.tsx, so the whole ' +
|
|
4298
|
+
'segment shows that fallback until the query arrives. A <Suspense> around the component that reads ' +
|
|
4299
|
+
'keeps the rest of the page painted.',
|
|
4300
|
+
)
|
|
4301
|
+
}
|
|
4302
|
+
|
|
4303
|
+
return digest
|
|
4304
|
+
}
|
|
4305
|
+
|
|
4306
|
+
console.error('[rsc-kit:' + phase + ']', error)
|
|
4307
|
+
}
|
|
4308
|
+
}
|
|
4309
|
+
|
|
4007
4310
|
/**
|
|
4008
4311
|
* Pick a build-time render back up, against data that exists now.
|
|
4009
4312
|
*
|
|
@@ -4023,7 +4326,12 @@ export async function handleSsrResume(
|
|
|
4023
4326
|
|
|
4024
4327
|
const html = await resume(root as any, postponed as any, {
|
|
4025
4328
|
nonce,
|
|
4026
|
-
|
|
4329
|
+
// The same reading of an error the first render has. This used to log
|
|
4330
|
+
// every error raw, so a query read the developer's own boundary caught -
|
|
4331
|
+
// the designed path, on every resume of a shell whose hole holds one -
|
|
4332
|
+
// printed as "[rsc-kit:resume] Error: useSearchParams() was read..." on
|
|
4333
|
+
// every request, with advice the app had already followed.
|
|
4334
|
+
onError: reportRenderError('resume'),
|
|
4027
4335
|
})
|
|
4028
4336
|
|
|
4029
4337
|
return DEV_ORIGIN ? rewriteViteDevUrlStream(html, DEV_ORIGIN) : html
|
|
@@ -4304,6 +4612,46 @@ self.addEventListener('activate', (event) => {
|
|
|
4304
4612
|
})())
|
|
4305
4613
|
})
|
|
4306
4614
|
`;
|
|
4615
|
+
/**
|
|
4616
|
+
* Named imports from a barrel package, unrolled on the server environments.
|
|
4617
|
+
*
|
|
4618
|
+
* The browser gets lucide pre-bundled; the server environments do not, so
|
|
4619
|
+
* `import { ArrowRight } from 'lucide-react'` loaded and transformed every
|
|
4620
|
+
* icon module - 3,700 transforms, twenty-five seconds before the first
|
|
4621
|
+
* document, for thirteen icons. See barrelImports. Development only: a
|
|
4622
|
+
* build tree-shakes the barrel itself.
|
|
4623
|
+
*/
|
|
4624
|
+
function barrelImportsPlugin() {
|
|
4625
|
+
const entries = new Map();
|
|
4626
|
+
return {
|
|
4627
|
+
name: "rsc-kit:barrel-imports",
|
|
4628
|
+
apply: () => barrelImports && process.env.NODE_ENV !== "production",
|
|
4629
|
+
enforce: "pre",
|
|
4630
|
+
applyToEnvironment: (environment) => environment.name === "rsc" || environment.name === "ssr",
|
|
4631
|
+
async transform(code, id) {
|
|
4632
|
+
if (id.includes("/node_modules/") ||
|
|
4633
|
+
!/\.[cm]?[jt]sx?$/.test(id.split("?")[0]))
|
|
4634
|
+
return;
|
|
4635
|
+
if (!code.includes("from"))
|
|
4636
|
+
return;
|
|
4637
|
+
const pending = [];
|
|
4638
|
+
const wanted = new Set();
|
|
4639
|
+
for (const m of code.matchAll(/import\s*(?:type\s+)?\{[^}]*\}\s*from\s*['"]([^'"./][^'"]*)['"]/g)) {
|
|
4640
|
+
wanted.add(m[1]);
|
|
4641
|
+
}
|
|
4642
|
+
for (const specifier of wanted) {
|
|
4643
|
+
if (entries.has(specifier))
|
|
4644
|
+
continue;
|
|
4645
|
+
pending.push(this.resolve(specifier, id).then((resolved) => {
|
|
4646
|
+
entries.set(specifier, resolved && !resolved.external ? resolved.id.split("?")[0] : null);
|
|
4647
|
+
}));
|
|
4648
|
+
}
|
|
4649
|
+
await Promise.all(pending);
|
|
4650
|
+
const out = unrollBarrelImports(code, (specifier) => entries.get(specifier) ?? null);
|
|
4651
|
+
return out === null ? undefined : { code: out, map: null };
|
|
4652
|
+
},
|
|
4653
|
+
};
|
|
4654
|
+
}
|
|
4307
4655
|
/**
|
|
4308
4656
|
* "use ssr" modules, rewritten for the server-components environment into
|
|
4309
4657
|
* proxies that call the real module in the ssr environment. See useSsr.ts.
|
|
@@ -4478,10 +4826,107 @@ function clientImportsAudit() {
|
|
|
4478
4826
|
},
|
|
4479
4827
|
};
|
|
4480
4828
|
}
|
|
4829
|
+
/**
|
|
4830
|
+
* A Nitro runtime plugin, so instrumentation.ts runs when the server starts.
|
|
4831
|
+
*
|
|
4832
|
+
* Nitro loads the rsc service at the first request, not at startup - so
|
|
4833
|
+
* without this, register() would run when the first visitor arrived, and a
|
|
4834
|
+
* bad environment would take down a server that had already reported itself
|
|
4835
|
+
* up. The plugin runs at app init and sends the service one request, which
|
|
4836
|
+
* loads the module; loading it begins register(), and the request waits for
|
|
4837
|
+
* it. Not on a Worker: there is no startup, and the first request is the
|
|
4838
|
+
* first chance to read a binding.
|
|
4839
|
+
*/
|
|
4840
|
+
const STARTUP_PLUGIN = `import { viteServices } from "#nitro/virtual/vite-services"
|
|
4841
|
+
|
|
4842
|
+
export default function rscKitStartup() {
|
|
4843
|
+
if (typeof navigator !== 'undefined' && navigator.userAgent === 'Cloudflare-Workers') return
|
|
4844
|
+
|
|
4845
|
+
// The ssr service is the one Nitro's renderer asks; its fetch imports the
|
|
4846
|
+
// rsc entry and hands the request on. The rsc service's own export is the
|
|
4847
|
+
// handler function, not a { fetch }, so it cannot be probed directly.
|
|
4848
|
+
const ssr = viteServices.ssr
|
|
4849
|
+
if (!ssr) return
|
|
4850
|
+
|
|
4851
|
+
// Two ways to fail, one outcome. register() rejecting is reported by the
|
|
4852
|
+
// module itself. instrumentation.ts throwing at import - an env schema
|
|
4853
|
+
// refusing - means the module never evaluated, so nothing in it can
|
|
4854
|
+
// report; the load rejects here instead. Either way a server that could
|
|
4855
|
+
// not bootstrap has nothing correct to serve, and says so and stops
|
|
4856
|
+
// rather than reporting itself up.
|
|
4857
|
+
ssr
|
|
4858
|
+
.fetch(new Request('http://rsc-kit.internal/_rsc/startup', { headers: { 'x-rsc-kit-startup': '1' } }))
|
|
4859
|
+
.then((response) => {
|
|
4860
|
+
if (!response.ok) throw new Error('the startup probe answered ' + response.status)
|
|
4861
|
+
})
|
|
4862
|
+
.catch((error) => {
|
|
4863
|
+
console.error('[rsc-kit] instrumentation.ts failed at startup:', error)
|
|
4864
|
+
|
|
4865
|
+
if (typeof process !== 'undefined' && typeof process.exit === 'function') process.exit(1)
|
|
4866
|
+
})
|
|
4867
|
+
}
|
|
4868
|
+
`;
|
|
4869
|
+
/**
|
|
4870
|
+
* Where NODE_ENV=development came from, and what to do about it.
|
|
4871
|
+
*
|
|
4872
|
+
* Vite reads NODE_ENV from its env files in this order, later winning, and
|
|
4873
|
+
* only when the process did not already set it - so the last file that
|
|
4874
|
+
* names it is the one that decided. No file naming it means the shell or
|
|
4875
|
+
* `--mode` did.
|
|
4876
|
+
*/
|
|
4877
|
+
function nodeEnvSetIn(mode, envDir) {
|
|
4878
|
+
const files = [".env", ".env.local", `.env.${mode}`, `.env.${mode}.local`];
|
|
4879
|
+
let culprit = null;
|
|
4880
|
+
// The project's root, not Vite's: this plugin points Vite's root at the
|
|
4881
|
+
// build directory, and the app's .env is read from the project root - by
|
|
4882
|
+
// Nitro's dotenv loading into process.env, and by Vite where envDir says
|
|
4883
|
+
// so. Both places are looked at; the project's is where the file is.
|
|
4884
|
+
const dirs = [...new Set([projectRoot, envDir || projectRoot])];
|
|
4885
|
+
for (const dir of dirs)
|
|
4886
|
+
for (const name of files) {
|
|
4887
|
+
const path = join(dir, name);
|
|
4888
|
+
if (!existsSync(path))
|
|
4889
|
+
continue;
|
|
4890
|
+
const lines = readFileSync(path, "utf-8").split("\n");
|
|
4891
|
+
const at = lines.findIndex((line) => /^\s*(?:export\s+)?NODE_ENV\s*=/.test(line));
|
|
4892
|
+
if (at !== -1)
|
|
4893
|
+
culprit = `${path}:${at + 1}`;
|
|
4894
|
+
}
|
|
4895
|
+
return culprit;
|
|
4896
|
+
}
|
|
4897
|
+
function refuseDevelopmentBuild(config) {
|
|
4898
|
+
const culprit = nodeEnvSetIn(config.mode, config.envDir);
|
|
4899
|
+
return ("[rsc-kit] vite build is running as a development build, and the output cannot work: " +
|
|
4900
|
+
"the pages are compiled against React's development JSX runtime (jsxDEV), the server " +
|
|
4901
|
+
"bundles carry React's production build, and every route fails to render.\n\n" +
|
|
4902
|
+
(culprit
|
|
4903
|
+
? `NODE_ENV=development is set in ${culprit}. Remove that line - Vite sets NODE_ENV ` +
|
|
4904
|
+
"itself: development under `vite`, production under `vite build`."
|
|
4905
|
+
: `NODE_ENV is "${process.env.NODE_ENV ?? ""}" from the environment or --mode ` +
|
|
4906
|
+
"(mode: " + JSON.stringify(config.mode) + "). Build with NODE_ENV=production, or unset it."));
|
|
4907
|
+
}
|
|
4481
4908
|
export function rscKit(options = {}) {
|
|
4482
4909
|
resolvePaths(options);
|
|
4910
|
+
const serverExternals = [
|
|
4911
|
+
...RUNTIME_BUILTINS,
|
|
4912
|
+
...[...DEFAULT_SERVER_EXTERNALS, ...(options.serverExternalPackages ?? [])].map(externalPackage),
|
|
4913
|
+
];
|
|
4483
4914
|
const routesPlugin = {
|
|
4484
4915
|
name: "rsc-kit",
|
|
4916
|
+
// A Nitro module, which Nitro's Vite plugin collects from any plugin
|
|
4917
|
+
// that carries one. Only for a built server: the dev server evaluates
|
|
4918
|
+
// the entry through Vite's runner when it is first asked for, and there
|
|
4919
|
+
// is no earlier moment to offer.
|
|
4920
|
+
nitro: {
|
|
4921
|
+
name: "rsc-kit",
|
|
4922
|
+
setup(nitro) {
|
|
4923
|
+
if (nitro.options.dev || !instrumentationFile())
|
|
4924
|
+
return;
|
|
4925
|
+
nitro.options.virtual ??= {};
|
|
4926
|
+
nitro.options.virtual["#rsc-kit/startup"] = STARTUP_PLUGIN;
|
|
4927
|
+
nitro.options.plugins = [...(nitro.options.plugins ?? []), "#rsc-kit/startup"];
|
|
4928
|
+
},
|
|
4929
|
+
},
|
|
4485
4930
|
config(_config, env) {
|
|
4486
4931
|
if (!existsSync(appDir)) {
|
|
4487
4932
|
throw new Error(`[rsc-kit] No app directory at ${appDir} — nothing to build.`);
|
|
@@ -4493,6 +4938,7 @@ export function rscKit(options = {}) {
|
|
|
4493
4938
|
apiRoutes.clear();
|
|
4494
4939
|
discover(appDir);
|
|
4495
4940
|
registerMetadataRoutes(appDir);
|
|
4941
|
+
siteHosts = ownHosts(rootMetadataBase(appDir), hostsOption);
|
|
4496
4942
|
// Silent when it worked. The names were printed on every dev start and
|
|
4497
4943
|
// every build — thirty of them for a middling app, above the output that
|
|
4498
4944
|
// actually says something, and the build's classification table lists
|
|
@@ -4680,7 +5126,7 @@ export function rscKit(options = {}) {
|
|
|
4680
5126
|
build: {
|
|
4681
5127
|
rollupOptions: {
|
|
4682
5128
|
input: { index: join(genDir, "entry.rsc.tsx") },
|
|
4683
|
-
external:
|
|
5129
|
+
external: serverExternals,
|
|
4684
5130
|
},
|
|
4685
5131
|
},
|
|
4686
5132
|
},
|
|
@@ -4688,7 +5134,7 @@ export function rscKit(options = {}) {
|
|
|
4688
5134
|
build: {
|
|
4689
5135
|
rollupOptions: {
|
|
4690
5136
|
input: { index: join(genDir, "entry.ssr.tsx") },
|
|
4691
|
-
external:
|
|
5137
|
+
external: serverExternals,
|
|
4692
5138
|
},
|
|
4693
5139
|
},
|
|
4694
5140
|
},
|
|
@@ -4927,6 +5373,24 @@ export function rscKit(options = {}) {
|
|
|
4927
5373
|
}
|
|
4928
5374
|
},
|
|
4929
5375
|
configResolved(config) {
|
|
5376
|
+
// A build that is not a production build cannot work here, and the
|
|
5377
|
+
// way it fails is opaque: plugin-react emits the development JSX
|
|
5378
|
+
// runtime (jsxDEV), the server bundles resolve React's production
|
|
5379
|
+
// build, and every route fails to render with React's "message
|
|
5380
|
+
// omitted in production builds". The usual cause is NODE_ENV=development
|
|
5381
|
+
// in a .env file, which Vite honours - a line the scaffold itself once
|
|
5382
|
+
// wrote. Named, with the file and line, before any of that happens.
|
|
5383
|
+
//
|
|
5384
|
+
// Refused rather than overridden, because a plugin cannot override it:
|
|
5385
|
+
// Vite notes whether NODE_ENV was set before it loads the config file
|
|
5386
|
+
// and applies the .env value after the config hooks have run, so an
|
|
5387
|
+
// assignment here is overwritten - and patching the resolved config
|
|
5388
|
+
// afterwards leaves the client bundle's process.env.NODE_ENV and
|
|
5389
|
+
// import.meta.env.DEV already decided, which is a production server
|
|
5390
|
+
// serving a development client.
|
|
5391
|
+
if (config.command === "build" && !config.isProduction) {
|
|
5392
|
+
throw new Error(refuseDevelopmentBuild(config));
|
|
5393
|
+
}
|
|
4930
5394
|
isWatch = config.build?.watch != null;
|
|
4931
5395
|
resolvedConfig = config;
|
|
4932
5396
|
const nitroMain = config.plugins.find((p) => p.name === "nitro:main");
|
|
@@ -4976,6 +5440,7 @@ export function rscKit(options = {}) {
|
|
|
4976
5440
|
clientChunks: (meta) => meta.normalizedId,
|
|
4977
5441
|
...actionEncryptionKey(),
|
|
4978
5442
|
}),
|
|
5443
|
+
barrelImportsPlugin(),
|
|
4979
5444
|
useSsrModules(),
|
|
4980
5445
|
serverRendererInRsc(),
|
|
4981
5446
|
metadataRoutesPlugin(),
|