@rsc-kit/core 0.16.3 → 0.17.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/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/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/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/useLinkStatus.d.ts +14 -1
- package/dist/js/useLinkStatus.js +15 -1
- package/dist/js/useLinkStatus.js.map +1 -1
- package/dist/metadata.d.ts +94 -15
- package/dist/metadata.js.map +1 -1
- package/dist/metadataRoutes.d.ts +41 -0
- package/dist/metadataRoutes.js +138 -0
- package/dist/metadataRoutes.js.map +1 -0
- package/dist/prerender.js +43 -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/request.d.ts +18 -1
- package/dist/request.js +91 -5
- package/dist/request.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 +48 -11
- package/dist/vite.js +495 -20
- package/dist/vite.js.map +1 -1
- package/package.json +1 -1
package/dist/vite.js
CHANGED
|
@@ -16,13 +16,19 @@ 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 { METADATA_ROUTES, ROOT_FILES, rootFileType } from "./metadataRoutes.js";
|
|
31
|
+
import { serverRendererMessage, SERVER_RENDERER, ssrProxyModule, UseSsrError, } from "./useSsr.js";
|
|
26
32
|
import { httpHostCalls } from "./hostCalls.js";
|
|
27
33
|
// Resolved once per rscKit() call. One build runs in one process, so these are
|
|
28
34
|
// module state rather than threaded through every helper.
|
|
@@ -30,8 +36,13 @@ let projectRoot;
|
|
|
30
36
|
let sourceDir;
|
|
31
37
|
let inlineStylesheets = "auto";
|
|
32
38
|
let resolvedConfig = null;
|
|
39
|
+
/** Server files importing a client library, read off the rsc graph when it is built. */
|
|
40
|
+
let clientLibraryImports = [];
|
|
33
41
|
/** Modules a runtime provides and no bundle should try to carry. */
|
|
34
42
|
const RUNTIME_BUILTINS = ["bun", /^bun:/];
|
|
43
|
+
function arrayOf(value) {
|
|
44
|
+
return value == null ? [] : Array.isArray(value) ? value : [value];
|
|
45
|
+
}
|
|
35
46
|
/**
|
|
36
47
|
* What a client chunk is called on disk.
|
|
37
48
|
*
|
|
@@ -93,10 +104,9 @@ let routeConfig;
|
|
|
93
104
|
let prerenderAfterBuild;
|
|
94
105
|
/** True during `vite build --watch`, where re-rendering every route is noise. */
|
|
95
106
|
let isWatch = false;
|
|
96
|
-
/** Whether navigations are wrapped in React's ViewTransition — see options. */
|
|
97
|
-
let viewTransitions = false;
|
|
98
107
|
/** Whether a service worker is generated and registered — see options. */
|
|
99
108
|
let offline = false;
|
|
109
|
+
let typecheck = true;
|
|
100
110
|
let webManifestOptions = null;
|
|
101
111
|
let foundAssets = {
|
|
102
112
|
favicon: null,
|
|
@@ -258,8 +268,8 @@ function resolvePaths(options) {
|
|
|
258
268
|
// sets it, and so does a host that drives the build out of process and
|
|
259
269
|
// prerenders itself afterwards with paths only it knows.
|
|
260
270
|
prerenderAfterBuild = process.env.RSC_PRERENDER !== "0";
|
|
261
|
-
viewTransitions = options.viewTransitions === true;
|
|
262
271
|
offline = options.offline === true;
|
|
272
|
+
typecheck = options.typecheck !== false;
|
|
263
273
|
inlineStylesheets = options.inlineStylesheets ?? "auto";
|
|
264
274
|
maxActionBody = options.maxActionBody;
|
|
265
275
|
// One place, and it is the file. A plugin option as well would be the same
|
|
@@ -474,11 +484,13 @@ function routeManifest() {
|
|
|
474
484
|
build: { output, exportPath, payloadName: staticPayloads },
|
|
475
485
|
routes,
|
|
476
486
|
intercepts,
|
|
477
|
-
apis: [...apiRoutes.values()].map(({ name, methods }) => ({
|
|
487
|
+
apis: [...apiRoutes.values()].map(({ name, methods, generated }) => ({
|
|
478
488
|
name,
|
|
479
489
|
segments: urlSegments(name),
|
|
480
490
|
methods,
|
|
481
|
-
|
|
491
|
+
// A synthesised robots.txt or sitemap.xml runs no guard: it exists to be
|
|
492
|
+
// read by anyone, and a guard on the root would 401 the crawler.
|
|
493
|
+
middleware: generated ? [] : ancestors(name, "middleware"),
|
|
482
494
|
})),
|
|
483
495
|
};
|
|
484
496
|
}
|
|
@@ -956,6 +968,9 @@ export function declaredManifest(appDir) {
|
|
|
956
968
|
function copyAppAssets(clientDir) {
|
|
957
969
|
if (!existsSync(clientDir))
|
|
958
970
|
return;
|
|
971
|
+
for (const file of rootFiles) {
|
|
972
|
+
copyFileSync(join(sourceDir, "app", file), join(clientDir, file));
|
|
973
|
+
}
|
|
959
974
|
const all = allAppAssets(foundAssets);
|
|
960
975
|
if (all.length === 0)
|
|
961
976
|
return;
|
|
@@ -1258,6 +1273,30 @@ async function prerenderAfterBundles(bundle, staticDir, assetsDir, knownActions
|
|
|
1258
1273
|
console.log(" Nothing checks who calls them. Fine for a public one; otherwise build it\n" +
|
|
1259
1274
|
" from an action client, so the check cannot be forgotten.");
|
|
1260
1275
|
}
|
|
1276
|
+
// Server files importing cache from React. Its cache() memoises on the
|
|
1277
|
+
// dispatcher a render installs, so in a guard, an action, an api route or
|
|
1278
|
+
// the SSR pass it calls straight through - no dedupe, no error, the helper
|
|
1279
|
+
// runs twice. The difference is silent, which is why this line exists.
|
|
1280
|
+
const reactCache = reactCacheImports(sourceDir);
|
|
1281
|
+
if (reactCache.length > 0) {
|
|
1282
|
+
console.log(`\n \u26a0 ${reactCache.length} server ${reactCache.length === 1 ? "file imports" : "files import"} cache from 'react': ` +
|
|
1283
|
+
reactCache.join(", "));
|
|
1284
|
+
console.log(" React's cache() dedupes only inside a component render; in a guard, an action or\n" +
|
|
1285
|
+
" an api route it calls straight through. Import it from @rsc-kit/core/cache instead.");
|
|
1286
|
+
}
|
|
1287
|
+
// Server files importing a client library. Legal - a server component may
|
|
1288
|
+
// render a client component from a package - but the shape that costs an
|
|
1289
|
+
// afternoon is a file with no "use client" that only wraps them, so the
|
|
1290
|
+
// library's internals run on the server. Said, with the packages and the
|
|
1291
|
+
// importer, so the person reading can tell which it is.
|
|
1292
|
+
if (clientLibraryImports.length > 0) {
|
|
1293
|
+
console.log(`\n \u2139 ${clientLibraryImports.length} server ${clientLibraryImports.length === 1 ? "file imports" : "files import"} a client library: ` +
|
|
1294
|
+
clientLibraryImports
|
|
1295
|
+
.map((c) => `${c.file} (${c.packages.join(", ")}${c.from ? `; imported by ${c.from}` : ""})`)
|
|
1296
|
+
.join("; "));
|
|
1297
|
+
console.log(' Legal for a server component. A file that only wraps client components wants "use client" -\n' +
|
|
1298
|
+
" as shadcn ships it - so the server stops at the boundary.");
|
|
1299
|
+
}
|
|
1261
1300
|
// Written from the rows that were just printed rather than recomputed: the
|
|
1262
1301
|
// report and the terminal must not be able to disagree about what happened.
|
|
1263
1302
|
writeFileSync(join(outDir, REPORT_FILE), buildReport(results.map((r) => ({
|
|
@@ -1274,7 +1313,7 @@ async function prerenderAfterBundles(bundle, staticDir, assetsDir, knownActions
|
|
|
1274
1313
|
type: a.type,
|
|
1275
1314
|
reason: a.reason,
|
|
1276
1315
|
warning: a.warning ?? null,
|
|
1277
|
-
})), audited));
|
|
1316
|
+
})), audited, reactCache, clientLibraryImports));
|
|
1278
1317
|
const note = notes(results);
|
|
1279
1318
|
const counted = [...results, ...apis];
|
|
1280
1319
|
console.log(`
|
|
@@ -1571,6 +1610,76 @@ const components = new Map();
|
|
|
1571
1610
|
* a default component.
|
|
1572
1611
|
*/
|
|
1573
1612
|
const apiRoutes = new Map();
|
|
1613
|
+
/** Files beside the root layout served at the root as they are: robots.txt, a hand-written sitemap.xml, humans.txt. */
|
|
1614
|
+
let rootFiles = [];
|
|
1615
|
+
const METADATA_ROUTE_ID = "virtual:rsc-kit/metadata-route/";
|
|
1616
|
+
/**
|
|
1617
|
+
* robots.ts, sitemap.ts, llms.ts beside the root layout, each registered as
|
|
1618
|
+
* an api route at the file it stands for. The module the entry imports is
|
|
1619
|
+
* synthesised (metadataRoutesPlugin): the app's default export, formatted by
|
|
1620
|
+
* the engine, with the root layout's metadataBase for relative urls.
|
|
1621
|
+
*
|
|
1622
|
+
* Api routes, so nothing else is new: the build stores the ones that read
|
|
1623
|
+
* nothing per request and names the ones that do. Without middleware, on
|
|
1624
|
+
* purpose - these exist to be read by anyone, and a guard on the root would
|
|
1625
|
+
* otherwise 401 the crawler asking for robots.txt.
|
|
1626
|
+
*/
|
|
1627
|
+
function registerMetadataRoutes(appDir) {
|
|
1628
|
+
rootFiles = existsSync(appDir)
|
|
1629
|
+
? readdirSync(appDir)
|
|
1630
|
+
.filter((name) => ROOT_FILES.test(name))
|
|
1631
|
+
.sort()
|
|
1632
|
+
: [];
|
|
1633
|
+
for (const kind of Object.keys(METADATA_ROUTES)) {
|
|
1634
|
+
const source = ["ts", "tsx", "js", "mjs"]
|
|
1635
|
+
.map((ext) => join(appDir, `${kind}.${ext}`))
|
|
1636
|
+
.find((file) => existsSync(file));
|
|
1637
|
+
if (!source)
|
|
1638
|
+
continue;
|
|
1639
|
+
const { file } = METADATA_ROUTES[kind];
|
|
1640
|
+
if (rootFiles.includes(file)) {
|
|
1641
|
+
throw new Error(`[rsc-kit] app/${basename(source)} and app/${file} both answer /${file}. Keep one: the function, or the file as written.`);
|
|
1642
|
+
}
|
|
1643
|
+
const name = `app/${file}/route`;
|
|
1644
|
+
if (apiRoutes.has(name)) {
|
|
1645
|
+
throw new Error(`[rsc-kit] app/${basename(source)} and app/${file}/route.ts both answer /${file}. Keep one.`);
|
|
1646
|
+
}
|
|
1647
|
+
apiRoutes.set(name, {
|
|
1648
|
+
name,
|
|
1649
|
+
absPath: METADATA_ROUTE_ID + kind,
|
|
1650
|
+
methods: ["GET"],
|
|
1651
|
+
generated: { kind, file: source },
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
/** The module behind a synthesised metadata route. */
|
|
1656
|
+
function metadataRoutesPlugin() {
|
|
1657
|
+
return {
|
|
1658
|
+
name: "rsc-kit:metadata-routes",
|
|
1659
|
+
resolveId(id) {
|
|
1660
|
+
if (id.startsWith(METADATA_ROUTE_ID))
|
|
1661
|
+
return "\0" + id;
|
|
1662
|
+
},
|
|
1663
|
+
load(id) {
|
|
1664
|
+
if (!id.startsWith("\0" + METADATA_ROUTE_ID))
|
|
1665
|
+
return;
|
|
1666
|
+
const kind = id.slice(("\0" + METADATA_ROUTE_ID).length);
|
|
1667
|
+
const route = [...apiRoutes.values()].find((r) => r.generated?.kind === kind);
|
|
1668
|
+
if (!route?.generated)
|
|
1669
|
+
return;
|
|
1670
|
+
const rootLayout = components.get("app/layout")?.absPath ?? null;
|
|
1671
|
+
return [
|
|
1672
|
+
`import produce from ${JSON.stringify(route.generated.file)}`,
|
|
1673
|
+
rootLayout
|
|
1674
|
+
? `import * as __root from ${JSON.stringify(rootLayout)}`
|
|
1675
|
+
: "const __root = {}",
|
|
1676
|
+
`import { metadataResponse } from ${JSON.stringify(join(packageDir, "metadataRoutes"))}`,
|
|
1677
|
+
`export const GET = () => metadataResponse(${JSON.stringify(kind)}, produce, __root.metadata?.metadataBase ?? null)`,
|
|
1678
|
+
"",
|
|
1679
|
+
].join("\n");
|
|
1680
|
+
},
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1574
1683
|
function register(absPath) {
|
|
1575
1684
|
const name = componentName(absPath);
|
|
1576
1685
|
const existing = components.get(name);
|
|
@@ -1907,6 +2016,9 @@ import { prerenderedBeside } from ${JSON.stringify(join(packageDir, "files"))}
|
|
|
1907
2016
|
import { renderToReadableStream, decodeReply, loadServerAction } from '@vitejs/plugin-rsc/rsc'
|
|
1908
2017
|
import { isQuery, queryCacheControl, isQueryValidationError } from ${JSON.stringify(join(packageDir, "query"))}
|
|
1909
2018
|
import { isActionValidationError, isClientBuilt } from ${JSON.stringify(join(packageDir, "action"))}
|
|
2019
|
+
import { noteFallback as noteCaughtRead } from ${JSON.stringify(join(packageDir, "request"))}
|
|
2020
|
+
import { isOutdatedOptimizedDep, outdatedDepResponse } from ${JSON.stringify(join(packageDir, "devReload"))}
|
|
2021
|
+
import { sharedDepth } from ${JSON.stringify(join(packageDir, "routing"))}
|
|
1910
2022
|
import { Suspense, createElement, Fragment } from 'react'
|
|
1911
2023
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
1912
2024
|
${imports.join("\n")}
|
|
@@ -2549,6 +2661,38 @@ async function renderTree(
|
|
|
2549
2661
|
}
|
|
2550
2662
|
if (md.description != null) head.push(createElement('meta', { key: '__d', name: 'description', content: String(md.description) }))
|
|
2551
2663
|
|
|
2664
|
+
// robots is a string, or the object Next takes: index and follow as
|
|
2665
|
+
// their no- forms, the flags by name, the limits as name:value. googleBot
|
|
2666
|
+
// is the same shape for the googlebot tag. The object used to fall
|
|
2667
|
+
// through to the catch-all below as "[object Object]" - which no crawler
|
|
2668
|
+
// reads, on the one page that asked not to be indexed.
|
|
2669
|
+
const robotsContent = (value: unknown): string => {
|
|
2670
|
+
if (typeof value !== 'object' || value === null) return String(value)
|
|
2671
|
+
|
|
2672
|
+
const r = value as Record<string, unknown>
|
|
2673
|
+
const parts: string[] = []
|
|
2674
|
+
|
|
2675
|
+
if (r.index != null) parts.push(r.index ? 'index' : 'noindex')
|
|
2676
|
+
if (r.follow != null) parts.push(r.follow ? 'follow' : 'nofollow')
|
|
2677
|
+
for (const flag of ['noarchive', 'nosnippet', 'noimageindex', 'nocache', 'notranslate', 'indexifembedded', 'nositelinkssearchbox']) {
|
|
2678
|
+
if (r[flag]) parts.push(flag)
|
|
2679
|
+
}
|
|
2680
|
+
if (r.unavailable_after != null) parts.push('unavailable_after: ' + String(r.unavailable_after))
|
|
2681
|
+
for (const limit of ['max-video-preview', 'max-image-preview', 'max-snippet']) {
|
|
2682
|
+
if (r[limit] != null) parts.push(limit + ':' + String(r[limit]))
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
return parts.join(', ')
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
if (md.robots != null) {
|
|
2689
|
+
head.push(createElement('meta', { key: '__r', name: 'robots', content: robotsContent(md.robots) }))
|
|
2690
|
+
|
|
2691
|
+
const bot = typeof md.robots === 'object' ? (md.robots as { googleBot?: unknown }).googleBot : null
|
|
2692
|
+
|
|
2693
|
+
if (bot != null) head.push(createElement('meta', { key: '__rg', name: 'googlebot', content: robotsContent(bot) }))
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2552
2696
|
// og: and its relatives are PROPERTY, not name. Facebook's scraper - and
|
|
2553
2697
|
// Slack's, and LinkedIn's - reads only property=, so every og tag this
|
|
2554
2698
|
// used to emit with name= was invisible to the thing it existed for.
|
|
@@ -2653,7 +2797,7 @@ async function renderTree(
|
|
|
2653
2797
|
// meta tags rather than a meta tag by that name. A key at the top level
|
|
2654
2798
|
// still renders - the type no longer invites one, but an app written
|
|
2655
2799
|
// against the old shape must not silently lose its tags.
|
|
2656
|
-
const structured = new Set(['title', 'description', 'metadataBase', 'openGraph', 'twitter', 'icons', 'other'])
|
|
2800
|
+
const structured = new Set(['title', 'description', 'robots', 'metadataBase', 'openGraph', 'twitter', 'icons', 'other'])
|
|
2657
2801
|
const named = Object.entries(md).filter(([k]) => !structured.has(k))
|
|
2658
2802
|
const extra = Object.entries((md.other ?? {}) as Record<string, unknown>)
|
|
2659
2803
|
|
|
@@ -3506,12 +3650,22 @@ export async function handleRscPprShell(
|
|
|
3506
3650
|
// page, stored, with the build reporting success.
|
|
3507
3651
|
let renderFailure: string | undefined
|
|
3508
3652
|
|
|
3509
|
-
const noteFailure = (e: unknown): string | undefined => {
|
|
3653
|
+
const noteFailure = (e: unknown, info?: { componentStack?: string }): string | undefined => {
|
|
3510
3654
|
const digest = redirectDigest(e)
|
|
3511
3655
|
|
|
3512
3656
|
// A redirect is a classification here, not a failure.
|
|
3513
3657
|
if (digest) return digest
|
|
3514
3658
|
|
|
3659
|
+
// A read the server could not answer, caught at a boundary: the fallback
|
|
3660
|
+
// there is what gets stored. Noted with its component so the build can
|
|
3661
|
+
// say so on the route's line; the digest goes into the document for the
|
|
3662
|
+
// browser to recognise.
|
|
3663
|
+
if ((e as { digest?: string } | null)?.digest === 'rsc-kit:search-params-fallback') {
|
|
3664
|
+
const where = /at ([A-Z][\\w$]*)/.exec(info?.componentStack ?? '')?.[1]
|
|
3665
|
+
noteCaughtRead('useSearchParams()' + (where ? ' in ' + where : ''))
|
|
3666
|
+
return 'rsc-kit:search-params-fallback'
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3515
3669
|
// Every probe ends by aborting, so React reports that abort. Asked of the
|
|
3516
3670
|
// signal rather than matched against the message: a string test would be a
|
|
3517
3671
|
// guess about wording, and would quietly stop working when React changed it.
|
|
@@ -3624,6 +3778,18 @@ ${fallbackOrigin ? FALLBACK_CONSTS.replace("__ORIGIN__", JSON.stringify(fallback
|
|
|
3624
3778
|
let devHandler: ((request: Request) => Promise<Response | null>) | null = null
|
|
3625
3779
|
|
|
3626
3780
|
export default async function handler(request: Request): Promise<Response> {
|
|
3781
|
+
try {
|
|
3782
|
+
return await serve(request)
|
|
3783
|
+
} catch (error) {
|
|
3784
|
+
// Vite re-optimised a server pre-bundle under this render. The condition
|
|
3785
|
+
// is over already; the page is asked to load again.
|
|
3786
|
+
if (import.meta.env.DEV && isOutdatedOptimizedDep(error)) return outdatedDepResponse(request)
|
|
3787
|
+
|
|
3788
|
+
throw error
|
|
3789
|
+
}
|
|
3790
|
+
}
|
|
3791
|
+
|
|
3792
|
+
async function serve(request: Request): Promise<Response> {
|
|
3627
3793
|
installHostCallsOnce()
|
|
3628
3794
|
|
|
3629
3795
|
devHandler ??= createRscHandler({
|
|
@@ -3647,7 +3813,7 @@ export default async function handler(request: Request): Promise<Response> {
|
|
|
3647
3813
|
${NITRO_HANDLER_OPTIONS}${NITRO_PRERENDERED} maxActionBody: ${maxActionBody === undefined ? "undefined" : String(maxActionBody)},
|
|
3648
3814
|
})
|
|
3649
3815
|
|
|
3650
|
-
${fallbackOrigin ? FALLBACK_BODY : " return (await devHandler(request)) ?? (await notFound())\n"}}
|
|
3816
|
+
${fallbackOrigin ? FALLBACK_BODY : " return (await devHandler(request)) ?? (await notFound(request))\n"}}
|
|
3651
3817
|
|
|
3652
3818
|
/**
|
|
3653
3819
|
* The page for a url nothing answers.
|
|
@@ -3658,9 +3824,42 @@ ${fallbackOrigin ? FALLBACK_BODY : " return (await devHandler(request)) ?? (awa
|
|
|
3658
3824
|
*
|
|
3659
3825
|
* Without a not-found.tsx this is the string it always was.
|
|
3660
3826
|
*/
|
|
3661
|
-
async function notFound(): Promise<Response> {
|
|
3827
|
+
async function notFound(request: Request): Promise<Response> {
|
|
3662
3828
|
${notFoundComponent
|
|
3663
3829
|
? `
|
|
3830
|
+
// A navigation, not a document: answer with the not-found tree as a
|
|
3831
|
+
// payload, at the depth the client already holds, so the router renders
|
|
3832
|
+
// it in place - the layout stays, the url changes, nothing reloads. The
|
|
3833
|
+
// status is still 404; a payload is a payload whatever it says.
|
|
3834
|
+
if (request.headers.get('X-RSC')) {
|
|
3835
|
+
try {
|
|
3836
|
+
const chain = ${JSON.stringify(notFoundLayouts)}
|
|
3837
|
+
const from = sharedDepth(request.headers.get('X-RSC-Segments'), chain)
|
|
3838
|
+
const { rscPayload } = await handleRscPayload(
|
|
3839
|
+
${JSON.stringify(notFoundComponent)},
|
|
3840
|
+
{},
|
|
3841
|
+
${JSON.stringify(notFoundLayouts.map((component) => ({ component, props: {} })))},
|
|
3842
|
+
[],
|
|
3843
|
+
{},
|
|
3844
|
+
from,
|
|
3845
|
+
'/404',
|
|
3846
|
+
)
|
|
3847
|
+
|
|
3848
|
+
return new Response(rscPayload, {
|
|
3849
|
+
status: 404,
|
|
3850
|
+
headers: {
|
|
3851
|
+
'Content-Type': 'text/x-component; charset=utf-8',
|
|
3852
|
+
'X-RSC-Segment-Depth': String(from),
|
|
3853
|
+
'X-RSC-Layouts': chain.join(','),
|
|
3854
|
+
'Cache-Control': 'no-store',
|
|
3855
|
+
Vary: 'X-RSC',
|
|
3856
|
+
},
|
|
3857
|
+
})
|
|
3858
|
+
} catch {
|
|
3859
|
+
// Fall through to the document answer below.
|
|
3860
|
+
}
|
|
3861
|
+
}
|
|
3862
|
+
|
|
3664
3863
|
try {
|
|
3665
3864
|
const { htmlStream } = await handleRscHtmlStream(
|
|
3666
3865
|
${JSON.stringify(notFoundComponent)},
|
|
@@ -3689,11 +3888,15 @@ async function notFound(): Promise<Response> {
|
|
|
3689
3888
|
}
|
|
3690
3889
|
function generateEntrySsr() {
|
|
3691
3890
|
const devUrls = join(packageDir, "devUrls");
|
|
3891
|
+
const request = join(packageDir, "request");
|
|
3892
|
+
const fallbackReport = join(packageDir, "js/fallbackReport");
|
|
3692
3893
|
return `// GENERATED by rscKit() — do not edit.
|
|
3693
3894
|
import { createFromReadableStream } from '@vitejs/plugin-rsc/ssr'
|
|
3694
3895
|
import { renderToReadableStream, resume } from 'react-dom/server.edge'
|
|
3695
3896
|
import { prerender } from 'react-dom/static.edge'
|
|
3696
3897
|
import { rewriteViteDevUrlStream } from ${JSON.stringify(devUrls)}
|
|
3898
|
+
import { noteFallback } from ${JSON.stringify(request)}
|
|
3899
|
+
import { cancelledByConsumer, caughtByLoading } from ${JSON.stringify(fallbackReport)}
|
|
3697
3900
|
|
|
3698
3901
|
// Set only by the dev server. @vitejs/plugin-rsc emits its bootstrap and CSS
|
|
3699
3902
|
// links root-relative in dev, which would send the browser to the host for
|
|
@@ -3724,12 +3927,28 @@ export async function handleSsr(
|
|
|
3724
3927
|
// The query-string fallback is the designed path for a stored page, and
|
|
3725
3928
|
// its digest is what lets the client tell it from a fault on hydration;
|
|
3726
3929
|
// returned here so React writes it into the document.
|
|
3727
|
-
onError: onError ?? ((error: unknown) => {
|
|
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
|
|
3728
3934
|
const digest = (error as { digest?: string } | null)?.digest
|
|
3729
3935
|
if (digest === 'rsc-kit:search-params-fallback') {
|
|
3730
|
-
//
|
|
3731
|
-
//
|
|
3732
|
-
|
|
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
|
+
}
|
|
3733
3952
|
return digest
|
|
3734
3953
|
}
|
|
3735
3954
|
console.error('[rsc-kit:ssr]', error)
|
|
@@ -3769,8 +3988,14 @@ export async function handleSsrPrerender(
|
|
|
3769
3988
|
bootstrapScriptContent,
|
|
3770
3989
|
nonce: options.nonce,
|
|
3771
3990
|
signal: options.signal,
|
|
3772
|
-
// Aborting is how this ends, so React's report of it is not news.
|
|
3773
|
-
|
|
3991
|
+
// Aborting is how this ends, so React's report of it is not news. A read
|
|
3992
|
+
// caught at a boundary is: the build attaches it to the route.
|
|
3993
|
+
onError: (error: unknown, info?: { componentStack?: string }) => {
|
|
3994
|
+
if ((error as { digest?: string } | null)?.digest !== 'rsc-kit:search-params-fallback') return
|
|
3995
|
+
const where = /at ([A-Z][\\w$]*)/.exec(info?.componentStack ?? '')?.[1]
|
|
3996
|
+
noteFallback('useSearchParams()' + (where ? ' in ' + where : ''))
|
|
3997
|
+
return 'rsc-kit:search-params-fallback'
|
|
3998
|
+
},
|
|
3774
3999
|
})
|
|
3775
4000
|
|
|
3776
4001
|
return {
|
|
@@ -4062,6 +4287,197 @@ function extendableClientReferences() {
|
|
|
4062
4287
|
},
|
|
4063
4288
|
};
|
|
4064
4289
|
}
|
|
4290
|
+
/**
|
|
4291
|
+
* The worker the dev server serves at /sw.js: it removes the one a production
|
|
4292
|
+
* run left on this origin, and the caches with it, then reloads each page it
|
|
4293
|
+
* was controlling so they load uncontrolled. Nothing else runs in development.
|
|
4294
|
+
*/
|
|
4295
|
+
export const DEV_WORKER = `self.addEventListener('install', () => self.skipWaiting())
|
|
4296
|
+
self.addEventListener('activate', (event) => {
|
|
4297
|
+
event.waitUntil((async () => {
|
|
4298
|
+
const keys = await caches.keys()
|
|
4299
|
+
await Promise.all(keys.filter((k) => k.startsWith('rsc-kit-')).map((k) => caches.delete(k)))
|
|
4300
|
+
await self.clients.claim()
|
|
4301
|
+
const pages = await self.clients.matchAll({ type: 'window' })
|
|
4302
|
+
await self.registration.unregister()
|
|
4303
|
+
for (const page of pages) page.navigate(page.url).catch(() => {})
|
|
4304
|
+
})())
|
|
4305
|
+
})
|
|
4306
|
+
`;
|
|
4307
|
+
/**
|
|
4308
|
+
* "use ssr" modules, rewritten for the server-components environment into
|
|
4309
|
+
* proxies that call the real module in the ssr environment. See useSsr.ts.
|
|
4310
|
+
*
|
|
4311
|
+
* Before every other transform, so the cross-environment import this emits
|
|
4312
|
+
* is still ahead of plugin-rsc's handling of it - and so the module's own
|
|
4313
|
+
* imports, react-dom/server among them, are never resolved here at all.
|
|
4314
|
+
*/
|
|
4315
|
+
function useSsrModules() {
|
|
4316
|
+
return {
|
|
4317
|
+
name: "rsc-kit:use-ssr",
|
|
4318
|
+
enforce: "pre",
|
|
4319
|
+
applyToEnvironment: (environment) => environment.name === "rsc",
|
|
4320
|
+
transform(code, id) {
|
|
4321
|
+
if (!code.includes("use ssr"))
|
|
4322
|
+
return;
|
|
4323
|
+
try {
|
|
4324
|
+
const proxy = ssrProxyModule(code, id);
|
|
4325
|
+
return proxy === null ? undefined : { code: proxy, map: null };
|
|
4326
|
+
}
|
|
4327
|
+
catch (error) {
|
|
4328
|
+
if (error instanceof UseSsrError)
|
|
4329
|
+
this.error(error.message);
|
|
4330
|
+
throw error;
|
|
4331
|
+
}
|
|
4332
|
+
},
|
|
4333
|
+
};
|
|
4334
|
+
}
|
|
4335
|
+
/**
|
|
4336
|
+
* react-dom/server, imported where server components render, becomes a
|
|
4337
|
+
* module that throws the fix instead of React's refusal - naming the app
|
|
4338
|
+
* file that imported it (through @react-email/render, say) and the
|
|
4339
|
+
* directive that moves it. The build says the same once, as a warning.
|
|
4340
|
+
*/
|
|
4341
|
+
const RENDERER_STUB = "\0rsc-kit:react-dom-server?from=";
|
|
4342
|
+
function serverRendererInRsc() {
|
|
4343
|
+
const warned = new Set();
|
|
4344
|
+
const appImporter = (ctx, importer) => {
|
|
4345
|
+
let current = importer;
|
|
4346
|
+
for (let hop = 0; current && hop < 12; hop++) {
|
|
4347
|
+
if (!current.includes("/node_modules/"))
|
|
4348
|
+
return relative(process.cwd(), current.split("?")[0]);
|
|
4349
|
+
const next = ctx.environment.mode === "build"
|
|
4350
|
+
? ctx.getModuleInfo(current)?.importers[0]
|
|
4351
|
+
: (ctx.environment.moduleGraph
|
|
4352
|
+
?.getModuleById(current)
|
|
4353
|
+
?.importers.values()
|
|
4354
|
+
.next().value?.id ?? undefined);
|
|
4355
|
+
if (!next)
|
|
4356
|
+
break;
|
|
4357
|
+
current = next;
|
|
4358
|
+
}
|
|
4359
|
+
return importer ? relative(process.cwd(), importer.split("?")[0]) : null;
|
|
4360
|
+
};
|
|
4361
|
+
return {
|
|
4362
|
+
name: "rsc-kit:server-renderer",
|
|
4363
|
+
applyToEnvironment: (environment) => environment.name === "rsc",
|
|
4364
|
+
resolveId(source, importer) {
|
|
4365
|
+
if (!SERVER_RENDERER.test(source))
|
|
4366
|
+
return;
|
|
4367
|
+
return RENDERER_STUB + encodeURIComponent(importer ?? "");
|
|
4368
|
+
},
|
|
4369
|
+
load(id) {
|
|
4370
|
+
if (!id.startsWith(RENDERER_STUB))
|
|
4371
|
+
return;
|
|
4372
|
+
const importer = decodeURIComponent(id.slice(RENDERER_STUB.length)) || null;
|
|
4373
|
+
const message = serverRendererMessage(appImporter(this, importer));
|
|
4374
|
+
if (this.environment.mode === "build" && !warned.has(message)) {
|
|
4375
|
+
warned.add(message);
|
|
4376
|
+
this.warn(message);
|
|
4377
|
+
}
|
|
4378
|
+
return `throw new Error(${JSON.stringify(message)});\n`;
|
|
4379
|
+
},
|
|
4380
|
+
};
|
|
4381
|
+
}
|
|
4382
|
+
/**
|
|
4383
|
+
* `tsc --noEmit` on the project's own tsconfig, the way `check` runs it.
|
|
4384
|
+
*
|
|
4385
|
+
* Through package.json rather than a subpath: TypeScript's exports map does
|
|
4386
|
+
* not expose bin/tsc, and the bin field is where the name lives. Its output
|
|
4387
|
+
* is the message, because that is what the developer would have read.
|
|
4388
|
+
*/
|
|
4389
|
+
export function typecheckProject(root) {
|
|
4390
|
+
const tsconfig = join(root, "tsconfig.json");
|
|
4391
|
+
if (!existsSync(tsconfig))
|
|
4392
|
+
return { ran: false, because: "no tsconfig" };
|
|
4393
|
+
let tsc;
|
|
4394
|
+
try {
|
|
4395
|
+
const fromApp = createRequire(join(root, "package.json"));
|
|
4396
|
+
const manifest = fromApp.resolve("typescript/package.json");
|
|
4397
|
+
const bin = fromApp(manifest)
|
|
4398
|
+
.bin;
|
|
4399
|
+
const relative = typeof bin === "string" ? bin : bin?.tsc;
|
|
4400
|
+
if (!relative)
|
|
4401
|
+
return { ran: false, because: "no typescript" };
|
|
4402
|
+
tsc = join(dirname(manifest), relative);
|
|
4403
|
+
}
|
|
4404
|
+
catch {
|
|
4405
|
+
return { ran: false, because: "no typescript" };
|
|
4406
|
+
}
|
|
4407
|
+
const started = Date.now();
|
|
4408
|
+
const script = /\.[cm]?js$/.test(tsc) || !/\.\w+$/.test(tsc);
|
|
4409
|
+
const result = spawnSync(script ? process.execPath : tsc,
|
|
4410
|
+
// Plain lines, not coloured ones: they go into a build error, and the
|
|
4411
|
+
// test that reads them is a reader too.
|
|
4412
|
+
[...(script ? [tsc] : []), "--noEmit", "--pretty", "false", "-p", tsconfig], { cwd: root, encoding: "utf-8", maxBuffer: 64 * 1024 * 1024 });
|
|
4413
|
+
if (result.status === 0)
|
|
4414
|
+
return { ran: true, ok: true, seconds: (Date.now() - started) / 1000 };
|
|
4415
|
+
return {
|
|
4416
|
+
ran: true,
|
|
4417
|
+
ok: false,
|
|
4418
|
+
output: `${result.stdout ?? ""}${result.stderr ?? ""}`.trim(),
|
|
4419
|
+
};
|
|
4420
|
+
}
|
|
4421
|
+
/**
|
|
4422
|
+
* The project's typecheck, as part of the build.
|
|
4423
|
+
*
|
|
4424
|
+
* Runs once, in the first environment to start, after the route types have
|
|
4425
|
+
* been written by the config hook - so a link to a route that does not exist
|
|
4426
|
+
* is an error here and not a 404 found after deploying. Skipped where the
|
|
4427
|
+
* project is not doing this checking at all: no tsconfig.json, or no
|
|
4428
|
+
* typescript to run.
|
|
4429
|
+
*/
|
|
4430
|
+
function typecheckPlugin() {
|
|
4431
|
+
let ran = false;
|
|
4432
|
+
return {
|
|
4433
|
+
name: "rsc-kit:typecheck",
|
|
4434
|
+
apply: "build",
|
|
4435
|
+
buildStart() {
|
|
4436
|
+
if (ran || !typecheck || isWatch)
|
|
4437
|
+
return;
|
|
4438
|
+
ran = true;
|
|
4439
|
+
const outcome = typecheckProject(projectRoot);
|
|
4440
|
+
if (!outcome.ran)
|
|
4441
|
+
return;
|
|
4442
|
+
if (outcome.ok) {
|
|
4443
|
+
log(`typecheck: ok (${outcome.seconds.toFixed(1)}s)`);
|
|
4444
|
+
return;
|
|
4445
|
+
}
|
|
4446
|
+
throw new Error("[rsc-kit] The typecheck failed, so the build stops here.\n\n" +
|
|
4447
|
+
outcome.output +
|
|
4448
|
+
"\n\nrscKit({ typecheck: false }) builds without it.");
|
|
4449
|
+
},
|
|
4450
|
+
};
|
|
4451
|
+
}
|
|
4452
|
+
/**
|
|
4453
|
+
* Which server files import a client library, read off the rsc environment's
|
|
4454
|
+
* module graph once it is built. plugin-rsc classifies the client packages
|
|
4455
|
+
* (every package with react among its peers) and excludes them from the
|
|
4456
|
+
* server optimizers; that list is the one used here, minus this package -
|
|
4457
|
+
* a server component importing Link is the norm - and plugin-rsc's own.
|
|
4458
|
+
*/
|
|
4459
|
+
function clientImportsAudit() {
|
|
4460
|
+
return {
|
|
4461
|
+
name: "rsc-kit:client-imports",
|
|
4462
|
+
apply: "build",
|
|
4463
|
+
applyToEnvironment: (environment) => environment.name === "rsc",
|
|
4464
|
+
buildEnd() {
|
|
4465
|
+
const excluded = resolvedConfig?.environments?.rsc?.optimizeDeps?.exclude ?? [];
|
|
4466
|
+
const clientPackages = excluded.filter((name) => !name.startsWith("@vitejs/plugin-rsc"));
|
|
4467
|
+
if (clientPackages.length === 0)
|
|
4468
|
+
return;
|
|
4469
|
+
clientLibraryImports = serverImportsOfClientPackages({
|
|
4470
|
+
moduleIds: () => this.getModuleIds(),
|
|
4471
|
+
importedIds: (id) => this.getModuleInfo(id)?.importedIds ?? [],
|
|
4472
|
+
importers: (id) => this.getModuleInfo(id)?.importers ?? [],
|
|
4473
|
+
}, {
|
|
4474
|
+
sourceDir,
|
|
4475
|
+
clientPackages,
|
|
4476
|
+
ignore: [PACKAGE_NAME, "server-only", "client-only"],
|
|
4477
|
+
});
|
|
4478
|
+
},
|
|
4479
|
+
};
|
|
4480
|
+
}
|
|
4065
4481
|
export function rscKit(options = {}) {
|
|
4066
4482
|
resolvePaths(options);
|
|
4067
4483
|
const routesPlugin = {
|
|
@@ -4070,8 +4486,13 @@ export function rscKit(options = {}) {
|
|
|
4070
4486
|
if (!existsSync(appDir)) {
|
|
4071
4487
|
throw new Error(`[rsc-kit] No app directory at ${appDir} — nothing to build.`);
|
|
4072
4488
|
}
|
|
4489
|
+
// Both maps, not one: the config hook runs again when the config
|
|
4490
|
+
// changes under a dev server, and a route.ts or a synthesised
|
|
4491
|
+
// robots.txt left over from the previous run read as a duplicate.
|
|
4073
4492
|
components.clear();
|
|
4493
|
+
apiRoutes.clear();
|
|
4074
4494
|
discover(appDir);
|
|
4495
|
+
registerMetadataRoutes(appDir);
|
|
4075
4496
|
// Silent when it worked. The names were printed on every dev start and
|
|
4076
4497
|
// every build — thirty of them for a middling app, above the output that
|
|
4077
4498
|
// actually says something, and the build's classification table lists
|
|
@@ -4160,9 +4581,6 @@ export function rscKit(options = {}) {
|
|
|
4160
4581
|
*/
|
|
4161
4582
|
define: {
|
|
4162
4583
|
"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
4584
|
__RSC_OFFLINE__: JSON.stringify(offline),
|
|
4167
4585
|
},
|
|
4168
4586
|
/*
|
|
@@ -4187,6 +4605,36 @@ export function rscKit(options = {}) {
|
|
|
4187
4605
|
*/
|
|
4188
4606
|
optimizeDeps: {
|
|
4189
4607
|
exclude: [PACKAGE_NAME, ...(_config.optimizeDeps?.exclude ?? [])],
|
|
4608
|
+
// Every "use client" file, so the browser's dependencies are all
|
|
4609
|
+
// found at startup. The client entry reaches only the engine and
|
|
4610
|
+
// React; the app's client components arrive through payloads, page
|
|
4611
|
+
// by page, and a dependency first seen on the third page visited
|
|
4612
|
+
// re-optimised every pre-bundle under a running page - two Reacts,
|
|
4613
|
+
// a blank document, then Vite's own reload. See clientEntries.
|
|
4614
|
+
// Naming entries replaces the input, so the browser entry is named
|
|
4615
|
+
// too, or the engine and React would be the ones left out.
|
|
4616
|
+
entries: [
|
|
4617
|
+
...(Array.isArray(_config.optimizeDeps?.entries)
|
|
4618
|
+
? _config.optimizeDeps.entries
|
|
4619
|
+
: _config.optimizeDeps?.entries
|
|
4620
|
+
? [_config.optimizeDeps.entries]
|
|
4621
|
+
: []),
|
|
4622
|
+
// Entries are globs: a route group's parentheses, or a bracket
|
|
4623
|
+
// in a dynamic segment, would otherwise read as pattern syntax
|
|
4624
|
+
// and match nothing - and the scanner would crawl nothing, quietly.
|
|
4625
|
+
...[
|
|
4626
|
+
join(genDir, "entry.browser.tsx"),
|
|
4627
|
+
...engineClientEntries(),
|
|
4628
|
+
...clientEntries(sourceDir),
|
|
4629
|
+
].map((file) => file.replace(/[()[\]{}*?!+@]/g, "\\$&")),
|
|
4630
|
+
],
|
|
4631
|
+
rolldownOptions: {
|
|
4632
|
+
...(_config.optimizeDeps?.rolldownOptions ?? {}),
|
|
4633
|
+
plugins: [
|
|
4634
|
+
clientScanPlugin(),
|
|
4635
|
+
...arrayOf(_config.optimizeDeps?.rolldownOptions?.plugins),
|
|
4636
|
+
],
|
|
4637
|
+
},
|
|
4190
4638
|
},
|
|
4191
4639
|
// Public URL for browser-facing client assets, and a BUILD concern
|
|
4192
4640
|
// only: it says where the built files will be served from.
|
|
@@ -4279,6 +4727,28 @@ export function rscKit(options = {}) {
|
|
|
4279
4727
|
// and the console a 404 for a file that is right there.
|
|
4280
4728
|
server.middlewares.use((req, res, next) => {
|
|
4281
4729
|
const url = (req.url ?? "").split("?")[0];
|
|
4730
|
+
// A worker registered by a production run on this origin - `bun run
|
|
4731
|
+
// start` on the port the dev server uses next - outlives that run and
|
|
4732
|
+
// answers the dev server's documents from its cache: a stored page is
|
|
4733
|
+
// cache-first, so a refresh brings back yesterday's document with
|
|
4734
|
+
// yesterday's module hashes, and a 504 for each. The browser fetches
|
|
4735
|
+
// /sw.js again on every navigation to look for an update; in
|
|
4736
|
+
// development that fetch gets a worker whose only job is to remove
|
|
4737
|
+
// itself, its caches, and reload the pages it controlled.
|
|
4738
|
+
// robots.txt, a hand-written sitemap.xml, humans.txt: beside the root
|
|
4739
|
+
// layout, served at the root. The build copies them beside the client
|
|
4740
|
+
// output; here they are read from app/.
|
|
4741
|
+
if (url.startsWith("/") && rootFiles.includes(url.slice(1))) {
|
|
4742
|
+
res.setHeader("Content-Type", rootFileType(url.slice(1)));
|
|
4743
|
+
res.end(readFileSync(join(sourceDir, "app", url.slice(1))));
|
|
4744
|
+
return;
|
|
4745
|
+
}
|
|
4746
|
+
if (url === "/sw.js") {
|
|
4747
|
+
res.setHeader("Content-Type", "text/javascript");
|
|
4748
|
+
res.setHeader("Cache-Control", "no-store");
|
|
4749
|
+
res.end(DEV_WORKER);
|
|
4750
|
+
return;
|
|
4751
|
+
}
|
|
4282
4752
|
if (webManifestOptions && url === MANIFEST_PATH) {
|
|
4283
4753
|
res.setHeader("Content-Type", "application/manifest+json");
|
|
4284
4754
|
res.end(webManifest(webManifestOptions));
|
|
@@ -4506,7 +4976,12 @@ export function rscKit(options = {}) {
|
|
|
4506
4976
|
clientChunks: (meta) => meta.normalizedId,
|
|
4507
4977
|
...actionEncryptionKey(),
|
|
4508
4978
|
}),
|
|
4979
|
+
useSsrModules(),
|
|
4980
|
+
serverRendererInRsc(),
|
|
4981
|
+
metadataRoutesPlugin(),
|
|
4509
4982
|
extendableClientReferences(),
|
|
4983
|
+
typecheckPlugin(),
|
|
4984
|
+
clientImportsAudit(),
|
|
4510
4985
|
routesPlugin,
|
|
4511
4986
|
];
|
|
4512
4987
|
}
|