@rsc-kit/core 0.18.1 → 0.20.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 +25 -7
- 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 +21 -4
- package/dist/js/Form.js +110 -83
- package/dist/js/Form.js.map +1 -1
- package/dist/js/Link.d.ts +5 -5
- package/dist/js/Link.js.map +1 -1
- package/dist/js/RedirectBoundary.js.map +1 -1
- package/dist/js/RouteErrorBoundary.d.ts +6 -2
- package/dist/js/RouteErrorBoundary.js +10 -0
- package/dist/js/RouteErrorBoundary.js.map +1 -1
- package/dist/js/SlotBoundary.d.ts +7 -1
- package/dist/js/SlotBoundary.js +9 -1
- package/dist/js/SlotBoundary.js.map +1 -1
- package/dist/js/createViteRscApp.d.ts +1 -0
- package/dist/js/createViteRscApp.js +54 -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 +6 -3
- package/dist/js/navigate.js +57 -7
- package/dist/js/navigate.js.map +1 -1
- package/dist/js/nuqs.js.map +1 -1
- package/dist/js/router.d.ts +3 -3
- package/dist/js/router.js.map +1 -1
- package/dist/js/updateStore.js +8 -2
- package/dist/js/updateStore.js.map +1 -1
- package/dist/manifest.d.ts +6 -0
- package/dist/manifest.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 +4 -4
- 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 +17 -6
- 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/useSsr.d.ts +11 -0
- package/dist/useSsr.js +15 -0
- package/dist/useSsr.js.map +1 -1
- package/dist/vite.d.ts +69 -1
- package/dist/vite.js +481 -51
- package/dist/vite.js.map +1 -1
- package/package.json +5 -1
package/dist/vite.js
CHANGED
|
@@ -30,8 +30,9 @@ import { serverImportsOfClientPackages } from "./clientImports.js";
|
|
|
30
30
|
import { automaticSitemap, METADATA_ROUTES, ROOT_FILES, rootFileType, } from "./metadataRoutes.js";
|
|
31
31
|
import { ownHosts } from "./hostRouting.js";
|
|
32
32
|
import { unrollBarrelImports } from "./barrelImports.js";
|
|
33
|
-
import { serverRendererMessage, SERVER_RENDERER, ssrProxyModule, UseSsrError, } from "./useSsr.js";
|
|
33
|
+
import { serverRendererMessage, SERVER_RENDERER, ssrProxyModule, UseSsrError, withoutSsrDirective } 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
|
|
@@ -550,10 +585,16 @@ function routeManifest() {
|
|
|
550
585
|
},
|
|
551
586
|
routes,
|
|
552
587
|
intercepts,
|
|
553
|
-
apis: [...apiRoutes.values()].map(({ name, methods, generated }) => ({
|
|
588
|
+
apis: [...apiRoutes.values()].map(({ name, methods, generated, absPath }) => ({
|
|
554
589
|
name,
|
|
555
590
|
segments: urlSegments(name),
|
|
556
591
|
methods,
|
|
592
|
+
// The OpenAPI document is synthesised from nothing on disk.
|
|
593
|
+
source: generated
|
|
594
|
+
? generated.file
|
|
595
|
+
? relative(sourceDir, generated.file).replace(/\\/g, "/")
|
|
596
|
+
: undefined
|
|
597
|
+
: relative(sourceDir, absPath).replace(/\\/g, "/"),
|
|
557
598
|
// A synthesised robots.txt or sitemap.xml runs no guard: it exists to be
|
|
558
599
|
// read by anyone, and a guard on the root would 401 the crawler.
|
|
559
600
|
middleware: generated ? [] : ancestors(name, "middleware"),
|
|
@@ -765,7 +806,32 @@ self.addEventListener('install', (event) => {
|
|
|
765
806
|
// it is served by the host out of the prerendered directory, which this
|
|
766
807
|
// worker cannot see. Added here so it is in the cache before it is
|
|
767
808
|
// needed, which is the only moment it cannot be fetched.
|
|
768
|
-
.then((cache) => cache.addAll(OFFLINE_URL ? [...PRECACHE, OFFLINE_URL] : PRECACHE))
|
|
809
|
+
.then((cache) => cache.addAll(OFFLINE_URL ? [...PRECACHE, OFFLINE_URL] : PRECACHE).then(() => cache))
|
|
810
|
+
// And the payload each precached page boots from. A document alone is
|
|
811
|
+
// markup that never hydrates: the client fetches its payload on boot,
|
|
812
|
+
// and a precached / rendered offline and stayed inert because that
|
|
813
|
+
// request had nothing cached to answer it.
|
|
814
|
+
.then((cache) =>
|
|
815
|
+
Promise.all(
|
|
816
|
+
PRECACHE.filter((url) => !/\\.[a-z0-9]+$/i.test(url))
|
|
817
|
+
.concat(OFFLINE_URL ? [OFFLINE_URL] : [])
|
|
818
|
+
.map((url) => {
|
|
819
|
+
const warm = new Request(url, { headers: { 'X-RSC': '1' } })
|
|
820
|
+
|
|
821
|
+
return fetch(warm)
|
|
822
|
+
.then((payload) => {
|
|
823
|
+
if (mayStore(payload)) return cache.put(keyFor(warm), payload)
|
|
824
|
+
|
|
825
|
+
// Said, not swallowed: a page whose payload cannot be kept
|
|
826
|
+
// is one that will render offline and never hydrate, and
|
|
827
|
+
// the reason - a no-store from a guard above it, usually -
|
|
828
|
+
// is only visible here.
|
|
829
|
+
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')
|
|
830
|
+
})
|
|
831
|
+
.catch(() => {})
|
|
832
|
+
}),
|
|
833
|
+
),
|
|
834
|
+
)
|
|
769
835
|
.then(() => self.skipWaiting()),
|
|
770
836
|
)
|
|
771
837
|
})
|
|
@@ -774,9 +840,16 @@ self.addEventListener('activate', (event) => {
|
|
|
774
840
|
event.waitUntil(
|
|
775
841
|
caches
|
|
776
842
|
.keys()
|
|
777
|
-
.then((keys) =>
|
|
778
|
-
|
|
779
|
-
|
|
843
|
+
.then((keys) => {
|
|
844
|
+
const older = keys.filter((k) => k.startsWith('rsc-kit-') && k !== CACHE)
|
|
845
|
+
|
|
846
|
+
return Promise.all(older.map((k) => caches.delete(k))).then(() => older.length > 0)
|
|
847
|
+
})
|
|
848
|
+
.then((swept) => self.clients.claim().then(() => swept))
|
|
849
|
+
// Only when something older was swept. The first worker a visitor ever
|
|
850
|
+
// gets activates too, and told them "a new version is ready" on their
|
|
851
|
+
// second page - there was no previous version to be new against.
|
|
852
|
+
.then((swept) => (swept ? tellTheOpenPages() : undefined)),
|
|
780
853
|
)
|
|
781
854
|
})
|
|
782
855
|
|
|
@@ -821,6 +894,37 @@ const keyFor = (request) => {
|
|
|
821
894
|
return new Request(url, { headers: request.headers })
|
|
822
895
|
}
|
|
823
896
|
|
|
897
|
+
// Nothing cached for a request and no network: the offline page for a
|
|
898
|
+
// navigation, and for the boot of a page the offline page stood in for -
|
|
899
|
+
// the runtime asks for the payload of the url in the address bar, which is
|
|
900
|
+
// the one thing nothing has - the offline page's own payload, which is what
|
|
901
|
+
// the document on screen is. A boot only, no segments header: a navigation
|
|
902
|
+
// while offline still fails as itself, and the page already open keeps its
|
|
903
|
+
// banner. Null when there is no offline page, or nothing of it cached.
|
|
904
|
+
const standIn = async (request) => {
|
|
905
|
+
if (!OFFLINE_URL) return null
|
|
906
|
+
|
|
907
|
+
if (request.mode === 'navigate') return (await caches.match(OFFLINE_URL)) ?? null
|
|
908
|
+
|
|
909
|
+
if (request.headers.get('X-RSC') && !request.headers.get('X-RSC-Segments')) {
|
|
910
|
+
return (
|
|
911
|
+
(await caches.match(
|
|
912
|
+
keyFor(new Request(new URL(OFFLINE_URL, self.location.origin), { headers: { 'X-RSC': '1' } })),
|
|
913
|
+
MATCH,
|
|
914
|
+
)) ?? null
|
|
915
|
+
)
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
return null
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// The Cache API honours Vary, and a stored payload varies on X-RSC - so a
|
|
922
|
+
// payload warmed with one spelling of that header was a miss for a boot
|
|
923
|
+
// that sent another, and a precached page rendered offline with its payload
|
|
924
|
+
// sitting in the cache, unmatched. What Vary is for is already in the key:
|
|
925
|
+
// the ?__rsc=<segments> above is the one header the answer differs on.
|
|
926
|
+
const MATCH = { ignoreVary: true }
|
|
927
|
+
|
|
824
928
|
self.addEventListener('fetch', (event) => {
|
|
825
929
|
const request = event.request
|
|
826
930
|
const url = new URL(request.url)
|
|
@@ -855,7 +959,7 @@ self.addEventListener('fetch', (event) => {
|
|
|
855
959
|
// differently for every value of it.
|
|
856
960
|
if (FROZEN.has(url.pathname.replace(/\\/+$/, '') || '/') && !url.search) {
|
|
857
961
|
event.respondWith(
|
|
858
|
-
caches.match(keyFor(request)).then((hit) => {
|
|
962
|
+
caches.match(keyFor(request), MATCH).then((hit) => {
|
|
859
963
|
const fresh = fetch(request)
|
|
860
964
|
.then((response) => {
|
|
861
965
|
if (mayStore(response)) {
|
|
@@ -865,10 +969,17 @@ self.addEventListener('fetch', (event) => {
|
|
|
865
969
|
|
|
866
970
|
return response
|
|
867
971
|
})
|
|
868
|
-
.catch((error) => {
|
|
972
|
+
.catch(async (error) => {
|
|
869
973
|
if (hit) return hit
|
|
870
974
|
|
|
871
|
-
|
|
975
|
+
// Stored by the build but never visited from this browser, so
|
|
976
|
+
// nothing is cached for it, and no network: the offline page is
|
|
977
|
+
// the honest answer, the same as for any other navigation - and
|
|
978
|
+
// its payload for the boot that follows, so the page it stood
|
|
979
|
+
// in for hydrates. Without this a frozen page failed with
|
|
980
|
+
// ERR_FAILED where an unstored one showed /offline, and then
|
|
981
|
+
// stood inert where an unstored one hydrated.
|
|
982
|
+
return (await standIn(request)) ?? Promise.reject(error)
|
|
872
983
|
})
|
|
873
984
|
|
|
874
985
|
// Not awaited when there is a hit: the point is that the visitor does
|
|
@@ -893,11 +1004,11 @@ self.addEventListener('fetch', (event) => {
|
|
|
893
1004
|
// cached, and a reload with no network has the markup and nothing to
|
|
894
1005
|
// hydrate it with.
|
|
895
1006
|
//
|
|
896
|
-
// \`X-RSC:
|
|
1007
|
+
// \`X-RSC: 1\` and no segments header is exactly what a fresh boot
|
|
897
1008
|
// sends, which is what makes this entry the one it finds: the server
|
|
898
1009
|
// varies on those, and the Cache API matches on the same.
|
|
899
1010
|
if (request.mode === 'navigate') {
|
|
900
|
-
const warm = new Request(request.url, { headers: { 'X-RSC': '
|
|
1011
|
+
const warm = new Request(request.url, { headers: { 'X-RSC': '1' } })
|
|
901
1012
|
|
|
902
1013
|
fetch(warm)
|
|
903
1014
|
.then((payload) => {
|
|
@@ -932,7 +1043,7 @@ self.addEventListener('fetch', (event) => {
|
|
|
932
1043
|
return response
|
|
933
1044
|
})
|
|
934
1045
|
.catch(async () => {
|
|
935
|
-
const hit = await caches.match(keyFor(request))
|
|
1046
|
+
const hit = await caches.match(keyFor(request), MATCH)
|
|
936
1047
|
|
|
937
1048
|
if (hit) return hit
|
|
938
1049
|
|
|
@@ -947,11 +1058,9 @@ self.addEventListener('fetch', (event) => {
|
|
|
947
1058
|
// stand in for another: it is about being offline, not about the url it
|
|
948
1059
|
// appears under. Navigations only — a payload request answered with a
|
|
949
1060
|
// document would be decoded as one and throw.
|
|
950
|
-
|
|
951
|
-
const page = await caches.match(OFFLINE_URL)
|
|
1061
|
+
const stood = await standIn(request)
|
|
952
1062
|
|
|
953
|
-
|
|
954
|
-
}
|
|
1063
|
+
if (stood) return stood
|
|
955
1064
|
|
|
956
1065
|
// Letting it fail says what is true, and a page already open is
|
|
957
1066
|
// unaffected.
|
|
@@ -1066,17 +1175,24 @@ function writeWebManifest(clientDir, options) {
|
|
|
1066
1175
|
* precached and found wanting at the one moment it matters, and the build says
|
|
1067
1176
|
* which read did it.
|
|
1068
1177
|
*/
|
|
1069
|
-
function offlineFallback(frozen, results) {
|
|
1178
|
+
export function offlineFallback(frozen, results) {
|
|
1070
1179
|
const found = results.find((r) => r.url === "/offline");
|
|
1071
1180
|
if (!found)
|
|
1072
1181
|
return null;
|
|
1073
1182
|
if (!frozen.includes("/offline")) {
|
|
1074
1183
|
// The reason already reads "dynamic — called cookies()", and this sentence
|
|
1075
1184
|
// 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
|
-
|
|
1185
|
+
// it twice with a dash in the middle of both. A shell's reason is a
|
|
1186
|
+
// sentence of its own - "connection() awaited by AuthLinks streams per
|
|
1187
|
+
// request; the rest is stored" - and takes no "it" in front, and the
|
|
1188
|
+
// part about the rest being stored is not the point here.
|
|
1189
|
+
const why = found.type === "shell"
|
|
1190
|
+
? (found.reason?.replace(/; the rest is stored$/, "") ?? null)
|
|
1191
|
+
: found.reason
|
|
1192
|
+
? "it " + found.reason.replace(/^dynamic — /, "")
|
|
1193
|
+
: null;
|
|
1078
1194
|
log("offline: /offline cannot be the fallback" +
|
|
1079
|
-
(why ? `, because
|
|
1195
|
+
(why ? `, because ${why}` : "") +
|
|
1080
1196
|
". A fallback has to be servable with no network at all.");
|
|
1081
1197
|
return null;
|
|
1082
1198
|
}
|
|
@@ -1102,6 +1218,24 @@ function copyServiceWorkerExtra(clientDir) {
|
|
|
1102
1218
|
copyFileSync(source, join(clientDir, "sw-app.js"));
|
|
1103
1219
|
return "/sw-app.js";
|
|
1104
1220
|
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Whether a public file is worth fetching before it is asked for.
|
|
1223
|
+
*
|
|
1224
|
+
* The precache is what lets the app boot with no network: the scripts, the
|
|
1225
|
+
* stylesheets, the fonts, the manifest and its icons. Everything else in
|
|
1226
|
+
* public/ - an image, a wasm module, the share card - is cached the first
|
|
1227
|
+
* time it is used, which is what the fetch handler does for any hashed
|
|
1228
|
+
* asset. Precaching all of it made an install cost a megabyte before the
|
|
1229
|
+
* visitor had seen a page: 765 kB of it a webp encoder, the rest a png no
|
|
1230
|
+
* browser ever renders.
|
|
1231
|
+
*/
|
|
1232
|
+
export function bootsTheApp(file) {
|
|
1233
|
+
if (/\.(?:m?js|css|woff2?|webmanifest|json)$/i.test(file))
|
|
1234
|
+
return !/\.wasm\.js$/i.test(file);
|
|
1235
|
+
if (/(?:^|\/)(?:icon|apple-icon|favicon)[^/]*\.(?:png|ico|svg)$/i.test(file))
|
|
1236
|
+
return true;
|
|
1237
|
+
return false;
|
|
1238
|
+
}
|
|
1105
1239
|
function writeServiceWorker(clientDir, frozen = [], offlineUrl = null) {
|
|
1106
1240
|
if (!existsSync(clientDir))
|
|
1107
1241
|
return;
|
|
@@ -1120,7 +1254,7 @@ function writeServiceWorker(clientDir, frozen = [], offlineUrl = null) {
|
|
|
1120
1254
|
const path = join(dir, entry.name);
|
|
1121
1255
|
if (entry.isDirectory())
|
|
1122
1256
|
walk(path, `${prefix}${entry.name}/`);
|
|
1123
|
-
else
|
|
1257
|
+
else if (bootsTheApp(`${prefix}${entry.name}`))
|
|
1124
1258
|
files.push(`${prefix}${entry.name}`);
|
|
1125
1259
|
}
|
|
1126
1260
|
};
|
|
@@ -1607,7 +1741,7 @@ function renderRouteTypes(manifest) {
|
|
|
1607
1741
|
"",
|
|
1608
1742
|
"// `export {}` is load-bearing: in a file with no import or export,",
|
|
1609
1743
|
"// `declare module` *replaces* the real module rather than augmenting it,",
|
|
1610
|
-
"// and
|
|
1744
|
+
"// and Route and route() vanish from it with no error to explain why.",
|
|
1611
1745
|
"export {}",
|
|
1612
1746
|
"",
|
|
1613
1747
|
"declare module '@rsc-kit/core/routes' {",
|
|
@@ -1723,8 +1857,20 @@ function componentName(absPath) {
|
|
|
1723
1857
|
const rel = relative(sourceDir, absPath).replace(/\\/g, "/");
|
|
1724
1858
|
return rel.replace(/\.(tsx|jsx|ts|js)$/, "");
|
|
1725
1859
|
}
|
|
1726
|
-
|
|
1727
|
-
|
|
1860
|
+
/**
|
|
1861
|
+
* The identifier a route file is imported as in the generated entries.
|
|
1862
|
+
*
|
|
1863
|
+
* One-to-one with the path, which the obvious "replace everything odd with an
|
|
1864
|
+
* underscore" is not: app/agent-account/page and app/agent/account/page
|
|
1865
|
+
* both came out as _c_app_agent_account_page, and the entry declared the
|
|
1866
|
+
* same import twice. A slash is an underscore, because that is what every
|
|
1867
|
+
* alias reads like; anything else that is not a letter or digit - a hyphen,
|
|
1868
|
+
* a dot, the brackets of a param, the parens of a group, an underscore of
|
|
1869
|
+
* its own - is its character code, so no two paths share an alias.
|
|
1870
|
+
*/
|
|
1871
|
+
export function toAlias(name) {
|
|
1872
|
+
return ("_c_" +
|
|
1873
|
+
name.replace(/[^a-zA-Z0-9]/g, (char) => (char === "/" ? "_" : "$" + char.charCodeAt(0).toString(16))));
|
|
1728
1874
|
}
|
|
1729
1875
|
const components = new Map();
|
|
1730
1876
|
/**
|
|
@@ -1736,6 +1882,48 @@ const components = new Map();
|
|
|
1736
1882
|
* a default component.
|
|
1737
1883
|
*/
|
|
1738
1884
|
const apiRoutes = new Map();
|
|
1885
|
+
const OPENAPI_ROUTE_ID = "virtual:rsc-kit/openapi";
|
|
1886
|
+
/**
|
|
1887
|
+
* /openapi.json, when asked for, registered as an api route at the path the
|
|
1888
|
+
* option names. Synthesised like a sitemap: stored by the build, without
|
|
1889
|
+
* middleware - a document exists to be read.
|
|
1890
|
+
*/
|
|
1891
|
+
function registerOpenApiRoutes() {
|
|
1892
|
+
if (!openapi)
|
|
1893
|
+
return;
|
|
1894
|
+
const name = `app${openapi.path.replace(/\/$/, "")}/route`;
|
|
1895
|
+
if (apiRoutes.has(name)) {
|
|
1896
|
+
throw new Error(`[rsc-kit] ${openapi.path} is answered by ${apiRoutes.get(name).absPath} and by rscKit({ openapi }). Move one.`);
|
|
1897
|
+
}
|
|
1898
|
+
apiRoutes.set(name, { name, absPath: OPENAPI_ROUTE_ID, methods: ["GET"], generated: { kind: "openapi", file: "" } });
|
|
1899
|
+
}
|
|
1900
|
+
/** The module behind the synthesised /openapi.json route. */
|
|
1901
|
+
function openApiPlugin() {
|
|
1902
|
+
return {
|
|
1903
|
+
name: "rsc-kit:openapi",
|
|
1904
|
+
resolveId(id) {
|
|
1905
|
+
if (id === OPENAPI_ROUTE_ID)
|
|
1906
|
+
return "\0" + id;
|
|
1907
|
+
},
|
|
1908
|
+
load(id) {
|
|
1909
|
+
if (id !== "\0" + OPENAPI_ROUTE_ID || !openapi)
|
|
1910
|
+
return;
|
|
1911
|
+
// Every route.ts the app wrote, imported so its schemas can describe
|
|
1912
|
+
// themselves at request time; the synthesised ones are not an API.
|
|
1913
|
+
const routes = [...apiRoutes.values()].filter((r) => !r.generated);
|
|
1914
|
+
const guarded = new Set((routeManifest().apis ?? []).filter((api) => api.middleware.length > 0).map((api) => api.name));
|
|
1915
|
+
const imports = routes.map((r, i) => `import * as __r${i} from ${JSON.stringify(r.absPath)}`);
|
|
1916
|
+
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} }`);
|
|
1917
|
+
return [
|
|
1918
|
+
...imports,
|
|
1919
|
+
`import { openApiResponse } from ${JSON.stringify(join(packageDir, "openapi"))}`,
|
|
1920
|
+
`const routes = [\n${entries.join(",\n")}\n]`,
|
|
1921
|
+
`export const GET = () => openApiResponse(routes, ${JSON.stringify(openapi.document)})`,
|
|
1922
|
+
"",
|
|
1923
|
+
].join("\n");
|
|
1924
|
+
},
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1739
1927
|
/** Files beside the root layout served at the root as they are: robots.txt, a hand-written sitemap.xml, humans.txt. */
|
|
1740
1928
|
let rootFiles = [];
|
|
1741
1929
|
const METADATA_ROUTE_ID = "virtual:rsc-kit/metadata-route/";
|
|
@@ -2071,6 +2259,9 @@ const NITRO_HANDLER_OPTIONS = ` props: (match, request) => ({
|
|
|
2071
2259
|
...Object.fromEntries(new URL(request.url).searchParams),
|
|
2072
2260
|
}),
|
|
2073
2261
|
version: process.env.RSC_BUILD_VERSION,
|
|
2262
|
+
// A built server gzips what it answers, where the runtime can; the dev
|
|
2263
|
+
// server answers raw, which is what a person reading a response wants.
|
|
2264
|
+
compress: import.meta.env.PROD,
|
|
2074
2265
|
`;
|
|
2075
2266
|
const NITRO_HOST_CALLS = `
|
|
2076
2267
|
let hostInstalled = false
|
|
@@ -2199,12 +2390,12 @@ import { DefaultRouteError } from ${JSON.stringify(join(packageDir, "js/DefaultR
|
|
|
2199
2390
|
import { searchParams as requestSearchParams } from ${JSON.stringify(join(packageDir, "request"))}
|
|
2200
2391
|
import { parseParams, parseSearchParams, parseBody, isSearchParamsError, isBodyError } from ${JSON.stringify(join(packageDir, "routeSchema"))}
|
|
2201
2392
|
import { notFoundDigest, isNotFoundSignal } from ${JSON.stringify(join(packageDir, "notFound"))}
|
|
2202
|
-
import { noteRequestRead } from ${JSON.stringify(join(packageDir, "request"))}
|
|
2393
|
+
import { noteRequestRead, urlOf } from ${JSON.stringify(join(packageDir, "request"))}
|
|
2203
2394
|
import { redirectDigest } from ${JSON.stringify(join(packageDir, "redirectDigest"))}
|
|
2204
2395
|
import { createRscHandler } from ${JSON.stringify(join(packageDir, "host"))}
|
|
2205
2396
|
import { httpHostCalls } from ${JSON.stringify(join(packageDir, "hostCalls"))}
|
|
2206
2397
|
import { prerenderedBeside } from ${JSON.stringify(join(packageDir, "files"))}
|
|
2207
|
-
import { renderToReadableStream, decodeReply, loadServerAction } from '@vitejs/plugin-rsc/rsc'
|
|
2398
|
+
import { renderToReadableStream, decodeReply, decodeAction, decodeFormState, loadServerAction } from '@vitejs/plugin-rsc/rsc'
|
|
2208
2399
|
import { isQuery, queryCacheControl, isQueryValidationError } from ${JSON.stringify(join(packageDir, "query"))}
|
|
2209
2400
|
import { isActionValidationError, isClientBuilt } from ${JSON.stringify(join(packageDir, "action"))}
|
|
2210
2401
|
import { noteFallback as noteCaughtRead } from ${JSON.stringify(join(packageDir, "request"))}
|
|
@@ -2364,7 +2555,10 @@ export async function handleApiRoute(
|
|
|
2364
2555
|
// reaches for the value.
|
|
2365
2556
|
noteRequestRead('searchParams')
|
|
2366
2557
|
|
|
2367
|
-
|
|
2558
|
+
// Through urlOf, so the build's probe books this read to searchParams
|
|
2559
|
+
// rather than to the url - a route reads request.url itself when it
|
|
2560
|
+
// wants the query the Next way, and that read is the one that matters.
|
|
2561
|
+
return parseSearchParams(mod.searchParams, new URL(urlOf(request)).searchParams)
|
|
2368
2562
|
}),
|
|
2369
2563
|
body: hasBody(method) ? lazily(() => parseBody(mod.body, request)) : undefined,
|
|
2370
2564
|
}
|
|
@@ -3215,8 +3409,18 @@ async function runMiddleware(component: string, props: Record<string, unknown> =
|
|
|
3215
3409
|
}
|
|
3216
3410
|
|
|
3217
3411
|
// Sequential and awaited, outermost first: an outer guard refusing means
|
|
3218
|
-
// the inner one should never have been asked.
|
|
3219
|
-
|
|
3412
|
+
// the inner one should never have been asked. A directory may declare
|
|
3413
|
+
// several as a list - reused checks imported from one place - and they
|
|
3414
|
+
// run in the order written, stopping at the first refusal.
|
|
3415
|
+
for (const check of Array.isArray(guard) ? guard : [guard]) {
|
|
3416
|
+
if (typeof check !== 'function') {
|
|
3417
|
+
throw new Error(
|
|
3418
|
+
'Route middleware ' + name + ' exports something that is not a function or a list of them.',
|
|
3419
|
+
)
|
|
3420
|
+
}
|
|
3421
|
+
|
|
3422
|
+
await check(props)
|
|
3423
|
+
}
|
|
3220
3424
|
}
|
|
3221
3425
|
}
|
|
3222
3426
|
|
|
@@ -3315,6 +3519,54 @@ export async function handleRscHtmlStream(
|
|
|
3315
3519
|
return { htmlStream, rscPayloadPromise, clientChunks: {} }
|
|
3316
3520
|
}
|
|
3317
3521
|
|
|
3522
|
+
/**
|
|
3523
|
+
* A form posted to the page before the page had a runtime.
|
|
3524
|
+
*
|
|
3525
|
+
* React writes the action's id into the form it emits for a server action,
|
|
3526
|
+
* so a browser with no javascript yet - or none at all - posts the fields
|
|
3527
|
+
* to the page's own url. The action runs from those fields, exactly as it
|
|
3528
|
+
* would have been called, and the page renders afterwards with what it
|
|
3529
|
+
* returned as React's form state: a useActionState form shows its result,
|
|
3530
|
+
* a redirect() thrown by the action leaves through the scope the way a
|
|
3531
|
+
* page's would, and a cookie it set is on the answer.
|
|
3532
|
+
*/
|
|
3533
|
+
export async function handleRscFormPost(
|
|
3534
|
+
component: string,
|
|
3535
|
+
props: Record<string, unknown> = {},
|
|
3536
|
+
layouts: LayoutEntry[] = [],
|
|
3537
|
+
loadings: string[] = [],
|
|
3538
|
+
parallelSlots: Record<string, string> = {},
|
|
3539
|
+
slotOverrides: Record<string, SlotOverride> = {},
|
|
3540
|
+
nonce?: string,
|
|
3541
|
+
pageKey = '',
|
|
3542
|
+
bootstrap = true,
|
|
3543
|
+
formData: FormData = new FormData(),
|
|
3544
|
+
): Promise<{ htmlStream: ReadableStream }> {
|
|
3545
|
+
await instrumented()
|
|
3546
|
+
applyHost()
|
|
3547
|
+
await runMiddleware(component, props)
|
|
3548
|
+
|
|
3549
|
+
// The function the form named, bound to what it posted. Nothing named -
|
|
3550
|
+
// a plain form posted here by mistake - and the page simply renders.
|
|
3551
|
+
const action = await decodeAction(formData)
|
|
3552
|
+
let formState: unknown
|
|
3553
|
+
|
|
3554
|
+
if (action) {
|
|
3555
|
+
const result = await action()
|
|
3556
|
+
|
|
3557
|
+
formState = await decodeFormState(result, formData)
|
|
3558
|
+
}
|
|
3559
|
+
|
|
3560
|
+
const flight = renderToReadableStream(
|
|
3561
|
+
await renderTree(component, props, layouts, loadings, parallelSlots, slotOverrides, 0, pageKey, bootstrap),
|
|
3562
|
+
{ onError: flightOnError },
|
|
3563
|
+
)
|
|
3564
|
+
const ssr = await (import.meta as any).viteRsc.loadModule('ssr', 'index')
|
|
3565
|
+
const htmlStream = await ssr.handleSsr(flight, nonce, undefined, bootstrap, formState)
|
|
3566
|
+
|
|
3567
|
+
return { htmlStream }
|
|
3568
|
+
}
|
|
3569
|
+
|
|
3318
3570
|
/**
|
|
3319
3571
|
* Finish a shell that was frozen at build time.
|
|
3320
3572
|
*
|
|
@@ -3941,7 +4193,7 @@ export async function handleRscPprShell(
|
|
|
3941
4193
|
// say so on the route's line; the digest goes into the document for the
|
|
3942
4194
|
// browser to recognise.
|
|
3943
4195
|
if ((e as { digest?: string } | null)?.digest === 'rsc-kit:search-params-fallback') {
|
|
3944
|
-
const where = /at ([A-
|
|
4196
|
+
const where = /at ([A-Z_$][\\w$]*)/.exec(info?.componentStack ?? '')?.[1]
|
|
3945
4197
|
noteCaughtRead('useSearchParams()' + (where ? ' in ' + where : ''))
|
|
3946
4198
|
return 'rsc-kit:search-params-fallback'
|
|
3947
4199
|
}
|
|
@@ -4091,6 +4343,7 @@ async function serve(request: Request): Promise<Response> {
|
|
|
4091
4343
|
handleRsc,
|
|
4092
4344
|
handleRscStream,
|
|
4093
4345
|
handleRscHtmlStream,
|
|
4346
|
+
handleRscFormPost,
|
|
4094
4347
|
handleRscRevalidate,
|
|
4095
4348
|
handleRscPayload,
|
|
4096
4349
|
handleRscPprShell,
|
|
@@ -4181,6 +4434,8 @@ function generateEntrySsr() {
|
|
|
4181
4434
|
const devUrls = join(packageDir, "devUrls");
|
|
4182
4435
|
const request = join(packageDir, "request");
|
|
4183
4436
|
const fallbackReport = join(packageDir, "js/fallbackReport");
|
|
4437
|
+
const redirectDigestModule = join(packageDir, "redirectDigest");
|
|
4438
|
+
const notFoundModule = join(packageDir, "notFound");
|
|
4184
4439
|
return `// GENERATED by rscKit() — do not edit.
|
|
4185
4440
|
import { createFromReadableStream } from '@vitejs/plugin-rsc/ssr'
|
|
4186
4441
|
import { renderToReadableStream, resume } from 'react-dom/server.edge'
|
|
@@ -4188,6 +4443,8 @@ import { prerender } from 'react-dom/static.edge'
|
|
|
4188
4443
|
import { rewriteViteDevUrlStream } from ${JSON.stringify(devUrls)}
|
|
4189
4444
|
import { noteFallback } from ${JSON.stringify(request)}
|
|
4190
4445
|
import { cancelledByConsumer, caughtByLoading } from ${JSON.stringify(fallbackReport)}
|
|
4446
|
+
import { parseRedirectDigest } from ${JSON.stringify(redirectDigestModule)}
|
|
4447
|
+
import { isNotFoundDigest } from ${JSON.stringify(notFoundModule)}
|
|
4191
4448
|
|
|
4192
4449
|
// Set only by the dev server. @vitejs/plugin-rsc emits its bootstrap and CSS
|
|
4193
4450
|
// links root-relative in dev, which would send the browser to the host for
|
|
@@ -4199,6 +4456,7 @@ export async function handleSsr(
|
|
|
4199
4456
|
nonce?: string,
|
|
4200
4457
|
onError?: (error: unknown) => void,
|
|
4201
4458
|
bootstrap = true,
|
|
4459
|
+
formState?: unknown,
|
|
4202
4460
|
): Promise<ReadableStream> {
|
|
4203
4461
|
const root = await createFromReadableStream(rscStream)
|
|
4204
4462
|
|
|
@@ -4216,6 +4474,9 @@ export async function handleSsr(
|
|
|
4216
4474
|
bootstrapScriptContent,
|
|
4217
4475
|
nonce,
|
|
4218
4476
|
onError: onError ?? reportRenderError('ssr'),
|
|
4477
|
+
// What a posted form's action returned, for the useActionState that
|
|
4478
|
+
// asked: React seats it in the form it belongs to.
|
|
4479
|
+
...(formState !== undefined ? { formState: formState as any } : {}),
|
|
4219
4480
|
})
|
|
4220
4481
|
|
|
4221
4482
|
return DEV_ORIGIN ? rewriteViteDevUrlStream(html, DEV_ORIGIN) : html
|
|
@@ -4255,7 +4516,7 @@ export async function handleSsrPrerender(
|
|
|
4255
4516
|
// caught at a boundary is: the build attaches it to the route.
|
|
4256
4517
|
onError: (error: unknown, info?: { componentStack?: string }) => {
|
|
4257
4518
|
if ((error as { digest?: string } | null)?.digest !== 'rsc-kit:search-params-fallback') return
|
|
4258
|
-
const where = /at ([A-
|
|
4519
|
+
const where = /at ([A-Z_$][\\w$]*)/.exec(info?.componentStack ?? '')?.[1]
|
|
4259
4520
|
noteFallback('useSearchParams()' + (where ? ' in ' + where : ''))
|
|
4260
4521
|
return 'rsc-kit:search-params-fallback'
|
|
4261
4522
|
},
|
|
@@ -4283,11 +4544,18 @@ function reportRenderError(phase: 'ssr' | 'resume') {
|
|
|
4283
4544
|
|
|
4284
4545
|
const digest = (error as { digest?: string } | null)?.digest
|
|
4285
4546
|
|
|
4547
|
+
// A redirect or a notFound() the render asked for arrives here as the
|
|
4548
|
+
// Flight row's error, once per boundary it was thrown in. The rsc side
|
|
4549
|
+
// already turned it into a digest on purpose; it is the page's answer,
|
|
4550
|
+
// and the host reads it off the digest. Not an error to print, four
|
|
4551
|
+
// times or once.
|
|
4552
|
+
if (parseRedirectDigest(digest) || isNotFoundDigest(digest)) return digest
|
|
4553
|
+
|
|
4286
4554
|
if (digest === 'rsc-kit:search-params-fallback') {
|
|
4287
4555
|
// The component is the first frame of React's stack. Noted on the
|
|
4288
4556
|
// request so the build attaches it to the route; printed as one line,
|
|
4289
4557
|
// not a stack, so the server says which boundary it wants.
|
|
4290
|
-
const where = /at ([A-
|
|
4558
|
+
const where = /at ([A-Z_$][\\w$]*)/.exec(info?.componentStack ?? '')?.[1]
|
|
4291
4559
|
|
|
4292
4560
|
noteFallback('useSearchParams()' + (where ? ' in ' + where : ''))
|
|
4293
4561
|
|
|
@@ -4381,6 +4649,9 @@ import { refresh } from ${JSON.stringify(refreshModule)}
|
|
|
4381
4649
|
createViteRscApp(document, ${JSON.stringify(interceptManifest())}, ${JSON.stringify({
|
|
4382
4650
|
staticPayloads: staticPayloads || null,
|
|
4383
4651
|
routes: routesForClient,
|
|
4652
|
+
// Every route.ts, as a url pattern, so a link to one is treated as an
|
|
4653
|
+
// anchor rather than prefetched and fetched as a page.
|
|
4654
|
+
apiRoutes: (routeManifest().apis ?? []).map((api) => patternOf(api.segments)),
|
|
4384
4655
|
})})
|
|
4385
4656
|
|
|
4386
4657
|
// A server component is not a module the browser has, so Vite cannot replace
|
|
@@ -4411,12 +4682,19 @@ ${offline
|
|
|
4411
4682
|
// worker answering from a cache in front of it turns every edit into a
|
|
4412
4683
|
// question about which copy you are looking at.
|
|
4413
4684
|
if ('serviceWorker' in navigator && import.meta.env.PROD) {
|
|
4414
|
-
|
|
4685
|
+
const register = () => {
|
|
4415
4686
|
void navigator.serviceWorker.register('/sw.js').catch(() => {
|
|
4416
4687
|
// A worker that will not register is not a reason for the page to fail.
|
|
4417
4688
|
// The app works; it just will not survive being reloaded offline.
|
|
4418
4689
|
})
|
|
4419
|
-
}
|
|
4690
|
+
}
|
|
4691
|
+
|
|
4692
|
+
// This runs after the boot payload has been fetched and decoded, and on a
|
|
4693
|
+
// fast load the page has finished loading by then - a listener added now
|
|
4694
|
+
// waits for an event that has already fired, and nothing registered until
|
|
4695
|
+
// the next navigation.
|
|
4696
|
+
if (document.readyState === 'complete') register()
|
|
4697
|
+
else window.addEventListener('load', register)
|
|
4420
4698
|
}
|
|
4421
4699
|
`
|
|
4422
4700
|
: ""}`;
|
|
@@ -4664,10 +4942,16 @@ function useSsrModules() {
|
|
|
4664
4942
|
return {
|
|
4665
4943
|
name: "rsc-kit:use-ssr",
|
|
4666
4944
|
enforce: "pre",
|
|
4667
|
-
applyToEnvironment: (environment) => environment.name === "rsc",
|
|
4945
|
+
applyToEnvironment: (environment) => environment.name === "rsc" || environment.name === "ssr",
|
|
4668
4946
|
transform(code, id) {
|
|
4669
4947
|
if (!code.includes("use ssr"))
|
|
4670
4948
|
return;
|
|
4949
|
+
// In the environment the module runs in, the directive has done its
|
|
4950
|
+
// work and is a string the bundler warns about. Out it goes.
|
|
4951
|
+
if (this.environment.name === "ssr") {
|
|
4952
|
+
const stripped = withoutSsrDirective(code);
|
|
4953
|
+
return stripped === null ? undefined : { code: stripped, map: null };
|
|
4954
|
+
}
|
|
4671
4955
|
try {
|
|
4672
4956
|
const proxy = ssrProxyModule(code, id);
|
|
4673
4957
|
return proxy === null ? undefined : { code: proxy, map: null };
|
|
@@ -4687,7 +4971,21 @@ function useSsrModules() {
|
|
|
4687
4971
|
* directive that moves it. The build says the same once, as a warning.
|
|
4688
4972
|
*/
|
|
4689
4973
|
const RENDERER_STUB = "\0rsc-kit:react-dom-server?from=";
|
|
4690
|
-
|
|
4974
|
+
/** What react-dom/server exports, across its node, edge and browser entries. */
|
|
4975
|
+
const SERVER_RENDERER_EXPORTS = [
|
|
4976
|
+
"renderToString",
|
|
4977
|
+
"renderToStaticMarkup",
|
|
4978
|
+
"renderToReadableStream",
|
|
4979
|
+
"renderToPipeableStream",
|
|
4980
|
+
"renderToStaticNodeStream",
|
|
4981
|
+
"resume",
|
|
4982
|
+
"resumeToPipeableStream",
|
|
4983
|
+
"resumeAndPrerender",
|
|
4984
|
+
"resumeAndPrerenderToNodeStream",
|
|
4985
|
+
"prerender",
|
|
4986
|
+
"prerenderToNodeStream",
|
|
4987
|
+
];
|
|
4988
|
+
export function serverRendererInRsc() {
|
|
4691
4989
|
const warned = new Set();
|
|
4692
4990
|
const appImporter = (ctx, importer) => {
|
|
4693
4991
|
let current = importer;
|
|
@@ -4706,24 +5004,93 @@ function serverRendererInRsc() {
|
|
|
4706
5004
|
}
|
|
4707
5005
|
return importer ? relative(process.cwd(), importer.split("?")[0]) : null;
|
|
4708
5006
|
};
|
|
5007
|
+
// Per package, so a dependency imported from twenty files is read once.
|
|
5008
|
+
const externalRenderers = new Map();
|
|
5009
|
+
// Packages already named by the warning above, so the stub's own warning
|
|
5010
|
+
// - "imported by node_modules/<package>/dist/index.mjs" - stays quiet for
|
|
5011
|
+
// them: one problem, one line.
|
|
5012
|
+
const warnedPackages = new Set();
|
|
4709
5013
|
return {
|
|
4710
5014
|
name: "rsc-kit:server-renderer",
|
|
5015
|
+
// Before Vite's own resolver, which otherwise answers react-dom/server
|
|
5016
|
+
// with React's react-server entry - a real module missing the export,
|
|
5017
|
+
// and a build that fails on MISSING_EXPORT with no mention of the fix.
|
|
5018
|
+
enforce: "pre",
|
|
4711
5019
|
applyToEnvironment: (environment) => environment.name === "rsc",
|
|
4712
|
-
resolveId(source, importer) {
|
|
4713
|
-
|
|
5020
|
+
async resolveId(source, importer) {
|
|
5021
|
+
// Only an import from a module: a resolve with no importer is Vite
|
|
5022
|
+
// asking whether the specifier exists - deciding what to externalise -
|
|
5023
|
+
// and answering that with the stub would answer the wrong question.
|
|
5024
|
+
if (SERVER_RENDERER.test(source)) {
|
|
5025
|
+
return importer ? RENDERER_STUB + encodeURIComponent(importer) : undefined;
|
|
5026
|
+
}
|
|
5027
|
+
// A bare import from app code of a package the build leaves external:
|
|
5028
|
+
// its own import of react-dom/server is never resolved here, so the
|
|
5029
|
+
// stub above never sees it. @react-email/render, imported by an
|
|
5030
|
+
// action, built clean and threw React's refusal at the first visitor.
|
|
5031
|
+
// The package is read once for the import, and the build warns the
|
|
5032
|
+
// same way it does for a direct one - naming the app file and the
|
|
5033
|
+
// package - without stubbing a module that may also be used well.
|
|
5034
|
+
if (!importer || importer.includes("/node_modules/") || importer.startsWith("\0"))
|
|
5035
|
+
return;
|
|
5036
|
+
if (!/^(?:@[\w.-]+\/)?[\w.-]+/.test(source) || source.startsWith("."))
|
|
5037
|
+
return;
|
|
5038
|
+
// The generated entries import plugin-rsc, whose ssr half imports the
|
|
5039
|
+
// renderer for its own reasons; that is the build's, not the app's.
|
|
5040
|
+
if (outDir && importer.startsWith(outDir))
|
|
5041
|
+
return;
|
|
5042
|
+
const name = source.startsWith("@") ? source.split("/").slice(0, 2).join("/") : source.split("/")[0];
|
|
5043
|
+
if (name === "react-dom" || name === "react" || name.startsWith("@rsc-kit/") || name.startsWith("@vitejs/"))
|
|
5044
|
+
return;
|
|
5045
|
+
let imports = externalRenderers.get(name);
|
|
5046
|
+
if (imports === undefined) {
|
|
5047
|
+
// Vite answers a dependency either as external, by its bare name,
|
|
5048
|
+
// or by the file it resolved to under node_modules - which of the
|
|
5049
|
+
// two depends on how the environment was set up, and both are a
|
|
5050
|
+
// dependency whose imports the build will not look inside.
|
|
5051
|
+
const resolved = await this.resolve(source, importer, { skipSelf: true });
|
|
5052
|
+
const id = resolved?.id ?? "";
|
|
5053
|
+
const marker = `/node_modules/${name}/`;
|
|
5054
|
+
const at = id.lastIndexOf(marker);
|
|
5055
|
+
const dir = resolved?.external
|
|
5056
|
+
? installedPackageDir(name, dirname(importer.split("?")[0]))
|
|
5057
|
+
: at !== -1
|
|
5058
|
+
? id.slice(0, at + marker.length - 1)
|
|
5059
|
+
: null;
|
|
5060
|
+
imports = dir !== null && importsServerRenderer(dir);
|
|
5061
|
+
externalRenderers.set(name, imports);
|
|
5062
|
+
}
|
|
5063
|
+
if (!imports)
|
|
4714
5064
|
return;
|
|
4715
|
-
|
|
5065
|
+
const message = `${relative(process.cwd(), importer.split("?")[0])} imports ${name}, which imports react-dom/server, and ` +
|
|
5066
|
+
serverRendererMessage(null).replace(/^react-dom\/server cannot run/, "that cannot run");
|
|
5067
|
+
if (!warned.has(message)) {
|
|
5068
|
+
warned.add(message);
|
|
5069
|
+
warnedPackages.add(name);
|
|
5070
|
+
this.warn(message);
|
|
5071
|
+
}
|
|
4716
5072
|
},
|
|
4717
5073
|
load(id) {
|
|
4718
5074
|
if (!id.startsWith(RENDERER_STUB))
|
|
4719
5075
|
return;
|
|
4720
5076
|
const importer = decodeURIComponent(id.slice(RENDERER_STUB.length)) || null;
|
|
4721
5077
|
const message = serverRendererMessage(appImporter(this, importer));
|
|
4722
|
-
|
|
5078
|
+
// The last node_modules segment: Bun's store nests the real package
|
|
5079
|
+
// under node_modules/.bun/<pkg>@<v>/node_modules/<pkg>.
|
|
5080
|
+
const viaPackage = [...(importer ?? "").matchAll(/\/node_modules\/((?:@[^/]+\/)?[^/]+)\//g)].at(-1)?.[1];
|
|
5081
|
+
if (this.environment.mode === "build" &&
|
|
5082
|
+
!warned.has(message) &&
|
|
5083
|
+
!(viaPackage && warnedPackages.has(viaPackage))) {
|
|
4723
5084
|
warned.add(message);
|
|
4724
5085
|
this.warn(message);
|
|
4725
5086
|
}
|
|
4726
|
-
|
|
5087
|
+
// Every name the renderer exports, each throwing the fix when called.
|
|
5088
|
+
// A module that threw when loaded took the whole server down at boot
|
|
5089
|
+
// for one action nobody had called yet, and a stub with no exports
|
|
5090
|
+
// failed the build on MISSING_EXPORT with the message scrolled past.
|
|
5091
|
+
const refuse = `() => { throw new Error(${JSON.stringify(message)}) }`;
|
|
5092
|
+
return (SERVER_RENDERER_EXPORTS.map((name) => `export const ${name} = ${refuse};`).join("\n") +
|
|
5093
|
+
`\nexport const version = "0.0.0";\nexport default { ${SERVER_RENDERER_EXPORTS.join(", ")}, version };\n`);
|
|
4727
5094
|
},
|
|
4728
5095
|
};
|
|
4729
5096
|
}
|
|
@@ -4911,6 +5278,14 @@ export function rscKit(options = {}) {
|
|
|
4911
5278
|
...RUNTIME_BUILTINS,
|
|
4912
5279
|
...[...DEFAULT_SERVER_EXTERNALS, ...(options.serverExternalPackages ?? [])].map(externalPackage),
|
|
4913
5280
|
];
|
|
5281
|
+
// Dependencies with a "use client" file that plugin-rsc would leave
|
|
5282
|
+
// external, because they never declared react as a peer. Bundled into the
|
|
5283
|
+
// server graphs so the directive is read, the same as a package that did.
|
|
5284
|
+
const bundledClientPackages = clientPackages(projectRoot);
|
|
5285
|
+
for (const name of bundledClientPackages) {
|
|
5286
|
+
console.warn(`[rsc-kit] bundling ${name}: it has "use client" files but does not declare react ` +
|
|
5287
|
+
"as a peer dependency, so its components would otherwise run on the server.");
|
|
5288
|
+
}
|
|
4914
5289
|
const routesPlugin = {
|
|
4915
5290
|
name: "rsc-kit",
|
|
4916
5291
|
// A Nitro module, which Nitro's Vite plugin collects from any plugin
|
|
@@ -4920,9 +5295,49 @@ export function rscKit(options = {}) {
|
|
|
4920
5295
|
nitro: {
|
|
4921
5296
|
name: "rsc-kit",
|
|
4922
5297
|
setup(nitro) {
|
|
4923
|
-
if (nitro.options.dev
|
|
5298
|
+
if (nitro.options.dev)
|
|
4924
5299
|
return;
|
|
5300
|
+
// External at Nitro's layer too. The Vite build leaves these as
|
|
5301
|
+
// imports, and Nitro would then bundle them into its own chunks -
|
|
5302
|
+
// which for a native package fails at load, and for a polyfill like
|
|
5303
|
+
// reflect-metadata runs it after the chunk that checks for it.
|
|
5304
|
+
// Traced, each is copied into .output/server/node_modules and
|
|
5305
|
+
// imported by the built server the way its author expected.
|
|
5306
|
+
// Only the ones the project has. Nitro's tracer says so, once per
|
|
5307
|
+
// name, for every entry it cannot find - and a list of every native
|
|
5308
|
+
// package anyone might install is mostly ones this app does not.
|
|
5309
|
+
nitro.options.traceDeps = [
|
|
5310
|
+
...(nitro.options.traceDeps ?? []),
|
|
5311
|
+
...[...DEFAULT_SERVER_EXTERNALS, ...(options.serverExternalPackages ?? [])].filter((name) => installedPackageDir(name, projectRoot) !== null || packageEntryInGraph(projectRoot, name) !== null),
|
|
5312
|
+
];
|
|
5313
|
+
// The assets, precompressed at build and served with their encoding
|
|
5314
|
+
// by Nitro's own static handler; the host gzips the rest as it
|
|
5315
|
+
// answers. Between them a bun or node server answering the internet
|
|
5316
|
+
// by itself sends nothing raw. A config that set this keeps it.
|
|
5317
|
+
// Nitro's default is a literal false, so ?? would not do; an object
|
|
5318
|
+
// is a configuration and is kept.
|
|
5319
|
+
if (typeof nitro.options.compressPublicAssets !== "object")
|
|
5320
|
+
nitro.options.compressPublicAssets = true;
|
|
4925
5321
|
nitro.options.virtual ??= {};
|
|
5322
|
+
// A polyfill a dependency checks for at module evaluation, loaded
|
|
5323
|
+
// before any service. The bundler places an external import after
|
|
5324
|
+
// the chunk imports of the module that had it, whatever the source
|
|
5325
|
+
// said, so reflect-metadata came up after tsyringe (under
|
|
5326
|
+
// @peculiar/x509, under @simplewebauthn/server) - by luck survivable
|
|
5327
|
+
// run as a directory, fatal compiled into a binary. A Nitro plugin
|
|
5328
|
+
// is evaluated by the entry itself, and the services that carry the
|
|
5329
|
+
// app are loaded lazily after it; imported by file, the polyfill is
|
|
5330
|
+
// inlined there and runs first wherever the server runs. Only for a
|
|
5331
|
+
// project whose graph has it.
|
|
5332
|
+
const polyfills = FIRST_POLYFILLS.map((name) => packageEntryInGraph(projectRoot, name)).filter((entry) => entry !== null);
|
|
5333
|
+
if (polyfills.length) {
|
|
5334
|
+
nitro.options.virtual["#rsc-kit/polyfills"] =
|
|
5335
|
+
polyfills.map((entry) => `import ${JSON.stringify(entry)};`).join("\n") +
|
|
5336
|
+
"\nexport default () => {};\n";
|
|
5337
|
+
nitro.options.plugins = ["#rsc-kit/polyfills", ...(nitro.options.plugins ?? [])];
|
|
5338
|
+
}
|
|
5339
|
+
if (!instrumentationFile())
|
|
5340
|
+
return;
|
|
4926
5341
|
nitro.options.virtual["#rsc-kit/startup"] = STARTUP_PLUGIN;
|
|
4927
5342
|
nitro.options.plugins = [...(nitro.options.plugins ?? []), "#rsc-kit/startup"];
|
|
4928
5343
|
},
|
|
@@ -4938,6 +5353,7 @@ export function rscKit(options = {}) {
|
|
|
4938
5353
|
apiRoutes.clear();
|
|
4939
5354
|
discover(appDir);
|
|
4940
5355
|
registerMetadataRoutes(appDir);
|
|
5356
|
+
registerOpenApiRoutes();
|
|
4941
5357
|
siteHosts = ownHosts(rootMetadataBase(appDir), hostsOption);
|
|
4942
5358
|
// Silent when it worked. The names were printed on every dev start and
|
|
4943
5359
|
// every build — thirty of them for a middling app, above the output that
|
|
@@ -5117,6 +5533,11 @@ export function rscKit(options = {}) {
|
|
|
5117
5533
|
alias: aliasEntries(),
|
|
5118
5534
|
},
|
|
5119
5535
|
build: { emptyOutDir: true },
|
|
5536
|
+
// What reaches the browser. Vite's own VITE_ prefix, and PUBLIC_ - the
|
|
5537
|
+
// scaffold's spelling, and Next's minus its brand - so a variable
|
|
5538
|
+
// named for a port reads through import.meta.env without a config
|
|
5539
|
+
// line. A config that set its own prefixes keeps them; Vite merges.
|
|
5540
|
+
envPrefix: ["VITE_", "PUBLIC_"],
|
|
5120
5541
|
environments: {
|
|
5121
5542
|
// Server bundles — stay under the (non-public) out dir. `bun` and
|
|
5122
5543
|
// `bun:*` are the runtime's own modules, like `node:*`: nothing to
|
|
@@ -5129,6 +5550,8 @@ export function rscKit(options = {}) {
|
|
|
5129
5550
|
external: serverExternals,
|
|
5130
5551
|
},
|
|
5131
5552
|
},
|
|
5553
|
+
resolve: { noExternal: bundledClientPackages },
|
|
5554
|
+
optimizeDeps: { exclude: bundledClientPackages },
|
|
5132
5555
|
},
|
|
5133
5556
|
ssr: {
|
|
5134
5557
|
build: {
|
|
@@ -5137,6 +5560,8 @@ export function rscKit(options = {}) {
|
|
|
5137
5560
|
external: serverExternals,
|
|
5138
5561
|
},
|
|
5139
5562
|
},
|
|
5563
|
+
resolve: { noExternal: bundledClientPackages },
|
|
5564
|
+
optimizeDeps: { exclude: bundledClientPackages },
|
|
5140
5565
|
},
|
|
5141
5566
|
// Client bundle — emitted into public/ for the web server to serve.
|
|
5142
5567
|
client: {
|
|
@@ -5358,8 +5783,12 @@ export function rscKit(options = {}) {
|
|
|
5358
5783
|
// when the directory is not there. Only under Nitro, whose presets are
|
|
5359
5784
|
// the ones without a disk; on its own the plugin serves from outDir.
|
|
5360
5785
|
if (clientOut && existsSync(staticDir)) {
|
|
5361
|
-
const { inlineModuleName, inlineModuleSource } = await import("./files.js");
|
|
5786
|
+
const { inlineModuleName, inlineModuleSource, compileEntrySource } = await import("./files.js");
|
|
5362
5787
|
writeFileSync(join(dirname(staticDir), inlineModuleName(NITRO_STATIC_DIR)), await inlineModuleSource(staticDir));
|
|
5788
|
+
// And the entry a binary is compiled from, so the pages above end up
|
|
5789
|
+
// inside it rather than rendered live: bun build --compile
|
|
5790
|
+
// .output/server/compile.mjs. The scaffold's script names it.
|
|
5791
|
+
writeFileSync(join(dirname(staticDir), "compile.mjs"), compileEntrySource(NITRO_STATIC_DIR));
|
|
5363
5792
|
}
|
|
5364
5793
|
// Manifest first. The service worker precaches whatever it finds in this
|
|
5365
5794
|
// directory, so writing it afterwards leaves it out of the list — and an
|
|
@@ -5444,6 +5873,7 @@ export function rscKit(options = {}) {
|
|
|
5444
5873
|
useSsrModules(),
|
|
5445
5874
|
serverRendererInRsc(),
|
|
5446
5875
|
metadataRoutesPlugin(),
|
|
5876
|
+
openApiPlugin(),
|
|
5447
5877
|
extendableClientReferences(),
|
|
5448
5878
|
typecheckPlugin(),
|
|
5449
5879
|
clientImportsAudit(),
|