@rsc-kit/core 0.18.0 → 0.19.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/action.d.ts +16 -2
- package/dist/action.js +21 -12
- package/dist/action.js.map +1 -1
- package/dist/apiPrerender.js +24 -6
- package/dist/apiPrerender.js.map +1 -1
- package/dist/clientPackages.d.ts +30 -0
- package/dist/clientPackages.js +234 -0
- package/dist/clientPackages.js.map +1 -0
- package/dist/compress.d.ts +12 -0
- package/dist/compress.js +134 -0
- package/dist/compress.js.map +1 -0
- package/dist/compressRuntime.d.ts +8 -0
- package/dist/compressRuntime.js +62 -0
- package/dist/compressRuntime.js.map +1 -0
- package/dist/files.d.ts +20 -0
- package/dist/files.js +38 -0
- package/dist/files.js.map +1 -1
- package/dist/formSubmit.d.ts +1 -0
- package/dist/formSubmit.js +14 -0
- package/dist/formSubmit.js.map +1 -0
- package/dist/host.d.ts +23 -0
- package/dist/host.js +175 -31
- package/dist/host.js.map +1 -1
- package/dist/hostCalls.d.ts +15 -0
- package/dist/hostCalls.js +75 -8
- package/dist/hostCalls.js.map +1 -1
- package/dist/js/Form.d.ts +19 -2
- package/dist/js/Form.js +110 -83
- package/dist/js/Form.js.map +1 -1
- 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/createViteRscApp.d.ts +1 -0
- package/dist/js/createViteRscApp.js +34 -5
- package/dist/js/createViteRscApp.js.map +1 -1
- package/dist/js/errors.d.ts +3 -1
- package/dist/js/errors.js +26 -3
- package/dist/js/errors.js.map +1 -1
- package/dist/js/fallbackReport.js +10 -8
- package/dist/js/fallbackReport.js.map +1 -1
- package/dist/js/formEncoding.d.ts +72 -3
- package/dist/js/formEncoding.js +284 -20
- package/dist/js/formEncoding.js.map +1 -1
- package/dist/js/navigate.d.ts +3 -0
- package/dist/js/navigate.js +56 -6
- package/dist/js/navigate.js.map +1 -1
- package/dist/js/updateStore.js +8 -2
- package/dist/js/updateStore.js.map +1 -1
- package/dist/js/useEvents.js +9 -7
- package/dist/js/useEvents.js.map +1 -1
- package/dist/js/usePolling.js +38 -37
- package/dist/js/usePolling.js.map +1 -1
- package/dist/openapi.d.ts +74 -0
- package/dist/openapi.js +172 -0
- package/dist/openapi.js.map +1 -0
- package/dist/prerender.js +1 -0
- package/dist/prerender.js.map +1 -1
- package/dist/redirect.d.ts +2 -2
- package/dist/redirect.js.map +1 -1
- package/dist/request.d.ts +56 -2
- package/dist/request.js +68 -4
- package/dist/request.js.map +1 -1
- package/dist/routeSchema.d.ts +49 -0
- package/dist/routeSchema.js.map +1 -1
- package/dist/routes.d.ts +15 -1
- package/dist/routes.js.map +1 -1
- package/dist/testing.d.ts +22 -0
- package/dist/testing.js +83 -5
- package/dist/testing.js.map +1 -1
- package/dist/vite.d.ts +80 -1
- package/dist/vite.js +625 -82
- package/dist/vite.js.map +1 -1
- package/package.json +7 -3
package/dist/vite.js
CHANGED
|
@@ -32,6 +32,7 @@ import { ownHosts } from "./hostRouting.js";
|
|
|
32
32
|
import { unrollBarrelImports } from "./barrelImports.js";
|
|
33
33
|
import { serverRendererMessage, SERVER_RENDERER, ssrProxyModule, UseSsrError, } from "./useSsr.js";
|
|
34
34
|
import { httpHostCalls } from "./hostCalls.js";
|
|
35
|
+
import { clientPackages, importsServerRenderer, packageDir as installedPackageDir, packageEntryInGraph } from "./clientPackages.js";
|
|
35
36
|
// Resolved once per rscKit() call. One build runs in one process, so these are
|
|
36
37
|
// module state rather than threaded through every helper.
|
|
37
38
|
let projectRoot;
|
|
@@ -42,6 +43,60 @@ let resolvedConfig = null;
|
|
|
42
43
|
let clientLibraryImports = [];
|
|
43
44
|
/** Modules a runtime provides and no bundle should try to carry. */
|
|
44
45
|
const RUNTIME_BUILTINS = ["bun", /^bun:/];
|
|
46
|
+
/**
|
|
47
|
+
* Packages the server bundles import rather than inline, by default.
|
|
48
|
+
*
|
|
49
|
+
* Each of these ships a native binary, spawns one, or reads files relative to
|
|
50
|
+
* its own location - none of which survives being rolled into a bundle. Left
|
|
51
|
+
* external, Nitro traces them into .output/server/node_modules with their
|
|
52
|
+
* binaries, and the built server imports them the way the package expects.
|
|
53
|
+
* Next keeps the same list under serverExternalPackages, for the same reason.
|
|
54
|
+
* `rscKit({ serverExternalPackages })` adds to it.
|
|
55
|
+
*/
|
|
56
|
+
/**
|
|
57
|
+
* Side-effect polyfills a dependency checks for at module evaluation, which
|
|
58
|
+
* the generated entry imports before anything else when the project has
|
|
59
|
+
* them. Bundler chunking does not keep an external import's place.
|
|
60
|
+
*/
|
|
61
|
+
const FIRST_POLYFILLS = ["reflect-metadata"];
|
|
62
|
+
const DEFAULT_SERVER_EXTERNALS = [
|
|
63
|
+
"sharp",
|
|
64
|
+
"canvas",
|
|
65
|
+
"bcrypt",
|
|
66
|
+
"argon2",
|
|
67
|
+
"@node-rs/argon2",
|
|
68
|
+
"@node-rs/bcrypt",
|
|
69
|
+
"better-sqlite3",
|
|
70
|
+
"sqlite3",
|
|
71
|
+
"libsql",
|
|
72
|
+
"@libsql/client",
|
|
73
|
+
"@prisma/client",
|
|
74
|
+
"prisma",
|
|
75
|
+
"puppeteer",
|
|
76
|
+
"puppeteer-core",
|
|
77
|
+
"playwright",
|
|
78
|
+
"playwright-core",
|
|
79
|
+
"jsdom",
|
|
80
|
+
"node-pty",
|
|
81
|
+
"onnxruntime-node",
|
|
82
|
+
"@sentry/profiling-node",
|
|
83
|
+
"pdfkit",
|
|
84
|
+
"mongodb",
|
|
85
|
+
"oslo",
|
|
86
|
+
"@resvg/resvg-js",
|
|
87
|
+
"@napi-rs/canvas",
|
|
88
|
+
// Not native: a polyfill that must run before anything reads Reflect.getMetadata.
|
|
89
|
+
// tsyringe, typeorm and inversify check for it at module evaluation, and a
|
|
90
|
+
// bundler that splits them into their own chunk hoists that chunk's evaluation
|
|
91
|
+
// above the polyfill's - so the check fails in a bundle that has the polyfill
|
|
92
|
+
// in it. External, the import statement stays where the source put it, and
|
|
93
|
+
// Node runs it first.
|
|
94
|
+
"reflect-metadata",
|
|
95
|
+
];
|
|
96
|
+
/** A package name as a rollup external: the package and every subpath of it. */
|
|
97
|
+
function externalPackage(name) {
|
|
98
|
+
return new RegExp("^" + name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "(?:/|$)");
|
|
99
|
+
}
|
|
45
100
|
function arrayOf(value) {
|
|
46
101
|
return value == null ? [] : Array.isArray(value) ? value : [value];
|
|
47
102
|
}
|
|
@@ -115,6 +170,8 @@ let siteHosts = [];
|
|
|
115
170
|
let hostsOption = [];
|
|
116
171
|
let barrelImports = true;
|
|
117
172
|
let identify = true;
|
|
173
|
+
/** Resolved from RscKitOptions.openapi; null when off. */
|
|
174
|
+
let openapi = null;
|
|
118
175
|
let foundAssets = {
|
|
119
176
|
favicon: null,
|
|
120
177
|
icons: [],
|
|
@@ -177,6 +234,12 @@ function fileHostActions(root) {
|
|
|
177
234
|
return {};
|
|
178
235
|
try {
|
|
179
236
|
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
237
|
+
// An empty list is an empty map. PHP's json_encode writes an empty
|
|
238
|
+
// array as [] whatever it was declared as, so a backend with no
|
|
239
|
+
// actions yet wrote exactly that - and the first `dev` of a fresh
|
|
240
|
+
// install refused to start over a file that said, correctly, "none".
|
|
241
|
+
if (Array.isArray(parsed) && parsed.length === 0)
|
|
242
|
+
return {};
|
|
180
243
|
if (parsed === null ||
|
|
181
244
|
typeof parsed !== "object" ||
|
|
182
245
|
Array.isArray(parsed)) {
|
|
@@ -202,16 +265,23 @@ function fileHostActions(root) {
|
|
|
202
265
|
* two cannot drift.
|
|
203
266
|
*/
|
|
204
267
|
function aliasEntries() {
|
|
268
|
+
const entries = [
|
|
269
|
+
// A library written for Next reads the request through next/headers -
|
|
270
|
+
// Vercel's flags SDK does, through `flags/next` - and headers() and
|
|
271
|
+
// cookies() here have the same names and shapes, with one object per
|
|
272
|
+
// request, which is what its per-request dedupe keys on. Answered by
|
|
273
|
+
// this package, so such a library runs unchanged and without Next.
|
|
274
|
+
{ find: /^next\/headers$/, replacement: join(packageDir, "request") },
|
|
275
|
+
];
|
|
205
276
|
if (!packageAlias)
|
|
206
|
-
return
|
|
277
|
+
return entries;
|
|
207
278
|
if (existsSync(join(projectRoot, "node_modules", packageAlias)))
|
|
208
|
-
return
|
|
209
|
-
|
|
210
|
-
{
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
];
|
|
279
|
+
return entries;
|
|
280
|
+
entries.push({
|
|
281
|
+
find: new RegExp("^" + packageAlias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "/(.*)$"),
|
|
282
|
+
replacement: join(packageDir, "js") + "/$1",
|
|
283
|
+
});
|
|
284
|
+
return entries;
|
|
215
285
|
}
|
|
216
286
|
function resolvePaths(options) {
|
|
217
287
|
projectRoot = resolve(options.projectRoot || process.env.RSC_PROJECT_ROOT || process.cwd());
|
|
@@ -280,6 +350,12 @@ function resolvePaths(options) {
|
|
|
280
350
|
hostsOption = options.hosts ?? [];
|
|
281
351
|
barrelImports = options.barrelImports !== false;
|
|
282
352
|
identify = options.identify !== false;
|
|
353
|
+
openapi = options.openapi
|
|
354
|
+
? (() => {
|
|
355
|
+
const { path, ...document } = typeof options.openapi === "object" ? options.openapi : {};
|
|
356
|
+
return { path: path || "/openapi.json", document };
|
|
357
|
+
})()
|
|
358
|
+
: null;
|
|
283
359
|
inlineStylesheets = options.inlineStylesheets ?? "auto";
|
|
284
360
|
maxActionBody = options.maxActionBody;
|
|
285
361
|
// One place, and it is the file. A plugin option as well would be the same
|
|
@@ -724,7 +800,32 @@ self.addEventListener('install', (event) => {
|
|
|
724
800
|
// it is served by the host out of the prerendered directory, which this
|
|
725
801
|
// worker cannot see. Added here so it is in the cache before it is
|
|
726
802
|
// needed, which is the only moment it cannot be fetched.
|
|
727
|
-
.then((cache) => cache.addAll(OFFLINE_URL ? [...PRECACHE, OFFLINE_URL] : PRECACHE))
|
|
803
|
+
.then((cache) => cache.addAll(OFFLINE_URL ? [...PRECACHE, OFFLINE_URL] : PRECACHE).then(() => cache))
|
|
804
|
+
// And the payload each precached page boots from. A document alone is
|
|
805
|
+
// markup that never hydrates: the client fetches its payload on boot,
|
|
806
|
+
// and a precached / rendered offline and stayed inert because that
|
|
807
|
+
// request had nothing cached to answer it.
|
|
808
|
+
.then((cache) =>
|
|
809
|
+
Promise.all(
|
|
810
|
+
PRECACHE.filter((url) => !/\\.[a-z0-9]+$/i.test(url))
|
|
811
|
+
.concat(OFFLINE_URL ? [OFFLINE_URL] : [])
|
|
812
|
+
.map((url) => {
|
|
813
|
+
const warm = new Request(url, { headers: { 'X-RSC': '1' } })
|
|
814
|
+
|
|
815
|
+
return fetch(warm)
|
|
816
|
+
.then((payload) => {
|
|
817
|
+
if (mayStore(payload)) return cache.put(keyFor(warm), payload)
|
|
818
|
+
|
|
819
|
+
// Said, not swallowed: a page whose payload cannot be kept
|
|
820
|
+
// is one that will render offline and never hydrate, and
|
|
821
|
+
// the reason - a no-store from a guard above it, usually -
|
|
822
|
+
// is only visible here.
|
|
823
|
+
console.warn('[rsc-kit] offline: the payload for ' + url + ' was not stored (' + (payload.headers.get('Cache-Control') || payload.status) + '), so it will not hydrate offline')
|
|
824
|
+
})
|
|
825
|
+
.catch(() => {})
|
|
826
|
+
}),
|
|
827
|
+
),
|
|
828
|
+
)
|
|
728
829
|
.then(() => self.skipWaiting()),
|
|
729
830
|
)
|
|
730
831
|
})
|
|
@@ -733,9 +834,16 @@ self.addEventListener('activate', (event) => {
|
|
|
733
834
|
event.waitUntil(
|
|
734
835
|
caches
|
|
735
836
|
.keys()
|
|
736
|
-
.then((keys) =>
|
|
737
|
-
|
|
738
|
-
|
|
837
|
+
.then((keys) => {
|
|
838
|
+
const older = keys.filter((k) => k.startsWith('rsc-kit-') && k !== CACHE)
|
|
839
|
+
|
|
840
|
+
return Promise.all(older.map((k) => caches.delete(k))).then(() => older.length > 0)
|
|
841
|
+
})
|
|
842
|
+
.then((swept) => self.clients.claim().then(() => swept))
|
|
843
|
+
// Only when something older was swept. The first worker a visitor ever
|
|
844
|
+
// gets activates too, and told them "a new version is ready" on their
|
|
845
|
+
// second page - there was no previous version to be new against.
|
|
846
|
+
.then((swept) => (swept ? tellTheOpenPages() : undefined)),
|
|
739
847
|
)
|
|
740
848
|
})
|
|
741
849
|
|
|
@@ -780,6 +888,37 @@ const keyFor = (request) => {
|
|
|
780
888
|
return new Request(url, { headers: request.headers })
|
|
781
889
|
}
|
|
782
890
|
|
|
891
|
+
// Nothing cached for a request and no network: the offline page for a
|
|
892
|
+
// navigation, and for the boot of a page the offline page stood in for -
|
|
893
|
+
// the runtime asks for the payload of the url in the address bar, which is
|
|
894
|
+
// the one thing nothing has - the offline page's own payload, which is what
|
|
895
|
+
// the document on screen is. A boot only, no segments header: a navigation
|
|
896
|
+
// while offline still fails as itself, and the page already open keeps its
|
|
897
|
+
// banner. Null when there is no offline page, or nothing of it cached.
|
|
898
|
+
const standIn = async (request) => {
|
|
899
|
+
if (!OFFLINE_URL) return null
|
|
900
|
+
|
|
901
|
+
if (request.mode === 'navigate') return (await caches.match(OFFLINE_URL)) ?? null
|
|
902
|
+
|
|
903
|
+
if (request.headers.get('X-RSC') && !request.headers.get('X-RSC-Segments')) {
|
|
904
|
+
return (
|
|
905
|
+
(await caches.match(
|
|
906
|
+
keyFor(new Request(new URL(OFFLINE_URL, self.location.origin), { headers: { 'X-RSC': '1' } })),
|
|
907
|
+
MATCH,
|
|
908
|
+
)) ?? null
|
|
909
|
+
)
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
return null
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// The Cache API honours Vary, and a stored payload varies on X-RSC - so a
|
|
916
|
+
// payload warmed with one spelling of that header was a miss for a boot
|
|
917
|
+
// that sent another, and a precached page rendered offline with its payload
|
|
918
|
+
// sitting in the cache, unmatched. What Vary is for is already in the key:
|
|
919
|
+
// the ?__rsc=<segments> above is the one header the answer differs on.
|
|
920
|
+
const MATCH = { ignoreVary: true }
|
|
921
|
+
|
|
783
922
|
self.addEventListener('fetch', (event) => {
|
|
784
923
|
const request = event.request
|
|
785
924
|
const url = new URL(request.url)
|
|
@@ -814,7 +953,7 @@ self.addEventListener('fetch', (event) => {
|
|
|
814
953
|
// differently for every value of it.
|
|
815
954
|
if (FROZEN.has(url.pathname.replace(/\\/+$/, '') || '/') && !url.search) {
|
|
816
955
|
event.respondWith(
|
|
817
|
-
caches.match(keyFor(request)).then((hit) => {
|
|
956
|
+
caches.match(keyFor(request), MATCH).then((hit) => {
|
|
818
957
|
const fresh = fetch(request)
|
|
819
958
|
.then((response) => {
|
|
820
959
|
if (mayStore(response)) {
|
|
@@ -824,10 +963,17 @@ self.addEventListener('fetch', (event) => {
|
|
|
824
963
|
|
|
825
964
|
return response
|
|
826
965
|
})
|
|
827
|
-
.catch((error) => {
|
|
966
|
+
.catch(async (error) => {
|
|
828
967
|
if (hit) return hit
|
|
829
968
|
|
|
830
|
-
|
|
969
|
+
// Stored by the build but never visited from this browser, so
|
|
970
|
+
// nothing is cached for it, and no network: the offline page is
|
|
971
|
+
// the honest answer, the same as for any other navigation - and
|
|
972
|
+
// its payload for the boot that follows, so the page it stood
|
|
973
|
+
// in for hydrates. Without this a frozen page failed with
|
|
974
|
+
// ERR_FAILED where an unstored one showed /offline, and then
|
|
975
|
+
// stood inert where an unstored one hydrated.
|
|
976
|
+
return (await standIn(request)) ?? Promise.reject(error)
|
|
831
977
|
})
|
|
832
978
|
|
|
833
979
|
// Not awaited when there is a hit: the point is that the visitor does
|
|
@@ -852,11 +998,11 @@ self.addEventListener('fetch', (event) => {
|
|
|
852
998
|
// cached, and a reload with no network has the markup and nothing to
|
|
853
999
|
// hydrate it with.
|
|
854
1000
|
//
|
|
855
|
-
// \`X-RSC:
|
|
1001
|
+
// \`X-RSC: 1\` and no segments header is exactly what a fresh boot
|
|
856
1002
|
// sends, which is what makes this entry the one it finds: the server
|
|
857
1003
|
// varies on those, and the Cache API matches on the same.
|
|
858
1004
|
if (request.mode === 'navigate') {
|
|
859
|
-
const warm = new Request(request.url, { headers: { 'X-RSC': '
|
|
1005
|
+
const warm = new Request(request.url, { headers: { 'X-RSC': '1' } })
|
|
860
1006
|
|
|
861
1007
|
fetch(warm)
|
|
862
1008
|
.then((payload) => {
|
|
@@ -891,7 +1037,7 @@ self.addEventListener('fetch', (event) => {
|
|
|
891
1037
|
return response
|
|
892
1038
|
})
|
|
893
1039
|
.catch(async () => {
|
|
894
|
-
const hit = await caches.match(keyFor(request))
|
|
1040
|
+
const hit = await caches.match(keyFor(request), MATCH)
|
|
895
1041
|
|
|
896
1042
|
if (hit) return hit
|
|
897
1043
|
|
|
@@ -906,11 +1052,9 @@ self.addEventListener('fetch', (event) => {
|
|
|
906
1052
|
// stand in for another: it is about being offline, not about the url it
|
|
907
1053
|
// appears under. Navigations only — a payload request answered with a
|
|
908
1054
|
// document would be decoded as one and throw.
|
|
909
|
-
|
|
910
|
-
const page = await caches.match(OFFLINE_URL)
|
|
1055
|
+
const stood = await standIn(request)
|
|
911
1056
|
|
|
912
|
-
|
|
913
|
-
}
|
|
1057
|
+
if (stood) return stood
|
|
914
1058
|
|
|
915
1059
|
// Letting it fail says what is true, and a page already open is
|
|
916
1060
|
// unaffected.
|
|
@@ -1025,17 +1169,24 @@ function writeWebManifest(clientDir, options) {
|
|
|
1025
1169
|
* precached and found wanting at the one moment it matters, and the build says
|
|
1026
1170
|
* which read did it.
|
|
1027
1171
|
*/
|
|
1028
|
-
function offlineFallback(frozen, results) {
|
|
1172
|
+
export function offlineFallback(frozen, results) {
|
|
1029
1173
|
const found = results.find((r) => r.url === "/offline");
|
|
1030
1174
|
if (!found)
|
|
1031
1175
|
return null;
|
|
1032
1176
|
if (!frozen.includes("/offline")) {
|
|
1033
1177
|
// The reason already reads "dynamic — called cookies()", and this sentence
|
|
1034
1178
|
// has said "not stored" by the time it gets there, so the prefix would say
|
|
1035
|
-
// it twice with a dash in the middle of both.
|
|
1036
|
-
|
|
1179
|
+
// it twice with a dash in the middle of both. A shell's reason is a
|
|
1180
|
+
// sentence of its own - "connection() awaited by AuthLinks streams per
|
|
1181
|
+
// request; the rest is stored" - and takes no "it" in front, and the
|
|
1182
|
+
// part about the rest being stored is not the point here.
|
|
1183
|
+
const why = found.type === "shell"
|
|
1184
|
+
? (found.reason?.replace(/; the rest is stored$/, "") ?? null)
|
|
1185
|
+
: found.reason
|
|
1186
|
+
? "it " + found.reason.replace(/^dynamic — /, "")
|
|
1187
|
+
: null;
|
|
1037
1188
|
log("offline: /offline cannot be the fallback" +
|
|
1038
|
-
(why ? `, because
|
|
1189
|
+
(why ? `, because ${why}` : "") +
|
|
1039
1190
|
". A fallback has to be servable with no network at all.");
|
|
1040
1191
|
return null;
|
|
1041
1192
|
}
|
|
@@ -1061,6 +1212,24 @@ function copyServiceWorkerExtra(clientDir) {
|
|
|
1061
1212
|
copyFileSync(source, join(clientDir, "sw-app.js"));
|
|
1062
1213
|
return "/sw-app.js";
|
|
1063
1214
|
}
|
|
1215
|
+
/**
|
|
1216
|
+
* Whether a public file is worth fetching before it is asked for.
|
|
1217
|
+
*
|
|
1218
|
+
* The precache is what lets the app boot with no network: the scripts, the
|
|
1219
|
+
* stylesheets, the fonts, the manifest and its icons. Everything else in
|
|
1220
|
+
* public/ - an image, a wasm module, the share card - is cached the first
|
|
1221
|
+
* time it is used, which is what the fetch handler does for any hashed
|
|
1222
|
+
* asset. Precaching all of it made an install cost a megabyte before the
|
|
1223
|
+
* visitor had seen a page: 765 kB of it a webp encoder, the rest a png no
|
|
1224
|
+
* browser ever renders.
|
|
1225
|
+
*/
|
|
1226
|
+
export function bootsTheApp(file) {
|
|
1227
|
+
if (/\.(?:m?js|css|woff2?|webmanifest|json)$/i.test(file))
|
|
1228
|
+
return !/\.wasm\.js$/i.test(file);
|
|
1229
|
+
if (/(?:^|\/)(?:icon|apple-icon|favicon)[^/]*\.(?:png|ico|svg)$/i.test(file))
|
|
1230
|
+
return true;
|
|
1231
|
+
return false;
|
|
1232
|
+
}
|
|
1064
1233
|
function writeServiceWorker(clientDir, frozen = [], offlineUrl = null) {
|
|
1065
1234
|
if (!existsSync(clientDir))
|
|
1066
1235
|
return;
|
|
@@ -1079,7 +1248,7 @@ function writeServiceWorker(clientDir, frozen = [], offlineUrl = null) {
|
|
|
1079
1248
|
const path = join(dir, entry.name);
|
|
1080
1249
|
if (entry.isDirectory())
|
|
1081
1250
|
walk(path, `${prefix}${entry.name}/`);
|
|
1082
|
-
else
|
|
1251
|
+
else if (bootsTheApp(`${prefix}${entry.name}`))
|
|
1083
1252
|
files.push(`${prefix}${entry.name}`);
|
|
1084
1253
|
}
|
|
1085
1254
|
};
|
|
@@ -1682,8 +1851,20 @@ function componentName(absPath) {
|
|
|
1682
1851
|
const rel = relative(sourceDir, absPath).replace(/\\/g, "/");
|
|
1683
1852
|
return rel.replace(/\.(tsx|jsx|ts|js)$/, "");
|
|
1684
1853
|
}
|
|
1685
|
-
|
|
1686
|
-
|
|
1854
|
+
/**
|
|
1855
|
+
* The identifier a route file is imported as in the generated entries.
|
|
1856
|
+
*
|
|
1857
|
+
* One-to-one with the path, which the obvious "replace everything odd with an
|
|
1858
|
+
* underscore" is not: app/agent-account/page and app/agent/account/page
|
|
1859
|
+
* both came out as _c_app_agent_account_page, and the entry declared the
|
|
1860
|
+
* same import twice. A slash is an underscore, because that is what every
|
|
1861
|
+
* alias reads like; anything else that is not a letter or digit - a hyphen,
|
|
1862
|
+
* a dot, the brackets of a param, the parens of a group, an underscore of
|
|
1863
|
+
* its own - is its character code, so no two paths share an alias.
|
|
1864
|
+
*/
|
|
1865
|
+
export function toAlias(name) {
|
|
1866
|
+
return ("_c_" +
|
|
1867
|
+
name.replace(/[^a-zA-Z0-9]/g, (char) => (char === "/" ? "_" : "$" + char.charCodeAt(0).toString(16))));
|
|
1687
1868
|
}
|
|
1688
1869
|
const components = new Map();
|
|
1689
1870
|
/**
|
|
@@ -1695,6 +1876,48 @@ const components = new Map();
|
|
|
1695
1876
|
* a default component.
|
|
1696
1877
|
*/
|
|
1697
1878
|
const apiRoutes = new Map();
|
|
1879
|
+
const OPENAPI_ROUTE_ID = "virtual:rsc-kit/openapi";
|
|
1880
|
+
/**
|
|
1881
|
+
* /openapi.json, when asked for, registered as an api route at the path the
|
|
1882
|
+
* option names. Synthesised like a sitemap: stored by the build, without
|
|
1883
|
+
* middleware - a document exists to be read.
|
|
1884
|
+
*/
|
|
1885
|
+
function registerOpenApiRoutes() {
|
|
1886
|
+
if (!openapi)
|
|
1887
|
+
return;
|
|
1888
|
+
const name = `app${openapi.path.replace(/\/$/, "")}/route`;
|
|
1889
|
+
if (apiRoutes.has(name)) {
|
|
1890
|
+
throw new Error(`[rsc-kit] ${openapi.path} is answered by ${apiRoutes.get(name).absPath} and by rscKit({ openapi }). Move one.`);
|
|
1891
|
+
}
|
|
1892
|
+
apiRoutes.set(name, { name, absPath: OPENAPI_ROUTE_ID, methods: ["GET"], generated: { kind: "openapi", file: "" } });
|
|
1893
|
+
}
|
|
1894
|
+
/** The module behind the synthesised /openapi.json route. */
|
|
1895
|
+
function openApiPlugin() {
|
|
1896
|
+
return {
|
|
1897
|
+
name: "rsc-kit:openapi",
|
|
1898
|
+
resolveId(id) {
|
|
1899
|
+
if (id === OPENAPI_ROUTE_ID)
|
|
1900
|
+
return "\0" + id;
|
|
1901
|
+
},
|
|
1902
|
+
load(id) {
|
|
1903
|
+
if (id !== "\0" + OPENAPI_ROUTE_ID || !openapi)
|
|
1904
|
+
return;
|
|
1905
|
+
// Every route.ts the app wrote, imported so its schemas can describe
|
|
1906
|
+
// themselves at request time; the synthesised ones are not an API.
|
|
1907
|
+
const routes = [...apiRoutes.values()].filter((r) => !r.generated);
|
|
1908
|
+
const guarded = new Set((routeManifest().apis ?? []).filter((api) => api.middleware.length > 0).map((api) => api.name));
|
|
1909
|
+
const imports = routes.map((r, i) => `import * as __r${i} from ${JSON.stringify(r.absPath)}`);
|
|
1910
|
+
const entries = routes.map((r, i) => ` { pattern: ${JSON.stringify(patternOf(urlSegments(r.name)))}, methods: ${JSON.stringify(r.methods)}, guarded: ${guarded.has(r.name)}, module: __r${i} }`);
|
|
1911
|
+
return [
|
|
1912
|
+
...imports,
|
|
1913
|
+
`import { openApiResponse } from ${JSON.stringify(join(packageDir, "openapi"))}`,
|
|
1914
|
+
`const routes = [\n${entries.join(",\n")}\n]`,
|
|
1915
|
+
`export const GET = () => openApiResponse(routes, ${JSON.stringify(openapi.document)})`,
|
|
1916
|
+
"",
|
|
1917
|
+
].join("\n");
|
|
1918
|
+
},
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1698
1921
|
/** Files beside the root layout served at the root as they are: robots.txt, a hand-written sitemap.xml, humans.txt. */
|
|
1699
1922
|
let rootFiles = [];
|
|
1700
1923
|
const METADATA_ROUTE_ID = "virtual:rsc-kit/metadata-route/";
|
|
@@ -2030,6 +2253,9 @@ const NITRO_HANDLER_OPTIONS = ` props: (match, request) => ({
|
|
|
2030
2253
|
...Object.fromEntries(new URL(request.url).searchParams),
|
|
2031
2254
|
}),
|
|
2032
2255
|
version: process.env.RSC_BUILD_VERSION,
|
|
2256
|
+
// A built server gzips what it answers, where the runtime can; the dev
|
|
2257
|
+
// server answers raw, which is what a person reading a response wants.
|
|
2258
|
+
compress: import.meta.env.PROD,
|
|
2033
2259
|
`;
|
|
2034
2260
|
const NITRO_HOST_CALLS = `
|
|
2035
2261
|
let hostInstalled = false
|
|
@@ -2068,8 +2294,15 @@ function installHostCallsOnce(): void {
|
|
|
2068
2294
|
function instrumentationFile() {
|
|
2069
2295
|
for (const ext of ["ts", "tsx", "mts", "js", "mjs"]) {
|
|
2070
2296
|
const file = join(sourceDir, `instrumentation.${ext}`);
|
|
2071
|
-
if (existsSync(file))
|
|
2072
|
-
|
|
2297
|
+
if (!existsSync(file))
|
|
2298
|
+
continue;
|
|
2299
|
+
// Both halves are optional: a file of imports is a bootstrap, a file
|
|
2300
|
+
// with register() is a hook. The entry must only name `register` when it
|
|
2301
|
+
// exists - a namespace import's missing member is a bundler warning on
|
|
2302
|
+
// every build (IMPORT_IS_UNDEFINED), which is the app being told off for
|
|
2303
|
+
// a file written exactly as documented.
|
|
2304
|
+
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"));
|
|
2305
|
+
return { file, hasRegister };
|
|
2073
2306
|
}
|
|
2074
2307
|
return null;
|
|
2075
2308
|
}
|
|
@@ -2135,10 +2368,13 @@ ${
|
|
|
2135
2368
|
// First, before any page: an import's side effects run in import order,
|
|
2136
2369
|
// and a package configured here has to be configured before a page module
|
|
2137
2370
|
// that reads it at evaluation time.
|
|
2138
|
-
instrumentation
|
|
2139
|
-
? `import * as __instrumentation from ${JSON.stringify(instrumentation)}`
|
|
2140
|
-
:
|
|
2371
|
+
instrumentation?.hasRegister
|
|
2372
|
+
? `import * as __instrumentation from ${JSON.stringify(instrumentation.file)}`
|
|
2373
|
+
: instrumentation
|
|
2374
|
+
? `import ${JSON.stringify(instrumentation.file)}\nconst __instrumentation: { register?: () => unknown } = {}`
|
|
2375
|
+
: "const __instrumentation: { register?: () => unknown } = {}"}
|
|
2141
2376
|
import { SegmentBoundary } from ${JSON.stringify(join(packageDir, "js/SegmentBoundary"))}
|
|
2377
|
+
import { LoadingBoundary } from ${JSON.stringify(join(packageDir, "js/LoadingBoundary"))}
|
|
2142
2378
|
import { DocumentTitle } from ${JSON.stringify(join(packageDir, "js/DocumentTitle"))}
|
|
2143
2379
|
import { SlotBoundary } from ${JSON.stringify(join(packageDir, "js/SlotBoundary"))}
|
|
2144
2380
|
import { RouteErrorBoundary } from ${JSON.stringify(join(packageDir, "js/RouteErrorBoundary"))}
|
|
@@ -2148,12 +2384,12 @@ import { DefaultRouteError } from ${JSON.stringify(join(packageDir, "js/DefaultR
|
|
|
2148
2384
|
import { searchParams as requestSearchParams } from ${JSON.stringify(join(packageDir, "request"))}
|
|
2149
2385
|
import { parseParams, parseSearchParams, parseBody, isSearchParamsError, isBodyError } from ${JSON.stringify(join(packageDir, "routeSchema"))}
|
|
2150
2386
|
import { notFoundDigest, isNotFoundSignal } from ${JSON.stringify(join(packageDir, "notFound"))}
|
|
2151
|
-
import { noteRequestRead } from ${JSON.stringify(join(packageDir, "request"))}
|
|
2387
|
+
import { noteRequestRead, urlOf } from ${JSON.stringify(join(packageDir, "request"))}
|
|
2152
2388
|
import { redirectDigest } from ${JSON.stringify(join(packageDir, "redirectDigest"))}
|
|
2153
2389
|
import { createRscHandler } from ${JSON.stringify(join(packageDir, "host"))}
|
|
2154
2390
|
import { httpHostCalls } from ${JSON.stringify(join(packageDir, "hostCalls"))}
|
|
2155
2391
|
import { prerenderedBeside } from ${JSON.stringify(join(packageDir, "files"))}
|
|
2156
|
-
import { renderToReadableStream, decodeReply, loadServerAction } from '@vitejs/plugin-rsc/rsc'
|
|
2392
|
+
import { renderToReadableStream, decodeReply, decodeAction, decodeFormState, loadServerAction } from '@vitejs/plugin-rsc/rsc'
|
|
2157
2393
|
import { isQuery, queryCacheControl, isQueryValidationError } from ${JSON.stringify(join(packageDir, "query"))}
|
|
2158
2394
|
import { isActionValidationError, isClientBuilt } from ${JSON.stringify(join(packageDir, "action"))}
|
|
2159
2395
|
import { noteFallback as noteCaughtRead } from ${JSON.stringify(join(packageDir, "request"))}
|
|
@@ -2313,7 +2549,10 @@ export async function handleApiRoute(
|
|
|
2313
2549
|
// reaches for the value.
|
|
2314
2550
|
noteRequestRead('searchParams')
|
|
2315
2551
|
|
|
2316
|
-
|
|
2552
|
+
// Through urlOf, so the build's probe books this read to searchParams
|
|
2553
|
+
// rather than to the url - a route reads request.url itself when it
|
|
2554
|
+
// wants the query the Next way, and that read is the one that matters.
|
|
2555
|
+
return parseSearchParams(mod.searchParams, new URL(urlOf(request)).searchParams)
|
|
2317
2556
|
}),
|
|
2318
2557
|
body: hasBody(method) ? lazily(() => parseBody(mod.body, request)) : undefined,
|
|
2319
2558
|
}
|
|
@@ -2675,9 +2914,17 @@ function buildElement(
|
|
|
2675
2914
|
searchParams: checkedSearchParams(schemas, pageSearchParams()),
|
|
2676
2915
|
})
|
|
2677
2916
|
|
|
2917
|
+
// Through LoadingBoundary when the runtime is shipped, so a server render
|
|
2918
|
+
// can tell the engine's boundary from one the developer wrote by name -
|
|
2919
|
+
// see that file. A page shipping no runtime gets a plain Suspense: a
|
|
2920
|
+
// client component would drag React in for a wrapper nothing can use.
|
|
2678
2921
|
for (let i = loadings.length - 1; i >= 0; i--) {
|
|
2679
2922
|
const Loading = components[loadings[i]]
|
|
2680
|
-
|
|
2923
|
+
const fallback = Loading ? createElement(Loading) : null
|
|
2924
|
+
|
|
2925
|
+
element = bootstrap
|
|
2926
|
+
? createElement(LoadingBoundary, { fallback }, element)
|
|
2927
|
+
: createElement(Suspense, { fallback }, element)
|
|
2681
2928
|
}
|
|
2682
2929
|
|
|
2683
2930
|
// Outside the Suspense boundary, innermost first — the nearest error.tsx to
|
|
@@ -3256,6 +3503,54 @@ export async function handleRscHtmlStream(
|
|
|
3256
3503
|
return { htmlStream, rscPayloadPromise, clientChunks: {} }
|
|
3257
3504
|
}
|
|
3258
3505
|
|
|
3506
|
+
/**
|
|
3507
|
+
* A form posted to the page before the page had a runtime.
|
|
3508
|
+
*
|
|
3509
|
+
* React writes the action's id into the form it emits for a server action,
|
|
3510
|
+
* so a browser with no javascript yet - or none at all - posts the fields
|
|
3511
|
+
* to the page's own url. The action runs from those fields, exactly as it
|
|
3512
|
+
* would have been called, and the page renders afterwards with what it
|
|
3513
|
+
* returned as React's form state: a useActionState form shows its result,
|
|
3514
|
+
* a redirect() thrown by the action leaves through the scope the way a
|
|
3515
|
+
* page's would, and a cookie it set is on the answer.
|
|
3516
|
+
*/
|
|
3517
|
+
export async function handleRscFormPost(
|
|
3518
|
+
component: string,
|
|
3519
|
+
props: Record<string, unknown> = {},
|
|
3520
|
+
layouts: LayoutEntry[] = [],
|
|
3521
|
+
loadings: string[] = [],
|
|
3522
|
+
parallelSlots: Record<string, string> = {},
|
|
3523
|
+
slotOverrides: Record<string, SlotOverride> = {},
|
|
3524
|
+
nonce?: string,
|
|
3525
|
+
pageKey = '',
|
|
3526
|
+
bootstrap = true,
|
|
3527
|
+
formData: FormData = new FormData(),
|
|
3528
|
+
): Promise<{ htmlStream: ReadableStream }> {
|
|
3529
|
+
await instrumented()
|
|
3530
|
+
applyHost()
|
|
3531
|
+
await runMiddleware(component, props)
|
|
3532
|
+
|
|
3533
|
+
// The function the form named, bound to what it posted. Nothing named -
|
|
3534
|
+
// a plain form posted here by mistake - and the page simply renders.
|
|
3535
|
+
const action = await decodeAction(formData)
|
|
3536
|
+
let formState: unknown
|
|
3537
|
+
|
|
3538
|
+
if (action) {
|
|
3539
|
+
const result = await action()
|
|
3540
|
+
|
|
3541
|
+
formState = await decodeFormState(result, formData)
|
|
3542
|
+
}
|
|
3543
|
+
|
|
3544
|
+
const flight = renderToReadableStream(
|
|
3545
|
+
await renderTree(component, props, layouts, loadings, parallelSlots, slotOverrides, 0, pageKey, bootstrap),
|
|
3546
|
+
{ onError: flightOnError },
|
|
3547
|
+
)
|
|
3548
|
+
const ssr = await (import.meta as any).viteRsc.loadModule('ssr', 'index')
|
|
3549
|
+
const htmlStream = await ssr.handleSsr(flight, nonce, undefined, bootstrap, formState)
|
|
3550
|
+
|
|
3551
|
+
return { htmlStream }
|
|
3552
|
+
}
|
|
3553
|
+
|
|
3259
3554
|
/**
|
|
3260
3555
|
* Finish a shell that was frozen at build time.
|
|
3261
3556
|
*
|
|
@@ -3882,7 +4177,7 @@ export async function handleRscPprShell(
|
|
|
3882
4177
|
// say so on the route's line; the digest goes into the document for the
|
|
3883
4178
|
// browser to recognise.
|
|
3884
4179
|
if ((e as { digest?: string } | null)?.digest === 'rsc-kit:search-params-fallback') {
|
|
3885
|
-
const where = /at ([A-
|
|
4180
|
+
const where = /at ([A-Z_$][\\w$]*)/.exec(info?.componentStack ?? '')?.[1]
|
|
3886
4181
|
noteCaughtRead('useSearchParams()' + (where ? ' in ' + where : ''))
|
|
3887
4182
|
return 'rsc-kit:search-params-fallback'
|
|
3888
4183
|
}
|
|
@@ -4032,6 +4327,7 @@ async function serve(request: Request): Promise<Response> {
|
|
|
4032
4327
|
handleRsc,
|
|
4033
4328
|
handleRscStream,
|
|
4034
4329
|
handleRscHtmlStream,
|
|
4330
|
+
handleRscFormPost,
|
|
4035
4331
|
handleRscRevalidate,
|
|
4036
4332
|
handleRscPayload,
|
|
4037
4333
|
handleRscPprShell,
|
|
@@ -4122,6 +4418,8 @@ function generateEntrySsr() {
|
|
|
4122
4418
|
const devUrls = join(packageDir, "devUrls");
|
|
4123
4419
|
const request = join(packageDir, "request");
|
|
4124
4420
|
const fallbackReport = join(packageDir, "js/fallbackReport");
|
|
4421
|
+
const redirectDigestModule = join(packageDir, "redirectDigest");
|
|
4422
|
+
const notFoundModule = join(packageDir, "notFound");
|
|
4125
4423
|
return `// GENERATED by rscKit() — do not edit.
|
|
4126
4424
|
import { createFromReadableStream } from '@vitejs/plugin-rsc/ssr'
|
|
4127
4425
|
import { renderToReadableStream, resume } from 'react-dom/server.edge'
|
|
@@ -4129,6 +4427,8 @@ import { prerender } from 'react-dom/static.edge'
|
|
|
4129
4427
|
import { rewriteViteDevUrlStream } from ${JSON.stringify(devUrls)}
|
|
4130
4428
|
import { noteFallback } from ${JSON.stringify(request)}
|
|
4131
4429
|
import { cancelledByConsumer, caughtByLoading } from ${JSON.stringify(fallbackReport)}
|
|
4430
|
+
import { parseRedirectDigest } from ${JSON.stringify(redirectDigestModule)}
|
|
4431
|
+
import { isNotFoundDigest } from ${JSON.stringify(notFoundModule)}
|
|
4132
4432
|
|
|
4133
4433
|
// Set only by the dev server. @vitejs/plugin-rsc emits its bootstrap and CSS
|
|
4134
4434
|
// links root-relative in dev, which would send the browser to the host for
|
|
@@ -4140,6 +4440,7 @@ export async function handleSsr(
|
|
|
4140
4440
|
nonce?: string,
|
|
4141
4441
|
onError?: (error: unknown) => void,
|
|
4142
4442
|
bootstrap = true,
|
|
4443
|
+
formState?: unknown,
|
|
4143
4444
|
): Promise<ReadableStream> {
|
|
4144
4445
|
const root = await createFromReadableStream(rscStream)
|
|
4145
4446
|
|
|
@@ -4156,35 +4457,10 @@ export async function handleSsr(
|
|
|
4156
4457
|
const html = await renderToReadableStream(root as any, {
|
|
4157
4458
|
bootstrapScriptContent,
|
|
4158
4459
|
nonce,
|
|
4159
|
-
|
|
4160
|
-
//
|
|
4161
|
-
//
|
|
4162
|
-
|
|
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
|
|
4166
|
-
const digest = (error as { digest?: string } | null)?.digest
|
|
4167
|
-
if (digest === 'rsc-kit:search-params-fallback') {
|
|
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
|
-
}
|
|
4184
|
-
return digest
|
|
4185
|
-
}
|
|
4186
|
-
console.error('[rsc-kit:ssr]', error)
|
|
4187
|
-
}),
|
|
4460
|
+
onError: onError ?? reportRenderError('ssr'),
|
|
4461
|
+
// What a posted form's action returned, for the useActionState that
|
|
4462
|
+
// asked: React seats it in the form it belongs to.
|
|
4463
|
+
...(formState !== undefined ? { formState: formState as any } : {}),
|
|
4188
4464
|
})
|
|
4189
4465
|
|
|
4190
4466
|
return DEV_ORIGIN ? rewriteViteDevUrlStream(html, DEV_ORIGIN) : html
|
|
@@ -4224,7 +4500,7 @@ export async function handleSsrPrerender(
|
|
|
4224
4500
|
// caught at a boundary is: the build attaches it to the route.
|
|
4225
4501
|
onError: (error: unknown, info?: { componentStack?: string }) => {
|
|
4226
4502
|
if ((error as { digest?: string } | null)?.digest !== 'rsc-kit:search-params-fallback') return
|
|
4227
|
-
const where = /at ([A-
|
|
4503
|
+
const where = /at ([A-Z_$][\\w$]*)/.exec(info?.componentStack ?? '')?.[1]
|
|
4228
4504
|
noteFallback('useSearchParams()' + (where ? ' in ' + where : ''))
|
|
4229
4505
|
return 'rsc-kit:search-params-fallback'
|
|
4230
4506
|
},
|
|
@@ -4236,6 +4512,53 @@ export async function handleSsrPrerender(
|
|
|
4236
4512
|
}
|
|
4237
4513
|
}
|
|
4238
4514
|
|
|
4515
|
+
/**
|
|
4516
|
+
* What a server render says about an error, first render and resume alike.
|
|
4517
|
+
*
|
|
4518
|
+
* The query-string fallback is the designed path for a stored page, and its
|
|
4519
|
+
* digest is what lets the client tell it from a fault on hydration; returned
|
|
4520
|
+
* so React writes it into the document. Under a boundary the developer wrote
|
|
4521
|
+
* there is nothing to say; with nothing closer than a loading.tsx, one line.
|
|
4522
|
+
*/
|
|
4523
|
+
function reportRenderError(phase: 'ssr' | 'resume') {
|
|
4524
|
+
return (error: unknown, info?: { componentStack?: string }) => {
|
|
4525
|
+
// The consumer cancelled - a browser that left mid-stream, a prefetch
|
|
4526
|
+
// abandoned. React reports it as an error; the page had none.
|
|
4527
|
+
if (cancelledByConsumer(error)) return
|
|
4528
|
+
|
|
4529
|
+
const digest = (error as { digest?: string } | null)?.digest
|
|
4530
|
+
|
|
4531
|
+
// A redirect or a notFound() the render asked for arrives here as the
|
|
4532
|
+
// Flight row's error, once per boundary it was thrown in. The rsc side
|
|
4533
|
+
// already turned it into a digest on purpose; it is the page's answer,
|
|
4534
|
+
// and the host reads it off the digest. Not an error to print, four
|
|
4535
|
+
// times or once.
|
|
4536
|
+
if (parseRedirectDigest(digest) || isNotFoundDigest(digest)) return digest
|
|
4537
|
+
|
|
4538
|
+
if (digest === 'rsc-kit:search-params-fallback') {
|
|
4539
|
+
// The component is the first frame of React's stack. Noted on the
|
|
4540
|
+
// request so the build attaches it to the route; printed as one line,
|
|
4541
|
+
// not a stack, so the server says which boundary it wants.
|
|
4542
|
+
const where = /at ([A-Z_$][\\w$]*)/.exec(info?.componentStack ?? '')?.[1]
|
|
4543
|
+
|
|
4544
|
+
noteFallback('useSearchParams()' + (where ? ' in ' + where : ''))
|
|
4545
|
+
|
|
4546
|
+
if (caughtByLoading(info?.componentStack)) {
|
|
4547
|
+
console.error(
|
|
4548
|
+
'[rsc-kit] ' + (where ? where + ': ' : '') +
|
|
4549
|
+
'useSearchParams() was read on the server with nothing closer than a loading.tsx, so the whole ' +
|
|
4550
|
+
'segment shows that fallback until the query arrives. A <Suspense> around the component that reads ' +
|
|
4551
|
+
'keeps the rest of the page painted.',
|
|
4552
|
+
)
|
|
4553
|
+
}
|
|
4554
|
+
|
|
4555
|
+
return digest
|
|
4556
|
+
}
|
|
4557
|
+
|
|
4558
|
+
console.error('[rsc-kit:' + phase + ']', error)
|
|
4559
|
+
}
|
|
4560
|
+
}
|
|
4561
|
+
|
|
4239
4562
|
/**
|
|
4240
4563
|
* Pick a build-time render back up, against data that exists now.
|
|
4241
4564
|
*
|
|
@@ -4255,7 +4578,12 @@ export async function handleSsrResume(
|
|
|
4255
4578
|
|
|
4256
4579
|
const html = await resume(root as any, postponed as any, {
|
|
4257
4580
|
nonce,
|
|
4258
|
-
|
|
4581
|
+
// The same reading of an error the first render has. This used to log
|
|
4582
|
+
// every error raw, so a query read the developer's own boundary caught -
|
|
4583
|
+
// the designed path, on every resume of a shell whose hole holds one -
|
|
4584
|
+
// printed as "[rsc-kit:resume] Error: useSearchParams() was read..." on
|
|
4585
|
+
// every request, with advice the app had already followed.
|
|
4586
|
+
onError: reportRenderError('resume'),
|
|
4259
4587
|
})
|
|
4260
4588
|
|
|
4261
4589
|
return DEV_ORIGIN ? rewriteViteDevUrlStream(html, DEV_ORIGIN) : html
|
|
@@ -4305,6 +4633,9 @@ import { refresh } from ${JSON.stringify(refreshModule)}
|
|
|
4305
4633
|
createViteRscApp(document, ${JSON.stringify(interceptManifest())}, ${JSON.stringify({
|
|
4306
4634
|
staticPayloads: staticPayloads || null,
|
|
4307
4635
|
routes: routesForClient,
|
|
4636
|
+
// Every route.ts, as a url pattern, so a link to one is treated as an
|
|
4637
|
+
// anchor rather than prefetched and fetched as a page.
|
|
4638
|
+
apiRoutes: (routeManifest().apis ?? []).map((api) => patternOf(api.segments)),
|
|
4308
4639
|
})})
|
|
4309
4640
|
|
|
4310
4641
|
// A server component is not a module the browser has, so Vite cannot replace
|
|
@@ -4335,12 +4666,19 @@ ${offline
|
|
|
4335
4666
|
// worker answering from a cache in front of it turns every edit into a
|
|
4336
4667
|
// question about which copy you are looking at.
|
|
4337
4668
|
if ('serviceWorker' in navigator && import.meta.env.PROD) {
|
|
4338
|
-
|
|
4669
|
+
const register = () => {
|
|
4339
4670
|
void navigator.serviceWorker.register('/sw.js').catch(() => {
|
|
4340
4671
|
// A worker that will not register is not a reason for the page to fail.
|
|
4341
4672
|
// The app works; it just will not survive being reloaded offline.
|
|
4342
4673
|
})
|
|
4343
|
-
}
|
|
4674
|
+
}
|
|
4675
|
+
|
|
4676
|
+
// This runs after the boot payload has been fetched and decoded, and on a
|
|
4677
|
+
// fast load the page has finished loading by then - a listener added now
|
|
4678
|
+
// waits for an event that has already fired, and nothing registered until
|
|
4679
|
+
// the next navigation.
|
|
4680
|
+
if (document.readyState === 'complete') register()
|
|
4681
|
+
else window.addEventListener('load', register)
|
|
4344
4682
|
}
|
|
4345
4683
|
`
|
|
4346
4684
|
: ""}`;
|
|
@@ -4611,7 +4949,21 @@ function useSsrModules() {
|
|
|
4611
4949
|
* directive that moves it. The build says the same once, as a warning.
|
|
4612
4950
|
*/
|
|
4613
4951
|
const RENDERER_STUB = "\0rsc-kit:react-dom-server?from=";
|
|
4614
|
-
|
|
4952
|
+
/** What react-dom/server exports, across its node, edge and browser entries. */
|
|
4953
|
+
const SERVER_RENDERER_EXPORTS = [
|
|
4954
|
+
"renderToString",
|
|
4955
|
+
"renderToStaticMarkup",
|
|
4956
|
+
"renderToReadableStream",
|
|
4957
|
+
"renderToPipeableStream",
|
|
4958
|
+
"renderToStaticNodeStream",
|
|
4959
|
+
"resume",
|
|
4960
|
+
"resumeToPipeableStream",
|
|
4961
|
+
"resumeAndPrerender",
|
|
4962
|
+
"resumeAndPrerenderToNodeStream",
|
|
4963
|
+
"prerender",
|
|
4964
|
+
"prerenderToNodeStream",
|
|
4965
|
+
];
|
|
4966
|
+
export function serverRendererInRsc() {
|
|
4615
4967
|
const warned = new Set();
|
|
4616
4968
|
const appImporter = (ctx, importer) => {
|
|
4617
4969
|
let current = importer;
|
|
@@ -4630,24 +4982,93 @@ function serverRendererInRsc() {
|
|
|
4630
4982
|
}
|
|
4631
4983
|
return importer ? relative(process.cwd(), importer.split("?")[0]) : null;
|
|
4632
4984
|
};
|
|
4985
|
+
// Per package, so a dependency imported from twenty files is read once.
|
|
4986
|
+
const externalRenderers = new Map();
|
|
4987
|
+
// Packages already named by the warning above, so the stub's own warning
|
|
4988
|
+
// - "imported by node_modules/<package>/dist/index.mjs" - stays quiet for
|
|
4989
|
+
// them: one problem, one line.
|
|
4990
|
+
const warnedPackages = new Set();
|
|
4633
4991
|
return {
|
|
4634
4992
|
name: "rsc-kit:server-renderer",
|
|
4993
|
+
// Before Vite's own resolver, which otherwise answers react-dom/server
|
|
4994
|
+
// with React's react-server entry - a real module missing the export,
|
|
4995
|
+
// and a build that fails on MISSING_EXPORT with no mention of the fix.
|
|
4996
|
+
enforce: "pre",
|
|
4635
4997
|
applyToEnvironment: (environment) => environment.name === "rsc",
|
|
4636
|
-
resolveId(source, importer) {
|
|
4637
|
-
|
|
4998
|
+
async resolveId(source, importer) {
|
|
4999
|
+
// Only an import from a module: a resolve with no importer is Vite
|
|
5000
|
+
// asking whether the specifier exists - deciding what to externalise -
|
|
5001
|
+
// and answering that with the stub would answer the wrong question.
|
|
5002
|
+
if (SERVER_RENDERER.test(source)) {
|
|
5003
|
+
return importer ? RENDERER_STUB + encodeURIComponent(importer) : undefined;
|
|
5004
|
+
}
|
|
5005
|
+
// A bare import from app code of a package the build leaves external:
|
|
5006
|
+
// its own import of react-dom/server is never resolved here, so the
|
|
5007
|
+
// stub above never sees it. @react-email/render, imported by an
|
|
5008
|
+
// action, built clean and threw React's refusal at the first visitor.
|
|
5009
|
+
// The package is read once for the import, and the build warns the
|
|
5010
|
+
// same way it does for a direct one - naming the app file and the
|
|
5011
|
+
// package - without stubbing a module that may also be used well.
|
|
5012
|
+
if (!importer || importer.includes("/node_modules/") || importer.startsWith("\0"))
|
|
5013
|
+
return;
|
|
5014
|
+
if (!/^(?:@[\w.-]+\/)?[\w.-]+/.test(source) || source.startsWith("."))
|
|
5015
|
+
return;
|
|
5016
|
+
// The generated entries import plugin-rsc, whose ssr half imports the
|
|
5017
|
+
// renderer for its own reasons; that is the build's, not the app's.
|
|
5018
|
+
if (outDir && importer.startsWith(outDir))
|
|
4638
5019
|
return;
|
|
4639
|
-
|
|
5020
|
+
const name = source.startsWith("@") ? source.split("/").slice(0, 2).join("/") : source.split("/")[0];
|
|
5021
|
+
if (name === "react-dom" || name === "react" || name.startsWith("@rsc-kit/") || name.startsWith("@vitejs/"))
|
|
5022
|
+
return;
|
|
5023
|
+
let imports = externalRenderers.get(name);
|
|
5024
|
+
if (imports === undefined) {
|
|
5025
|
+
// Vite answers a dependency either as external, by its bare name,
|
|
5026
|
+
// or by the file it resolved to under node_modules - which of the
|
|
5027
|
+
// two depends on how the environment was set up, and both are a
|
|
5028
|
+
// dependency whose imports the build will not look inside.
|
|
5029
|
+
const resolved = await this.resolve(source, importer, { skipSelf: true });
|
|
5030
|
+
const id = resolved?.id ?? "";
|
|
5031
|
+
const marker = `/node_modules/${name}/`;
|
|
5032
|
+
const at = id.lastIndexOf(marker);
|
|
5033
|
+
const dir = resolved?.external
|
|
5034
|
+
? installedPackageDir(name, dirname(importer.split("?")[0]))
|
|
5035
|
+
: at !== -1
|
|
5036
|
+
? id.slice(0, at + marker.length - 1)
|
|
5037
|
+
: null;
|
|
5038
|
+
imports = dir !== null && importsServerRenderer(dir);
|
|
5039
|
+
externalRenderers.set(name, imports);
|
|
5040
|
+
}
|
|
5041
|
+
if (!imports)
|
|
5042
|
+
return;
|
|
5043
|
+
const message = `${relative(process.cwd(), importer.split("?")[0])} imports ${name}, which imports react-dom/server, and ` +
|
|
5044
|
+
serverRendererMessage(null).replace(/^react-dom\/server cannot run/, "that cannot run");
|
|
5045
|
+
if (!warned.has(message)) {
|
|
5046
|
+
warned.add(message);
|
|
5047
|
+
warnedPackages.add(name);
|
|
5048
|
+
this.warn(message);
|
|
5049
|
+
}
|
|
4640
5050
|
},
|
|
4641
5051
|
load(id) {
|
|
4642
5052
|
if (!id.startsWith(RENDERER_STUB))
|
|
4643
5053
|
return;
|
|
4644
5054
|
const importer = decodeURIComponent(id.slice(RENDERER_STUB.length)) || null;
|
|
4645
5055
|
const message = serverRendererMessage(appImporter(this, importer));
|
|
4646
|
-
|
|
5056
|
+
// The last node_modules segment: Bun's store nests the real package
|
|
5057
|
+
// under node_modules/.bun/<pkg>@<v>/node_modules/<pkg>.
|
|
5058
|
+
const viaPackage = [...(importer ?? "").matchAll(/\/node_modules\/((?:@[^/]+\/)?[^/]+)\//g)].at(-1)?.[1];
|
|
5059
|
+
if (this.environment.mode === "build" &&
|
|
5060
|
+
!warned.has(message) &&
|
|
5061
|
+
!(viaPackage && warnedPackages.has(viaPackage))) {
|
|
4647
5062
|
warned.add(message);
|
|
4648
5063
|
this.warn(message);
|
|
4649
5064
|
}
|
|
4650
|
-
|
|
5065
|
+
// Every name the renderer exports, each throwing the fix when called.
|
|
5066
|
+
// A module that threw when loaded took the whole server down at boot
|
|
5067
|
+
// for one action nobody had called yet, and a stub with no exports
|
|
5068
|
+
// failed the build on MISSING_EXPORT with the message scrolled past.
|
|
5069
|
+
const refuse = `() => { throw new Error(${JSON.stringify(message)}) }`;
|
|
5070
|
+
return (SERVER_RENDERER_EXPORTS.map((name) => `export const ${name} = ${refuse};`).join("\n") +
|
|
5071
|
+
`\nexport const version = "0.0.0";\nexport default { ${SERVER_RENDERER_EXPORTS.join(", ")}, version };\n`);
|
|
4651
5072
|
},
|
|
4652
5073
|
};
|
|
4653
5074
|
}
|
|
@@ -4790,8 +5211,59 @@ export default function rscKitStartup() {
|
|
|
4790
5211
|
})
|
|
4791
5212
|
}
|
|
4792
5213
|
`;
|
|
5214
|
+
/**
|
|
5215
|
+
* Where NODE_ENV=development came from, and what to do about it.
|
|
5216
|
+
*
|
|
5217
|
+
* Vite reads NODE_ENV from its env files in this order, later winning, and
|
|
5218
|
+
* only when the process did not already set it - so the last file that
|
|
5219
|
+
* names it is the one that decided. No file naming it means the shell or
|
|
5220
|
+
* `--mode` did.
|
|
5221
|
+
*/
|
|
5222
|
+
function nodeEnvSetIn(mode, envDir) {
|
|
5223
|
+
const files = [".env", ".env.local", `.env.${mode}`, `.env.${mode}.local`];
|
|
5224
|
+
let culprit = null;
|
|
5225
|
+
// The project's root, not Vite's: this plugin points Vite's root at the
|
|
5226
|
+
// build directory, and the app's .env is read from the project root - by
|
|
5227
|
+
// Nitro's dotenv loading into process.env, and by Vite where envDir says
|
|
5228
|
+
// so. Both places are looked at; the project's is where the file is.
|
|
5229
|
+
const dirs = [...new Set([projectRoot, envDir || projectRoot])];
|
|
5230
|
+
for (const dir of dirs)
|
|
5231
|
+
for (const name of files) {
|
|
5232
|
+
const path = join(dir, name);
|
|
5233
|
+
if (!existsSync(path))
|
|
5234
|
+
continue;
|
|
5235
|
+
const lines = readFileSync(path, "utf-8").split("\n");
|
|
5236
|
+
const at = lines.findIndex((line) => /^\s*(?:export\s+)?NODE_ENV\s*=/.test(line));
|
|
5237
|
+
if (at !== -1)
|
|
5238
|
+
culprit = `${path}:${at + 1}`;
|
|
5239
|
+
}
|
|
5240
|
+
return culprit;
|
|
5241
|
+
}
|
|
5242
|
+
function refuseDevelopmentBuild(config) {
|
|
5243
|
+
const culprit = nodeEnvSetIn(config.mode, config.envDir);
|
|
5244
|
+
return ("[rsc-kit] vite build is running as a development build, and the output cannot work: " +
|
|
5245
|
+
"the pages are compiled against React's development JSX runtime (jsxDEV), the server " +
|
|
5246
|
+
"bundles carry React's production build, and every route fails to render.\n\n" +
|
|
5247
|
+
(culprit
|
|
5248
|
+
? `NODE_ENV=development is set in ${culprit}. Remove that line - Vite sets NODE_ENV ` +
|
|
5249
|
+
"itself: development under `vite`, production under `vite build`."
|
|
5250
|
+
: `NODE_ENV is "${process.env.NODE_ENV ?? ""}" from the environment or --mode ` +
|
|
5251
|
+
"(mode: " + JSON.stringify(config.mode) + "). Build with NODE_ENV=production, or unset it."));
|
|
5252
|
+
}
|
|
4793
5253
|
export function rscKit(options = {}) {
|
|
4794
5254
|
resolvePaths(options);
|
|
5255
|
+
const serverExternals = [
|
|
5256
|
+
...RUNTIME_BUILTINS,
|
|
5257
|
+
...[...DEFAULT_SERVER_EXTERNALS, ...(options.serverExternalPackages ?? [])].map(externalPackage),
|
|
5258
|
+
];
|
|
5259
|
+
// Dependencies with a "use client" file that plugin-rsc would leave
|
|
5260
|
+
// external, because they never declared react as a peer. Bundled into the
|
|
5261
|
+
// server graphs so the directive is read, the same as a package that did.
|
|
5262
|
+
const bundledClientPackages = clientPackages(projectRoot);
|
|
5263
|
+
for (const name of bundledClientPackages) {
|
|
5264
|
+
console.warn(`[rsc-kit] bundling ${name}: it has "use client" files but does not declare react ` +
|
|
5265
|
+
"as a peer dependency, so its components would otherwise run on the server.");
|
|
5266
|
+
}
|
|
4795
5267
|
const routesPlugin = {
|
|
4796
5268
|
name: "rsc-kit",
|
|
4797
5269
|
// A Nitro module, which Nitro's Vite plugin collects from any plugin
|
|
@@ -4801,9 +5273,47 @@ export function rscKit(options = {}) {
|
|
|
4801
5273
|
nitro: {
|
|
4802
5274
|
name: "rsc-kit",
|
|
4803
5275
|
setup(nitro) {
|
|
4804
|
-
if (nitro.options.dev
|
|
5276
|
+
if (nitro.options.dev)
|
|
4805
5277
|
return;
|
|
5278
|
+
// External at Nitro's layer too. The Vite build leaves these as
|
|
5279
|
+
// imports, and Nitro would then bundle them into its own chunks -
|
|
5280
|
+
// which for a native package fails at load, and for a polyfill like
|
|
5281
|
+
// reflect-metadata runs it after the chunk that checks for it.
|
|
5282
|
+
// Traced, each is copied into .output/server/node_modules and
|
|
5283
|
+
// imported by the built server the way its author expected.
|
|
5284
|
+
nitro.options.traceDeps = [
|
|
5285
|
+
...(nitro.options.traceDeps ?? []),
|
|
5286
|
+
...DEFAULT_SERVER_EXTERNALS,
|
|
5287
|
+
...(options.serverExternalPackages ?? []),
|
|
5288
|
+
];
|
|
5289
|
+
// The assets, precompressed at build and served with their encoding
|
|
5290
|
+
// by Nitro's own static handler; the host gzips the rest as it
|
|
5291
|
+
// answers. Between them a bun or node server answering the internet
|
|
5292
|
+
// by itself sends nothing raw. A config that set this keeps it.
|
|
5293
|
+
// Nitro's default is a literal false, so ?? would not do; an object
|
|
5294
|
+
// is a configuration and is kept.
|
|
5295
|
+
if (typeof nitro.options.compressPublicAssets !== "object")
|
|
5296
|
+
nitro.options.compressPublicAssets = true;
|
|
4806
5297
|
nitro.options.virtual ??= {};
|
|
5298
|
+
// A polyfill a dependency checks for at module evaluation, loaded
|
|
5299
|
+
// before any service. The bundler places an external import after
|
|
5300
|
+
// the chunk imports of the module that had it, whatever the source
|
|
5301
|
+
// said, so reflect-metadata came up after tsyringe (under
|
|
5302
|
+
// @peculiar/x509, under @simplewebauthn/server) - by luck survivable
|
|
5303
|
+
// run as a directory, fatal compiled into a binary. A Nitro plugin
|
|
5304
|
+
// is evaluated by the entry itself, and the services that carry the
|
|
5305
|
+
// app are loaded lazily after it; imported by file, the polyfill is
|
|
5306
|
+
// inlined there and runs first wherever the server runs. Only for a
|
|
5307
|
+
// project whose graph has it.
|
|
5308
|
+
const polyfills = FIRST_POLYFILLS.map((name) => packageEntryInGraph(projectRoot, name)).filter((entry) => entry !== null);
|
|
5309
|
+
if (polyfills.length) {
|
|
5310
|
+
nitro.options.virtual["#rsc-kit/polyfills"] =
|
|
5311
|
+
polyfills.map((entry) => `import ${JSON.stringify(entry)};`).join("\n") +
|
|
5312
|
+
"\nexport default () => {};\n";
|
|
5313
|
+
nitro.options.plugins = ["#rsc-kit/polyfills", ...(nitro.options.plugins ?? [])];
|
|
5314
|
+
}
|
|
5315
|
+
if (!instrumentationFile())
|
|
5316
|
+
return;
|
|
4807
5317
|
nitro.options.virtual["#rsc-kit/startup"] = STARTUP_PLUGIN;
|
|
4808
5318
|
nitro.options.plugins = [...(nitro.options.plugins ?? []), "#rsc-kit/startup"];
|
|
4809
5319
|
},
|
|
@@ -4819,6 +5329,7 @@ export function rscKit(options = {}) {
|
|
|
4819
5329
|
apiRoutes.clear();
|
|
4820
5330
|
discover(appDir);
|
|
4821
5331
|
registerMetadataRoutes(appDir);
|
|
5332
|
+
registerOpenApiRoutes();
|
|
4822
5333
|
siteHosts = ownHosts(rootMetadataBase(appDir), hostsOption);
|
|
4823
5334
|
// Silent when it worked. The names were printed on every dev start and
|
|
4824
5335
|
// every build — thirty of them for a middling app, above the output that
|
|
@@ -4998,6 +5509,11 @@ export function rscKit(options = {}) {
|
|
|
4998
5509
|
alias: aliasEntries(),
|
|
4999
5510
|
},
|
|
5000
5511
|
build: { emptyOutDir: true },
|
|
5512
|
+
// What reaches the browser. Vite's own VITE_ prefix, and PUBLIC_ - the
|
|
5513
|
+
// scaffold's spelling, and Next's minus its brand - so a variable
|
|
5514
|
+
// named for a port reads through import.meta.env without a config
|
|
5515
|
+
// line. A config that set its own prefixes keeps them; Vite merges.
|
|
5516
|
+
envPrefix: ["VITE_", "PUBLIC_"],
|
|
5001
5517
|
environments: {
|
|
5002
5518
|
// Server bundles — stay under the (non-public) out dir. `bun` and
|
|
5003
5519
|
// `bun:*` are the runtime's own modules, like `node:*`: nothing to
|
|
@@ -5007,17 +5523,21 @@ export function rscKit(options = {}) {
|
|
|
5007
5523
|
build: {
|
|
5008
5524
|
rollupOptions: {
|
|
5009
5525
|
input: { index: join(genDir, "entry.rsc.tsx") },
|
|
5010
|
-
external:
|
|
5526
|
+
external: serverExternals,
|
|
5011
5527
|
},
|
|
5012
5528
|
},
|
|
5529
|
+
resolve: { noExternal: bundledClientPackages },
|
|
5530
|
+
optimizeDeps: { exclude: bundledClientPackages },
|
|
5013
5531
|
},
|
|
5014
5532
|
ssr: {
|
|
5015
5533
|
build: {
|
|
5016
5534
|
rollupOptions: {
|
|
5017
5535
|
input: { index: join(genDir, "entry.ssr.tsx") },
|
|
5018
|
-
external:
|
|
5536
|
+
external: serverExternals,
|
|
5019
5537
|
},
|
|
5020
5538
|
},
|
|
5539
|
+
resolve: { noExternal: bundledClientPackages },
|
|
5540
|
+
optimizeDeps: { exclude: bundledClientPackages },
|
|
5021
5541
|
},
|
|
5022
5542
|
// Client bundle — emitted into public/ for the web server to serve.
|
|
5023
5543
|
client: {
|
|
@@ -5239,8 +5759,12 @@ export function rscKit(options = {}) {
|
|
|
5239
5759
|
// when the directory is not there. Only under Nitro, whose presets are
|
|
5240
5760
|
// the ones without a disk; on its own the plugin serves from outDir.
|
|
5241
5761
|
if (clientOut && existsSync(staticDir)) {
|
|
5242
|
-
const { inlineModuleName, inlineModuleSource } = await import("./files.js");
|
|
5762
|
+
const { inlineModuleName, inlineModuleSource, compileEntrySource } = await import("./files.js");
|
|
5243
5763
|
writeFileSync(join(dirname(staticDir), inlineModuleName(NITRO_STATIC_DIR)), await inlineModuleSource(staticDir));
|
|
5764
|
+
// And the entry a binary is compiled from, so the pages above end up
|
|
5765
|
+
// inside it rather than rendered live: bun build --compile
|
|
5766
|
+
// .output/server/compile.mjs. The scaffold's script names it.
|
|
5767
|
+
writeFileSync(join(dirname(staticDir), "compile.mjs"), compileEntrySource(NITRO_STATIC_DIR));
|
|
5244
5768
|
}
|
|
5245
5769
|
// Manifest first. The service worker precaches whatever it finds in this
|
|
5246
5770
|
// directory, so writing it afterwards leaves it out of the list — and an
|
|
@@ -5254,6 +5778,24 @@ export function rscKit(options = {}) {
|
|
|
5254
5778
|
}
|
|
5255
5779
|
},
|
|
5256
5780
|
configResolved(config) {
|
|
5781
|
+
// A build that is not a production build cannot work here, and the
|
|
5782
|
+
// way it fails is opaque: plugin-react emits the development JSX
|
|
5783
|
+
// runtime (jsxDEV), the server bundles resolve React's production
|
|
5784
|
+
// build, and every route fails to render with React's "message
|
|
5785
|
+
// omitted in production builds". The usual cause is NODE_ENV=development
|
|
5786
|
+
// in a .env file, which Vite honours - a line the scaffold itself once
|
|
5787
|
+
// wrote. Named, with the file and line, before any of that happens.
|
|
5788
|
+
//
|
|
5789
|
+
// Refused rather than overridden, because a plugin cannot override it:
|
|
5790
|
+
// Vite notes whether NODE_ENV was set before it loads the config file
|
|
5791
|
+
// and applies the .env value after the config hooks have run, so an
|
|
5792
|
+
// assignment here is overwritten - and patching the resolved config
|
|
5793
|
+
// afterwards leaves the client bundle's process.env.NODE_ENV and
|
|
5794
|
+
// import.meta.env.DEV already decided, which is a production server
|
|
5795
|
+
// serving a development client.
|
|
5796
|
+
if (config.command === "build" && !config.isProduction) {
|
|
5797
|
+
throw new Error(refuseDevelopmentBuild(config));
|
|
5798
|
+
}
|
|
5257
5799
|
isWatch = config.build?.watch != null;
|
|
5258
5800
|
resolvedConfig = config;
|
|
5259
5801
|
const nitroMain = config.plugins.find((p) => p.name === "nitro:main");
|
|
@@ -5307,6 +5849,7 @@ export function rscKit(options = {}) {
|
|
|
5307
5849
|
useSsrModules(),
|
|
5308
5850
|
serverRendererInRsc(),
|
|
5309
5851
|
metadataRoutesPlugin(),
|
|
5852
|
+
openApiPlugin(),
|
|
5310
5853
|
extendableClientReferences(),
|
|
5311
5854
|
typecheckPlugin(),
|
|
5312
5855
|
clientImportsAudit(),
|