@rsc-kit/core 0.18.1 → 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/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/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/openapi.d.ts +74 -0
- package/dist/openapi.js +172 -0
- package/dist/openapi.js.map +1 -0
- 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/routes.d.ts +15 -1
- package/dist/routes.js.map +1 -1
- package/dist/testing.d.ts +14 -0
- package/dist/testing.js +56 -2
- package/dist/testing.js.map +1 -1
- package/dist/vite.d.ts +69 -1
- package/dist/vite.js +451 -45
- package/dist/vite.js.map +1 -1
- package/package.json +5 -1
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;
|
|
@@ -52,6 +53,12 @@ const RUNTIME_BUILTINS = ["bun", /^bun:/];
|
|
|
52
53
|
* Next keeps the same list under serverExternalPackages, for the same reason.
|
|
53
54
|
* `rscKit({ serverExternalPackages })` adds to it.
|
|
54
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"];
|
|
55
62
|
const DEFAULT_SERVER_EXTERNALS = [
|
|
56
63
|
"sharp",
|
|
57
64
|
"canvas",
|
|
@@ -78,6 +85,13 @@ const DEFAULT_SERVER_EXTERNALS = [
|
|
|
78
85
|
"oslo",
|
|
79
86
|
"@resvg/resvg-js",
|
|
80
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",
|
|
81
95
|
];
|
|
82
96
|
/** A package name as a rollup external: the package and every subpath of it. */
|
|
83
97
|
function externalPackage(name) {
|
|
@@ -156,6 +170,8 @@ let siteHosts = [];
|
|
|
156
170
|
let hostsOption = [];
|
|
157
171
|
let barrelImports = true;
|
|
158
172
|
let identify = true;
|
|
173
|
+
/** Resolved from RscKitOptions.openapi; null when off. */
|
|
174
|
+
let openapi = null;
|
|
159
175
|
let foundAssets = {
|
|
160
176
|
favicon: null,
|
|
161
177
|
icons: [],
|
|
@@ -218,6 +234,12 @@ function fileHostActions(root) {
|
|
|
218
234
|
return {};
|
|
219
235
|
try {
|
|
220
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 {};
|
|
221
243
|
if (parsed === null ||
|
|
222
244
|
typeof parsed !== "object" ||
|
|
223
245
|
Array.isArray(parsed)) {
|
|
@@ -243,16 +265,23 @@ function fileHostActions(root) {
|
|
|
243
265
|
* two cannot drift.
|
|
244
266
|
*/
|
|
245
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
|
+
];
|
|
246
276
|
if (!packageAlias)
|
|
247
|
-
return
|
|
277
|
+
return entries;
|
|
248
278
|
if (existsSync(join(projectRoot, "node_modules", packageAlias)))
|
|
249
|
-
return
|
|
250
|
-
|
|
251
|
-
{
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
];
|
|
279
|
+
return entries;
|
|
280
|
+
entries.push({
|
|
281
|
+
find: new RegExp("^" + packageAlias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "/(.*)$"),
|
|
282
|
+
replacement: join(packageDir, "js") + "/$1",
|
|
283
|
+
});
|
|
284
|
+
return entries;
|
|
256
285
|
}
|
|
257
286
|
function resolvePaths(options) {
|
|
258
287
|
projectRoot = resolve(options.projectRoot || process.env.RSC_PROJECT_ROOT || process.cwd());
|
|
@@ -321,6 +350,12 @@ function resolvePaths(options) {
|
|
|
321
350
|
hostsOption = options.hosts ?? [];
|
|
322
351
|
barrelImports = options.barrelImports !== false;
|
|
323
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;
|
|
324
359
|
inlineStylesheets = options.inlineStylesheets ?? "auto";
|
|
325
360
|
maxActionBody = options.maxActionBody;
|
|
326
361
|
// One place, and it is the file. A plugin option as well would be the same
|
|
@@ -765,7 +800,32 @@ self.addEventListener('install', (event) => {
|
|
|
765
800
|
// it is served by the host out of the prerendered directory, which this
|
|
766
801
|
// worker cannot see. Added here so it is in the cache before it is
|
|
767
802
|
// needed, which is the only moment it cannot be fetched.
|
|
768
|
-
.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
|
+
)
|
|
769
829
|
.then(() => self.skipWaiting()),
|
|
770
830
|
)
|
|
771
831
|
})
|
|
@@ -774,9 +834,16 @@ self.addEventListener('activate', (event) => {
|
|
|
774
834
|
event.waitUntil(
|
|
775
835
|
caches
|
|
776
836
|
.keys()
|
|
777
|
-
.then((keys) =>
|
|
778
|
-
|
|
779
|
-
|
|
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)),
|
|
780
847
|
)
|
|
781
848
|
})
|
|
782
849
|
|
|
@@ -821,6 +888,37 @@ const keyFor = (request) => {
|
|
|
821
888
|
return new Request(url, { headers: request.headers })
|
|
822
889
|
}
|
|
823
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
|
+
|
|
824
922
|
self.addEventListener('fetch', (event) => {
|
|
825
923
|
const request = event.request
|
|
826
924
|
const url = new URL(request.url)
|
|
@@ -855,7 +953,7 @@ self.addEventListener('fetch', (event) => {
|
|
|
855
953
|
// differently for every value of it.
|
|
856
954
|
if (FROZEN.has(url.pathname.replace(/\\/+$/, '') || '/') && !url.search) {
|
|
857
955
|
event.respondWith(
|
|
858
|
-
caches.match(keyFor(request)).then((hit) => {
|
|
956
|
+
caches.match(keyFor(request), MATCH).then((hit) => {
|
|
859
957
|
const fresh = fetch(request)
|
|
860
958
|
.then((response) => {
|
|
861
959
|
if (mayStore(response)) {
|
|
@@ -865,10 +963,17 @@ self.addEventListener('fetch', (event) => {
|
|
|
865
963
|
|
|
866
964
|
return response
|
|
867
965
|
})
|
|
868
|
-
.catch((error) => {
|
|
966
|
+
.catch(async (error) => {
|
|
869
967
|
if (hit) return hit
|
|
870
968
|
|
|
871
|
-
|
|
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)
|
|
872
977
|
})
|
|
873
978
|
|
|
874
979
|
// Not awaited when there is a hit: the point is that the visitor does
|
|
@@ -893,11 +998,11 @@ self.addEventListener('fetch', (event) => {
|
|
|
893
998
|
// cached, and a reload with no network has the markup and nothing to
|
|
894
999
|
// hydrate it with.
|
|
895
1000
|
//
|
|
896
|
-
// \`X-RSC:
|
|
1001
|
+
// \`X-RSC: 1\` and no segments header is exactly what a fresh boot
|
|
897
1002
|
// sends, which is what makes this entry the one it finds: the server
|
|
898
1003
|
// varies on those, and the Cache API matches on the same.
|
|
899
1004
|
if (request.mode === 'navigate') {
|
|
900
|
-
const warm = new Request(request.url, { headers: { 'X-RSC': '
|
|
1005
|
+
const warm = new Request(request.url, { headers: { 'X-RSC': '1' } })
|
|
901
1006
|
|
|
902
1007
|
fetch(warm)
|
|
903
1008
|
.then((payload) => {
|
|
@@ -932,7 +1037,7 @@ self.addEventListener('fetch', (event) => {
|
|
|
932
1037
|
return response
|
|
933
1038
|
})
|
|
934
1039
|
.catch(async () => {
|
|
935
|
-
const hit = await caches.match(keyFor(request))
|
|
1040
|
+
const hit = await caches.match(keyFor(request), MATCH)
|
|
936
1041
|
|
|
937
1042
|
if (hit) return hit
|
|
938
1043
|
|
|
@@ -947,11 +1052,9 @@ self.addEventListener('fetch', (event) => {
|
|
|
947
1052
|
// stand in for another: it is about being offline, not about the url it
|
|
948
1053
|
// appears under. Navigations only — a payload request answered with a
|
|
949
1054
|
// document would be decoded as one and throw.
|
|
950
|
-
|
|
951
|
-
const page = await caches.match(OFFLINE_URL)
|
|
1055
|
+
const stood = await standIn(request)
|
|
952
1056
|
|
|
953
|
-
|
|
954
|
-
}
|
|
1057
|
+
if (stood) return stood
|
|
955
1058
|
|
|
956
1059
|
// Letting it fail says what is true, and a page already open is
|
|
957
1060
|
// unaffected.
|
|
@@ -1066,17 +1169,24 @@ function writeWebManifest(clientDir, options) {
|
|
|
1066
1169
|
* precached and found wanting at the one moment it matters, and the build says
|
|
1067
1170
|
* which read did it.
|
|
1068
1171
|
*/
|
|
1069
|
-
function offlineFallback(frozen, results) {
|
|
1172
|
+
export function offlineFallback(frozen, results) {
|
|
1070
1173
|
const found = results.find((r) => r.url === "/offline");
|
|
1071
1174
|
if (!found)
|
|
1072
1175
|
return null;
|
|
1073
1176
|
if (!frozen.includes("/offline")) {
|
|
1074
1177
|
// The reason already reads "dynamic — called cookies()", and this sentence
|
|
1075
1178
|
// has said "not stored" by the time it gets there, so the prefix would say
|
|
1076
|
-
// it twice with a dash in the middle of both.
|
|
1077
|
-
|
|
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;
|
|
1078
1188
|
log("offline: /offline cannot be the fallback" +
|
|
1079
|
-
(why ? `, because
|
|
1189
|
+
(why ? `, because ${why}` : "") +
|
|
1080
1190
|
". A fallback has to be servable with no network at all.");
|
|
1081
1191
|
return null;
|
|
1082
1192
|
}
|
|
@@ -1102,6 +1212,24 @@ function copyServiceWorkerExtra(clientDir) {
|
|
|
1102
1212
|
copyFileSync(source, join(clientDir, "sw-app.js"));
|
|
1103
1213
|
return "/sw-app.js";
|
|
1104
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
|
+
}
|
|
1105
1233
|
function writeServiceWorker(clientDir, frozen = [], offlineUrl = null) {
|
|
1106
1234
|
if (!existsSync(clientDir))
|
|
1107
1235
|
return;
|
|
@@ -1120,7 +1248,7 @@ function writeServiceWorker(clientDir, frozen = [], offlineUrl = null) {
|
|
|
1120
1248
|
const path = join(dir, entry.name);
|
|
1121
1249
|
if (entry.isDirectory())
|
|
1122
1250
|
walk(path, `${prefix}${entry.name}/`);
|
|
1123
|
-
else
|
|
1251
|
+
else if (bootsTheApp(`${prefix}${entry.name}`))
|
|
1124
1252
|
files.push(`${prefix}${entry.name}`);
|
|
1125
1253
|
}
|
|
1126
1254
|
};
|
|
@@ -1723,8 +1851,20 @@ function componentName(absPath) {
|
|
|
1723
1851
|
const rel = relative(sourceDir, absPath).replace(/\\/g, "/");
|
|
1724
1852
|
return rel.replace(/\.(tsx|jsx|ts|js)$/, "");
|
|
1725
1853
|
}
|
|
1726
|
-
|
|
1727
|
-
|
|
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))));
|
|
1728
1868
|
}
|
|
1729
1869
|
const components = new Map();
|
|
1730
1870
|
/**
|
|
@@ -1736,6 +1876,48 @@ const components = new Map();
|
|
|
1736
1876
|
* a default component.
|
|
1737
1877
|
*/
|
|
1738
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
|
+
}
|
|
1739
1921
|
/** Files beside the root layout served at the root as they are: robots.txt, a hand-written sitemap.xml, humans.txt. */
|
|
1740
1922
|
let rootFiles = [];
|
|
1741
1923
|
const METADATA_ROUTE_ID = "virtual:rsc-kit/metadata-route/";
|
|
@@ -2071,6 +2253,9 @@ const NITRO_HANDLER_OPTIONS = ` props: (match, request) => ({
|
|
|
2071
2253
|
...Object.fromEntries(new URL(request.url).searchParams),
|
|
2072
2254
|
}),
|
|
2073
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,
|
|
2074
2259
|
`;
|
|
2075
2260
|
const NITRO_HOST_CALLS = `
|
|
2076
2261
|
let hostInstalled = false
|
|
@@ -2199,12 +2384,12 @@ import { DefaultRouteError } from ${JSON.stringify(join(packageDir, "js/DefaultR
|
|
|
2199
2384
|
import { searchParams as requestSearchParams } from ${JSON.stringify(join(packageDir, "request"))}
|
|
2200
2385
|
import { parseParams, parseSearchParams, parseBody, isSearchParamsError, isBodyError } from ${JSON.stringify(join(packageDir, "routeSchema"))}
|
|
2201
2386
|
import { notFoundDigest, isNotFoundSignal } from ${JSON.stringify(join(packageDir, "notFound"))}
|
|
2202
|
-
import { noteRequestRead } from ${JSON.stringify(join(packageDir, "request"))}
|
|
2387
|
+
import { noteRequestRead, urlOf } from ${JSON.stringify(join(packageDir, "request"))}
|
|
2203
2388
|
import { redirectDigest } from ${JSON.stringify(join(packageDir, "redirectDigest"))}
|
|
2204
2389
|
import { createRscHandler } from ${JSON.stringify(join(packageDir, "host"))}
|
|
2205
2390
|
import { httpHostCalls } from ${JSON.stringify(join(packageDir, "hostCalls"))}
|
|
2206
2391
|
import { prerenderedBeside } from ${JSON.stringify(join(packageDir, "files"))}
|
|
2207
|
-
import { renderToReadableStream, decodeReply, loadServerAction } from '@vitejs/plugin-rsc/rsc'
|
|
2392
|
+
import { renderToReadableStream, decodeReply, decodeAction, decodeFormState, loadServerAction } from '@vitejs/plugin-rsc/rsc'
|
|
2208
2393
|
import { isQuery, queryCacheControl, isQueryValidationError } from ${JSON.stringify(join(packageDir, "query"))}
|
|
2209
2394
|
import { isActionValidationError, isClientBuilt } from ${JSON.stringify(join(packageDir, "action"))}
|
|
2210
2395
|
import { noteFallback as noteCaughtRead } from ${JSON.stringify(join(packageDir, "request"))}
|
|
@@ -2364,7 +2549,10 @@ export async function handleApiRoute(
|
|
|
2364
2549
|
// reaches for the value.
|
|
2365
2550
|
noteRequestRead('searchParams')
|
|
2366
2551
|
|
|
2367
|
-
|
|
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)
|
|
2368
2556
|
}),
|
|
2369
2557
|
body: hasBody(method) ? lazily(() => parseBody(mod.body, request)) : undefined,
|
|
2370
2558
|
}
|
|
@@ -3315,6 +3503,54 @@ export async function handleRscHtmlStream(
|
|
|
3315
3503
|
return { htmlStream, rscPayloadPromise, clientChunks: {} }
|
|
3316
3504
|
}
|
|
3317
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
|
+
|
|
3318
3554
|
/**
|
|
3319
3555
|
* Finish a shell that was frozen at build time.
|
|
3320
3556
|
*
|
|
@@ -3941,7 +4177,7 @@ export async function handleRscPprShell(
|
|
|
3941
4177
|
// say so on the route's line; the digest goes into the document for the
|
|
3942
4178
|
// browser to recognise.
|
|
3943
4179
|
if ((e as { digest?: string } | null)?.digest === 'rsc-kit:search-params-fallback') {
|
|
3944
|
-
const where = /at ([A-
|
|
4180
|
+
const where = /at ([A-Z_$][\\w$]*)/.exec(info?.componentStack ?? '')?.[1]
|
|
3945
4181
|
noteCaughtRead('useSearchParams()' + (where ? ' in ' + where : ''))
|
|
3946
4182
|
return 'rsc-kit:search-params-fallback'
|
|
3947
4183
|
}
|
|
@@ -4091,6 +4327,7 @@ async function serve(request: Request): Promise<Response> {
|
|
|
4091
4327
|
handleRsc,
|
|
4092
4328
|
handleRscStream,
|
|
4093
4329
|
handleRscHtmlStream,
|
|
4330
|
+
handleRscFormPost,
|
|
4094
4331
|
handleRscRevalidate,
|
|
4095
4332
|
handleRscPayload,
|
|
4096
4333
|
handleRscPprShell,
|
|
@@ -4181,6 +4418,8 @@ function generateEntrySsr() {
|
|
|
4181
4418
|
const devUrls = join(packageDir, "devUrls");
|
|
4182
4419
|
const request = join(packageDir, "request");
|
|
4183
4420
|
const fallbackReport = join(packageDir, "js/fallbackReport");
|
|
4421
|
+
const redirectDigestModule = join(packageDir, "redirectDigest");
|
|
4422
|
+
const notFoundModule = join(packageDir, "notFound");
|
|
4184
4423
|
return `// GENERATED by rscKit() — do not edit.
|
|
4185
4424
|
import { createFromReadableStream } from '@vitejs/plugin-rsc/ssr'
|
|
4186
4425
|
import { renderToReadableStream, resume } from 'react-dom/server.edge'
|
|
@@ -4188,6 +4427,8 @@ import { prerender } from 'react-dom/static.edge'
|
|
|
4188
4427
|
import { rewriteViteDevUrlStream } from ${JSON.stringify(devUrls)}
|
|
4189
4428
|
import { noteFallback } from ${JSON.stringify(request)}
|
|
4190
4429
|
import { cancelledByConsumer, caughtByLoading } from ${JSON.stringify(fallbackReport)}
|
|
4430
|
+
import { parseRedirectDigest } from ${JSON.stringify(redirectDigestModule)}
|
|
4431
|
+
import { isNotFoundDigest } from ${JSON.stringify(notFoundModule)}
|
|
4191
4432
|
|
|
4192
4433
|
// Set only by the dev server. @vitejs/plugin-rsc emits its bootstrap and CSS
|
|
4193
4434
|
// links root-relative in dev, which would send the browser to the host for
|
|
@@ -4199,6 +4440,7 @@ export async function handleSsr(
|
|
|
4199
4440
|
nonce?: string,
|
|
4200
4441
|
onError?: (error: unknown) => void,
|
|
4201
4442
|
bootstrap = true,
|
|
4443
|
+
formState?: unknown,
|
|
4202
4444
|
): Promise<ReadableStream> {
|
|
4203
4445
|
const root = await createFromReadableStream(rscStream)
|
|
4204
4446
|
|
|
@@ -4216,6 +4458,9 @@ export async function handleSsr(
|
|
|
4216
4458
|
bootstrapScriptContent,
|
|
4217
4459
|
nonce,
|
|
4218
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 } : {}),
|
|
4219
4464
|
})
|
|
4220
4465
|
|
|
4221
4466
|
return DEV_ORIGIN ? rewriteViteDevUrlStream(html, DEV_ORIGIN) : html
|
|
@@ -4255,7 +4500,7 @@ export async function handleSsrPrerender(
|
|
|
4255
4500
|
// caught at a boundary is: the build attaches it to the route.
|
|
4256
4501
|
onError: (error: unknown, info?: { componentStack?: string }) => {
|
|
4257
4502
|
if ((error as { digest?: string } | null)?.digest !== 'rsc-kit:search-params-fallback') return
|
|
4258
|
-
const where = /at ([A-
|
|
4503
|
+
const where = /at ([A-Z_$][\\w$]*)/.exec(info?.componentStack ?? '')?.[1]
|
|
4259
4504
|
noteFallback('useSearchParams()' + (where ? ' in ' + where : ''))
|
|
4260
4505
|
return 'rsc-kit:search-params-fallback'
|
|
4261
4506
|
},
|
|
@@ -4283,11 +4528,18 @@ function reportRenderError(phase: 'ssr' | 'resume') {
|
|
|
4283
4528
|
|
|
4284
4529
|
const digest = (error as { digest?: string } | null)?.digest
|
|
4285
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
|
+
|
|
4286
4538
|
if (digest === 'rsc-kit:search-params-fallback') {
|
|
4287
4539
|
// The component is the first frame of React's stack. Noted on the
|
|
4288
4540
|
// request so the build attaches it to the route; printed as one line,
|
|
4289
4541
|
// not a stack, so the server says which boundary it wants.
|
|
4290
|
-
const where = /at ([A-
|
|
4542
|
+
const where = /at ([A-Z_$][\\w$]*)/.exec(info?.componentStack ?? '')?.[1]
|
|
4291
4543
|
|
|
4292
4544
|
noteFallback('useSearchParams()' + (where ? ' in ' + where : ''))
|
|
4293
4545
|
|
|
@@ -4381,6 +4633,9 @@ import { refresh } from ${JSON.stringify(refreshModule)}
|
|
|
4381
4633
|
createViteRscApp(document, ${JSON.stringify(interceptManifest())}, ${JSON.stringify({
|
|
4382
4634
|
staticPayloads: staticPayloads || null,
|
|
4383
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)),
|
|
4384
4639
|
})})
|
|
4385
4640
|
|
|
4386
4641
|
// A server component is not a module the browser has, so Vite cannot replace
|
|
@@ -4411,12 +4666,19 @@ ${offline
|
|
|
4411
4666
|
// worker answering from a cache in front of it turns every edit into a
|
|
4412
4667
|
// question about which copy you are looking at.
|
|
4413
4668
|
if ('serviceWorker' in navigator && import.meta.env.PROD) {
|
|
4414
|
-
|
|
4669
|
+
const register = () => {
|
|
4415
4670
|
void navigator.serviceWorker.register('/sw.js').catch(() => {
|
|
4416
4671
|
// A worker that will not register is not a reason for the page to fail.
|
|
4417
4672
|
// The app works; it just will not survive being reloaded offline.
|
|
4418
4673
|
})
|
|
4419
|
-
}
|
|
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)
|
|
4420
4682
|
}
|
|
4421
4683
|
`
|
|
4422
4684
|
: ""}`;
|
|
@@ -4687,7 +4949,21 @@ function useSsrModules() {
|
|
|
4687
4949
|
* directive that moves it. The build says the same once, as a warning.
|
|
4688
4950
|
*/
|
|
4689
4951
|
const RENDERER_STUB = "\0rsc-kit:react-dom-server?from=";
|
|
4690
|
-
|
|
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() {
|
|
4691
4967
|
const warned = new Set();
|
|
4692
4968
|
const appImporter = (ctx, importer) => {
|
|
4693
4969
|
let current = importer;
|
|
@@ -4706,24 +4982,93 @@ function serverRendererInRsc() {
|
|
|
4706
4982
|
}
|
|
4707
4983
|
return importer ? relative(process.cwd(), importer.split("?")[0]) : null;
|
|
4708
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();
|
|
4709
4991
|
return {
|
|
4710
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",
|
|
4711
4997
|
applyToEnvironment: (environment) => environment.name === "rsc",
|
|
4712
|
-
resolveId(source, importer) {
|
|
4713
|
-
|
|
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"))
|
|
4714
5013
|
return;
|
|
4715
|
-
|
|
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))
|
|
5019
|
+
return;
|
|
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
|
+
}
|
|
4716
5050
|
},
|
|
4717
5051
|
load(id) {
|
|
4718
5052
|
if (!id.startsWith(RENDERER_STUB))
|
|
4719
5053
|
return;
|
|
4720
5054
|
const importer = decodeURIComponent(id.slice(RENDERER_STUB.length)) || null;
|
|
4721
5055
|
const message = serverRendererMessage(appImporter(this, importer));
|
|
4722
|
-
|
|
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))) {
|
|
4723
5062
|
warned.add(message);
|
|
4724
5063
|
this.warn(message);
|
|
4725
5064
|
}
|
|
4726
|
-
|
|
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`);
|
|
4727
5072
|
},
|
|
4728
5073
|
};
|
|
4729
5074
|
}
|
|
@@ -4911,6 +5256,14 @@ export function rscKit(options = {}) {
|
|
|
4911
5256
|
...RUNTIME_BUILTINS,
|
|
4912
5257
|
...[...DEFAULT_SERVER_EXTERNALS, ...(options.serverExternalPackages ?? [])].map(externalPackage),
|
|
4913
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
|
+
}
|
|
4914
5267
|
const routesPlugin = {
|
|
4915
5268
|
name: "rsc-kit",
|
|
4916
5269
|
// A Nitro module, which Nitro's Vite plugin collects from any plugin
|
|
@@ -4920,9 +5273,47 @@ export function rscKit(options = {}) {
|
|
|
4920
5273
|
nitro: {
|
|
4921
5274
|
name: "rsc-kit",
|
|
4922
5275
|
setup(nitro) {
|
|
4923
|
-
if (nitro.options.dev
|
|
5276
|
+
if (nitro.options.dev)
|
|
4924
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;
|
|
4925
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;
|
|
4926
5317
|
nitro.options.virtual["#rsc-kit/startup"] = STARTUP_PLUGIN;
|
|
4927
5318
|
nitro.options.plugins = [...(nitro.options.plugins ?? []), "#rsc-kit/startup"];
|
|
4928
5319
|
},
|
|
@@ -4938,6 +5329,7 @@ export function rscKit(options = {}) {
|
|
|
4938
5329
|
apiRoutes.clear();
|
|
4939
5330
|
discover(appDir);
|
|
4940
5331
|
registerMetadataRoutes(appDir);
|
|
5332
|
+
registerOpenApiRoutes();
|
|
4941
5333
|
siteHosts = ownHosts(rootMetadataBase(appDir), hostsOption);
|
|
4942
5334
|
// Silent when it worked. The names were printed on every dev start and
|
|
4943
5335
|
// every build — thirty of them for a middling app, above the output that
|
|
@@ -5117,6 +5509,11 @@ export function rscKit(options = {}) {
|
|
|
5117
5509
|
alias: aliasEntries(),
|
|
5118
5510
|
},
|
|
5119
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_"],
|
|
5120
5517
|
environments: {
|
|
5121
5518
|
// Server bundles — stay under the (non-public) out dir. `bun` and
|
|
5122
5519
|
// `bun:*` are the runtime's own modules, like `node:*`: nothing to
|
|
@@ -5129,6 +5526,8 @@ export function rscKit(options = {}) {
|
|
|
5129
5526
|
external: serverExternals,
|
|
5130
5527
|
},
|
|
5131
5528
|
},
|
|
5529
|
+
resolve: { noExternal: bundledClientPackages },
|
|
5530
|
+
optimizeDeps: { exclude: bundledClientPackages },
|
|
5132
5531
|
},
|
|
5133
5532
|
ssr: {
|
|
5134
5533
|
build: {
|
|
@@ -5137,6 +5536,8 @@ export function rscKit(options = {}) {
|
|
|
5137
5536
|
external: serverExternals,
|
|
5138
5537
|
},
|
|
5139
5538
|
},
|
|
5539
|
+
resolve: { noExternal: bundledClientPackages },
|
|
5540
|
+
optimizeDeps: { exclude: bundledClientPackages },
|
|
5140
5541
|
},
|
|
5141
5542
|
// Client bundle — emitted into public/ for the web server to serve.
|
|
5142
5543
|
client: {
|
|
@@ -5358,8 +5759,12 @@ export function rscKit(options = {}) {
|
|
|
5358
5759
|
// when the directory is not there. Only under Nitro, whose presets are
|
|
5359
5760
|
// the ones without a disk; on its own the plugin serves from outDir.
|
|
5360
5761
|
if (clientOut && existsSync(staticDir)) {
|
|
5361
|
-
const { inlineModuleName, inlineModuleSource } = await import("./files.js");
|
|
5762
|
+
const { inlineModuleName, inlineModuleSource, compileEntrySource } = await import("./files.js");
|
|
5362
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));
|
|
5363
5768
|
}
|
|
5364
5769
|
// Manifest first. The service worker precaches whatever it finds in this
|
|
5365
5770
|
// directory, so writing it afterwards leaves it out of the list — and an
|
|
@@ -5444,6 +5849,7 @@ export function rscKit(options = {}) {
|
|
|
5444
5849
|
useSsrModules(),
|
|
5445
5850
|
serverRendererInRsc(),
|
|
5446
5851
|
metadataRoutesPlugin(),
|
|
5852
|
+
openApiPlugin(),
|
|
5447
5853
|
extendableClientReferences(),
|
|
5448
5854
|
typecheckPlugin(),
|
|
5449
5855
|
clientImportsAudit(),
|