@rsc-kit/core 0.16.1 → 0.16.3
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/apiPrerender.d.ts +2 -0
- package/dist/apiPrerender.js +111 -81
- package/dist/apiPrerender.js.map +1 -1
- package/dist/appAssets.d.ts +4 -1
- package/dist/appAssets.js +55 -19
- package/dist/appAssets.js.map +1 -1
- package/dist/buildReport.d.ts +2 -0
- package/dist/buildReport.js.map +1 -1
- package/dist/files.js +29 -5
- package/dist/files.js.map +1 -1
- package/dist/host.d.ts +11 -0
- package/dist/host.js +61 -5
- package/dist/host.js.map +1 -1
- package/dist/js/createViteRscApp.js +57 -11
- package/dist/js/createViteRscApp.js.map +1 -1
- package/dist/js/devNotice.d.ts +13 -0
- package/dist/js/devNotice.js +45 -0
- package/dist/js/devNotice.js.map +1 -0
- package/dist/js/errors.js +4 -0
- package/dist/js/errors.js.map +1 -1
- package/dist/js/navigate.js +23 -0
- package/dist/js/navigate.js.map +1 -1
- package/dist/js/staleAssets.d.ts +17 -0
- package/dist/js/staleAssets.js +39 -0
- package/dist/js/staleAssets.js.map +1 -0
- package/dist/js/useSearchParams.d.ts +6 -0
- package/dist/js/useSearchParams.js +16 -3
- package/dist/js/useSearchParams.js.map +1 -1
- package/dist/prerender.d.ts +2 -2
- package/dist/prerender.js +190 -124
- package/dist/prerender.js.map +1 -1
- package/dist/request.d.ts +3 -1
- package/dist/request.js +131 -45
- package/dist/request.js.map +1 -1
- package/dist/routing.d.ts +0 -7
- package/dist/routing.js +54 -20
- package/dist/routing.js.map +1 -1
- package/dist/vite.d.ts +13 -12
- package/dist/vite.js +554 -324
- package/dist/vite.js.map +1 -1
- package/package.json +2 -2
package/dist/vite.js
CHANGED
|
@@ -12,23 +12,58 @@
|
|
|
12
12
|
// carry the route composition and the worker's render contract, and supplies
|
|
13
13
|
// the structural config (entries, output dirs, base). @vitejs/plugin-rsc is
|
|
14
14
|
// included here so it always runs before any react() layer the app adds.
|
|
15
|
-
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from
|
|
16
|
-
import { gzipSync } from
|
|
17
|
-
import { createHash } from
|
|
18
|
-
import { createRequire } from
|
|
19
|
-
import { dirname, join, relative, resolve } from
|
|
20
|
-
import { fileURLToPath, pathToFileURL } from
|
|
21
|
-
import rsc, { getPluginApi } from
|
|
22
|
-
import { loadEnv } from
|
|
23
|
-
import { REPORT_FILE, buildReport } from
|
|
24
|
-
import { MANIFEST_PATH, manifestWarning, webManifest } from
|
|
25
|
-
import { ASSET_BASE, appAssets, headTags } from
|
|
26
|
-
import { httpHostCalls } from
|
|
15
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, createReadStream, } from "node:fs";
|
|
16
|
+
import { gzipSync } from "node:zlib";
|
|
17
|
+
import { createHash } from "node:crypto";
|
|
18
|
+
import { createRequire } from "node:module";
|
|
19
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
20
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
21
|
+
import rsc, { getPluginApi } from "@vitejs/plugin-rsc";
|
|
22
|
+
import { loadEnv } from "vite";
|
|
23
|
+
import { REPORT_FILE, buildReport } from "./buildReport.js";
|
|
24
|
+
import { MANIFEST_PATH, manifestWarning, webManifest } from "./webManifest.js";
|
|
25
|
+
import { ASSET_BASE, allAppAssets, appAssets, headTags, typeOf, } from "./appAssets.js";
|
|
26
|
+
import { httpHostCalls } from "./hostCalls.js";
|
|
27
27
|
// Resolved once per rscKit() call. One build runs in one process, so these are
|
|
28
28
|
// module state rather than threaded through every helper.
|
|
29
29
|
let projectRoot;
|
|
30
30
|
let sourceDir;
|
|
31
|
-
let inlineStylesheets =
|
|
31
|
+
let inlineStylesheets = "auto";
|
|
32
|
+
let resolvedConfig = null;
|
|
33
|
+
/** Modules a runtime provides and no bundle should try to carry. */
|
|
34
|
+
const RUNTIME_BUILTINS = ["bun", /^bun:/];
|
|
35
|
+
/**
|
|
36
|
+
* What a client chunk is called on disk.
|
|
37
|
+
*
|
|
38
|
+
* A "use server" module reaches the browser as a proxy - one stub per export
|
|
39
|
+
* that calls the action by id - and the proxy keeps the file's identity, so
|
|
40
|
+
* the bundler can name a chunk after it. When shared client code is merged
|
|
41
|
+
* into that chunk the output has a file called `auth-actions-…js` in
|
|
42
|
+
* public/assets: fifty kilobytes of UI components with the name of the
|
|
43
|
+
* server file, which reads as a leak to anyone who looks. Nothing leaked -
|
|
44
|
+
* the proxy holds ids, never bodies - but a name that says otherwise is a
|
|
45
|
+
* bug. Such a chunk is called what it is.
|
|
46
|
+
*/
|
|
47
|
+
function clientChunkFileName(chunk) {
|
|
48
|
+
const table = resolvedConfig
|
|
49
|
+
? getPluginApi(resolvedConfig)?.manager?.serverReferences?.metaMap
|
|
50
|
+
: undefined;
|
|
51
|
+
if (table && table.size > 0) {
|
|
52
|
+
const bare = (id) => {
|
|
53
|
+
const file = id.split("?")[0].split("/").pop() ?? "";
|
|
54
|
+
return file.replace(/\.[^.]+$/, "");
|
|
55
|
+
};
|
|
56
|
+
const serverModules = new Set([...table.keys()].map(bare));
|
|
57
|
+
const namedAfterServerModule = (chunk.facadeModuleId !== null &&
|
|
58
|
+
serverModules.has(bare(chunk.facadeModuleId))) ||
|
|
59
|
+
(serverModules.has(chunk.name) &&
|
|
60
|
+
chunk.moduleIds.some((id) => bare(id) === chunk.name));
|
|
61
|
+
if (namedAfterServerModule)
|
|
62
|
+
return "assets/client-[hash].js";
|
|
63
|
+
}
|
|
64
|
+
return "assets/[name]-[hash].js";
|
|
65
|
+
}
|
|
66
|
+
let maxActionBody;
|
|
32
67
|
let outDir;
|
|
33
68
|
let appDir;
|
|
34
69
|
let genDir;
|
|
@@ -63,7 +98,13 @@ let viewTransitions = false;
|
|
|
63
98
|
/** Whether a service worker is generated and registered — see options. */
|
|
64
99
|
let offline = false;
|
|
65
100
|
let webManifestOptions = null;
|
|
66
|
-
let foundAssets = {
|
|
101
|
+
let foundAssets = {
|
|
102
|
+
favicon: null,
|
|
103
|
+
icons: [],
|
|
104
|
+
appleIcon: null,
|
|
105
|
+
openGraph: null,
|
|
106
|
+
twitter: null,
|
|
107
|
+
};
|
|
67
108
|
/** Host functions to generate stubs for — see RscKitOptions.hostActions. */
|
|
68
109
|
let hostActions;
|
|
69
110
|
/**
|
|
@@ -78,11 +119,11 @@ let hostActions;
|
|
|
78
119
|
/** This package's name, for excluding it from dep optimization. */
|
|
79
120
|
const PACKAGE_NAME = (() => {
|
|
80
121
|
try {
|
|
81
|
-
const manifest = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)),
|
|
82
|
-
return manifest.name ??
|
|
122
|
+
const manifest = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf-8"));
|
|
123
|
+
return manifest.name ?? "@rsc-kit/core";
|
|
83
124
|
}
|
|
84
125
|
catch {
|
|
85
|
-
return
|
|
126
|
+
return "@rsc-kit/core";
|
|
86
127
|
}
|
|
87
128
|
})();
|
|
88
129
|
function thisDir() {
|
|
@@ -97,7 +138,7 @@ function envRouteConfig() {
|
|
|
97
138
|
return { file, dynamicPattern: new RegExp(pattern) };
|
|
98
139
|
}
|
|
99
140
|
/** The file a backend writes its action names into. */
|
|
100
|
-
const HOST_ACTIONS_FILE =
|
|
141
|
+
const HOST_ACTIONS_FILE = "rsc-host-actions.json";
|
|
101
142
|
/**
|
|
102
143
|
* Host actions, read from a file the backend wrote.
|
|
103
144
|
*
|
|
@@ -118,8 +159,10 @@ function fileHostActions(root) {
|
|
|
118
159
|
if (!existsSync(path))
|
|
119
160
|
return {};
|
|
120
161
|
try {
|
|
121
|
-
const parsed = JSON.parse(readFileSync(path,
|
|
122
|
-
if (parsed === null ||
|
|
162
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
163
|
+
if (parsed === null ||
|
|
164
|
+
typeof parsed !== "object" ||
|
|
165
|
+
Array.isArray(parsed)) {
|
|
123
166
|
throw new Error('expected an object of { jsName: "Class.method" }');
|
|
124
167
|
}
|
|
125
168
|
return parsed;
|
|
@@ -144,23 +187,23 @@ function fileHostActions(root) {
|
|
|
144
187
|
function aliasEntries() {
|
|
145
188
|
if (!packageAlias)
|
|
146
189
|
return [];
|
|
147
|
-
if (existsSync(join(projectRoot,
|
|
190
|
+
if (existsSync(join(projectRoot, "node_modules", packageAlias)))
|
|
148
191
|
return [];
|
|
149
192
|
return [
|
|
150
193
|
{
|
|
151
|
-
find: new RegExp(
|
|
152
|
-
replacement: join(packageDir,
|
|
194
|
+
find: new RegExp("^" + packageAlias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "/(.*)$"),
|
|
195
|
+
replacement: join(packageDir, "js") + "/$1",
|
|
153
196
|
},
|
|
154
197
|
];
|
|
155
198
|
}
|
|
156
199
|
function resolvePaths(options) {
|
|
157
200
|
projectRoot = resolve(options.projectRoot || process.env.RSC_PROJECT_ROOT || process.cwd());
|
|
158
|
-
sourceDir = resolve(options.sourceDir || process.env.RSC_SOURCE_DIR || join(projectRoot,
|
|
159
|
-
outDir = resolve(options.outDir || process.env.RSC_OUT_DIR || join(projectRoot,
|
|
160
|
-
appDir = join(sourceDir,
|
|
201
|
+
sourceDir = resolve(options.sourceDir || process.env.RSC_SOURCE_DIR || join(projectRoot, "src"));
|
|
202
|
+
outDir = resolve(options.outDir || process.env.RSC_OUT_DIR || join(projectRoot, ".rsc"));
|
|
203
|
+
appDir = join(sourceDir, "app");
|
|
161
204
|
// Generated entries live under the (in-project) out dir so module resolution
|
|
162
205
|
// can walk up to the project's node_modules (@vitejs/plugin-rsc, react, ...).
|
|
163
|
-
genDir = join(outDir,
|
|
206
|
+
genDir = join(outDir, ".gen");
|
|
164
207
|
// Not a migration guard — those are not worth carrying in a pre-release.
|
|
165
208
|
// This is the silent-failure guard the rest of this file is written to be.
|
|
166
209
|
//
|
|
@@ -172,31 +215,35 @@ function resolvePaths(options) {
|
|
|
172
215
|
//
|
|
173
216
|
// There is deliberately no check for `nitro`. It is gone from the type, and
|
|
174
217
|
// a config still passing `nitro: true` is asking for exactly what it gets.
|
|
175
|
-
const removed = [
|
|
218
|
+
const removed = ["assetsDir", "assetsUrl"].filter((key) => options[key] !== undefined);
|
|
176
219
|
if (removed.length > 0) {
|
|
177
|
-
throw new Error(`[rsc-kit] ${removed.join(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
220
|
+
throw new Error(`[rsc-kit] ${removed.join(" and ")} ${removed.length === 1 ? "is" : "are"} no longer an option.\n\n` +
|
|
221
|
+
"Nitro publishes the browser assets to .output/public and serves them from its own\n" +
|
|
222
|
+
"root, so there is no prefix to set. That directory is the deployment — point nginx\n" +
|
|
223
|
+
"or a CDN at it if something other than the app should serve them.");
|
|
181
224
|
}
|
|
182
225
|
// Vite's own default. Nitro overrides it with .output/public, which is why
|
|
183
226
|
// there is nothing here to configure.
|
|
184
|
-
publicAssetsDir = resolve(join(projectRoot,
|
|
185
|
-
typesDir = join(projectRoot,
|
|
186
|
-
hotFile = options.hotFile || process.env.RSC_HOT_FILE ||
|
|
227
|
+
publicAssetsDir = resolve(join(projectRoot, "dist/client"));
|
|
228
|
+
typesDir = join(projectRoot, ".rsc-kit");
|
|
229
|
+
hotFile = options.hotFile || process.env.RSC_HOT_FILE || "";
|
|
187
230
|
hostCallOptions = options.hostCall;
|
|
188
231
|
packageDir = resolve(options.packageDir || process.env.RSC_PACKAGE_DIR || thisDir());
|
|
189
|
-
hostGlobal = options.hostGlobal || process.env.RSC_HOST_GLOBAL ||
|
|
190
|
-
interceptManifestFile = resolve(options.interceptManifestFile ||
|
|
232
|
+
hostGlobal = options.hostGlobal || process.env.RSC_HOST_GLOBAL || "rpc";
|
|
233
|
+
interceptManifestFile = resolve(options.interceptManifestFile ||
|
|
234
|
+
process.env.RSC_INTERCEPT_MANIFEST ||
|
|
235
|
+
join(outDir, "intercept-manifest.json"));
|
|
191
236
|
packageAlias = options.packageAlias || process.env.RSC_PACKAGE_ALIAS || null;
|
|
192
|
-
devOrigin = options.devOrigin || process.env.RSC_DEV_ORIGIN ||
|
|
193
|
-
output = options.output || process.env.RSC_OUTPUT ||
|
|
194
|
-
exportPath = options.exportPath || process.env.RSC_EXPORT_PATH ||
|
|
237
|
+
devOrigin = options.devOrigin || process.env.RSC_DEV_ORIGIN || "";
|
|
238
|
+
output = options.output || process.env.RSC_OUTPUT || "server";
|
|
239
|
+
exportPath = options.exportPath || process.env.RSC_EXPORT_PATH || "dist";
|
|
195
240
|
// An export decides this for itself: the client has to ask for payloads by
|
|
196
241
|
// url because there is no server to read a header, and the name it asks for
|
|
197
242
|
// is the one the export writes.
|
|
198
243
|
staticPayloads =
|
|
199
|
-
options.staticPayloads ||
|
|
244
|
+
options.staticPayloads ||
|
|
245
|
+
process.env.RSC_STATIC_PAYLOADS ||
|
|
246
|
+
(output === "export" ? "index.rsc" : "");
|
|
200
247
|
// No default: which file marks a route dynamic is the host's convention, and
|
|
201
248
|
// guessing one here would bake a particular backend into a generic plugin.
|
|
202
249
|
// The env pair exists so a host driving the build out of process can pass it
|
|
@@ -204,19 +251,28 @@ function resolvePaths(options) {
|
|
|
204
251
|
routeConfig = options.routeConfig ?? envRouteConfig();
|
|
205
252
|
// A host driving the build out of process cannot pass an option, and may
|
|
206
253
|
// prerender itself afterwards with paths only it knows.
|
|
207
|
-
|
|
254
|
+
// No public switch. A page that must not be frozen says so with
|
|
255
|
+
// `await connection()`, and the build names every page that could not be
|
|
256
|
+
// rendered - the same model Next has with cache components, where the
|
|
257
|
+
// only opt-out is per page. RSC_PRERENDER=0 remains, internal: watch mode
|
|
258
|
+
// sets it, and so does a host that drives the build out of process and
|
|
259
|
+
// prerenders itself afterwards with paths only it knows.
|
|
260
|
+
prerenderAfterBuild = process.env.RSC_PRERENDER !== "0";
|
|
208
261
|
viewTransitions = options.viewTransitions === true;
|
|
209
262
|
offline = options.offline === true;
|
|
210
|
-
inlineStylesheets = options.inlineStylesheets ??
|
|
263
|
+
inlineStylesheets = options.inlineStylesheets ?? "auto";
|
|
264
|
+
maxActionBody = options.maxActionBody;
|
|
211
265
|
// One place, and it is the file. A plugin option as well would be the same
|
|
212
266
|
// thing sayable in two places, which is the problem the file was moved to
|
|
213
267
|
// solve rather than a convenience to keep beside it.
|
|
214
|
-
webManifestOptions = declaredManifest(join(sourceDir,
|
|
215
|
-
foundAssets = appAssets(join(sourceDir,
|
|
268
|
+
webManifestOptions = declaredManifest(join(sourceDir, "app"));
|
|
269
|
+
foundAssets = appAssets(join(sourceDir, "app"));
|
|
216
270
|
// An app that put icons where they could be found has already listed them.
|
|
217
271
|
// Writing them again in the manifest is the same set in two places, and the
|
|
218
272
|
// one that goes stale is the one nobody looks at.
|
|
219
|
-
if (webManifestOptions &&
|
|
273
|
+
if (webManifestOptions &&
|
|
274
|
+
!webManifestOptions.icons?.length &&
|
|
275
|
+
foundAssets.icons.length) {
|
|
220
276
|
webManifestOptions = {
|
|
221
277
|
...webManifestOptions,
|
|
222
278
|
icons: foundAssets.icons.map((icon) => icon.href),
|
|
@@ -225,7 +281,7 @@ function resolvePaths(options) {
|
|
|
225
281
|
hostActions = options.hostActions ?? fileHostActions(projectRoot);
|
|
226
282
|
}
|
|
227
283
|
function log(...args) {
|
|
228
|
-
console.error(
|
|
284
|
+
console.error("[rsc-kit]", ...args);
|
|
229
285
|
}
|
|
230
286
|
// ── The route manifest ───────────────────────────────────────────────────────
|
|
231
287
|
/**
|
|
@@ -243,37 +299,37 @@ function log(...args) {
|
|
|
243
299
|
*/
|
|
244
300
|
/** `[...path]` → catchAll, `[id]` → param, `(group)` → nothing at all. */
|
|
245
301
|
function urlSegments(componentName) {
|
|
246
|
-
const parts = componentName.split(
|
|
302
|
+
const parts = componentName.split("/").slice(1, -1);
|
|
247
303
|
const segments = [];
|
|
248
304
|
for (const part of parts) {
|
|
249
305
|
// A route group organises files without appearing in the url.
|
|
250
|
-
if (part.startsWith(
|
|
306
|
+
if (part.startsWith("(") && part.endsWith(")"))
|
|
251
307
|
continue;
|
|
252
308
|
// A slot directory is not part of its page's url either.
|
|
253
|
-
if (part.startsWith(
|
|
309
|
+
if (part.startsWith("@"))
|
|
254
310
|
continue;
|
|
255
|
-
if (part.startsWith(
|
|
256
|
-
segments.push({ type:
|
|
311
|
+
if (part.startsWith("[...") && part.endsWith("]")) {
|
|
312
|
+
segments.push({ type: "catchAll", value: part.slice(4, -1) });
|
|
257
313
|
continue;
|
|
258
314
|
}
|
|
259
|
-
if (part.startsWith(
|
|
260
|
-
segments.push({ type:
|
|
315
|
+
if (part.startsWith("[") && part.endsWith("]")) {
|
|
316
|
+
segments.push({ type: "param", value: part.slice(1, -1) });
|
|
261
317
|
continue;
|
|
262
318
|
}
|
|
263
319
|
// An interception marker says which url this replaces, not what it is
|
|
264
320
|
// called: (.)photo intercepts the sibling /photo. Left in place the
|
|
265
321
|
// manifest would claim a route at /(.)photo, which nothing can navigate to.
|
|
266
|
-
segments.push({ type:
|
|
322
|
+
segments.push({ type: "static", value: part.replace(/^\(\.{1,3}\)/, "") });
|
|
267
323
|
}
|
|
268
324
|
return segments;
|
|
269
325
|
}
|
|
270
326
|
/** Whether a component sits under an interception marker: (.) (..) (...) */
|
|
271
327
|
function isIntercept(componentName) {
|
|
272
|
-
return componentName.split(
|
|
328
|
+
return componentName.split("/").some((part) => /^\(\.{1,3}\)/.test(part));
|
|
273
329
|
}
|
|
274
330
|
/** The slot directory a component lives under, if any. */
|
|
275
331
|
function slotOf(componentName) {
|
|
276
|
-
const part = componentName.split(
|
|
332
|
+
const part = componentName.split("/").find((p) => p.startsWith("@"));
|
|
277
333
|
return part ? part.slice(1) : null;
|
|
278
334
|
}
|
|
279
335
|
/**
|
|
@@ -284,15 +340,17 @@ function slotOf(componentName) {
|
|
|
284
340
|
*/
|
|
285
341
|
function routeManifest() {
|
|
286
342
|
const names = [...components.keys()];
|
|
287
|
-
const dirOf = (name) => name.split(
|
|
343
|
+
const dirOf = (name) => name.split("/").slice(0, -1).join("/");
|
|
288
344
|
// By path, not by string: 'app/slow3' begins with 'app/slow' as text and is
|
|
289
345
|
// not inside it, which would hand /slow3 the loading state of /slow.
|
|
290
|
-
const isUnder = (dir, ancestor) => dir === ancestor || dir.startsWith(ancestor +
|
|
346
|
+
const isUnder = (dir, ancestor) => dir === ancestor || dir.startsWith(ancestor + "/");
|
|
291
347
|
const ancestors = (name, base) => names
|
|
292
|
-
.filter((n) => n.endsWith(
|
|
348
|
+
.filter((n) => n.endsWith("/" + base) &&
|
|
349
|
+
!isIntercept(n) &&
|
|
350
|
+
isUnder(dirOf(name), dirOf(n)))
|
|
293
351
|
.sort((a, b) => a.length - b.length);
|
|
294
352
|
/** Project-root-relative, posix — the same string on every machine. */
|
|
295
|
-
const fromRoot = (abs) => relative(projectRoot, abs).replace(/\\/g,
|
|
353
|
+
const fromRoot = (abs) => relative(projectRoot, abs).replace(/\\/g, "/");
|
|
296
354
|
/** The host's config file in a directory, if the host named one and it exists. */
|
|
297
355
|
const configIn = (absDir) => {
|
|
298
356
|
if (!routeConfig)
|
|
@@ -303,9 +361,9 @@ function routeManifest() {
|
|
|
303
361
|
/** Ancestor configs, outermost first, excluding the page's own directory. */
|
|
304
362
|
const ancestorConfigs = (dir) => {
|
|
305
363
|
const found = [];
|
|
306
|
-
const parts = dir.split(
|
|
364
|
+
const parts = dir.split("/").slice(0, -1);
|
|
307
365
|
while (parts.length > 0) {
|
|
308
|
-
const path = configIn(join(sourceDir, parts.join(
|
|
366
|
+
const path = configIn(join(sourceDir, parts.join("/")));
|
|
309
367
|
if (path)
|
|
310
368
|
found.unshift(path);
|
|
311
369
|
parts.pop();
|
|
@@ -327,11 +385,11 @@ function routeManifest() {
|
|
|
327
385
|
* to whoever knows what they mean.
|
|
328
386
|
*/
|
|
329
387
|
const middlewareIn = (absDir) => {
|
|
330
|
-
for (const file of [
|
|
388
|
+
for (const file of ["route.ts", "route.tsx"]) {
|
|
331
389
|
const path = join(absDir, file);
|
|
332
390
|
if (!existsSync(path))
|
|
333
391
|
continue;
|
|
334
|
-
const match = readFileSync(path,
|
|
392
|
+
const match = readFileSync(path, "utf-8").match(/export\s+const\s+middleware\s*(?::[^=]+)?=\s*\[([^\]]*)\]/);
|
|
335
393
|
if (!match)
|
|
336
394
|
continue;
|
|
337
395
|
// Each quoted literal, rather than splitting the list on commas: a
|
|
@@ -353,7 +411,7 @@ function routeManifest() {
|
|
|
353
411
|
* outermost point it was asked for.
|
|
354
412
|
*/
|
|
355
413
|
const hostMiddleware = (dir) => {
|
|
356
|
-
const parts = dir.split(
|
|
414
|
+
const parts = dir.split("/").filter(Boolean);
|
|
357
415
|
const found = [];
|
|
358
416
|
for (let depth = 0; depth <= parts.length; depth++) {
|
|
359
417
|
for (const name of middlewareIn(join(sourceDir, ...parts.slice(0, depth)))) {
|
|
@@ -366,35 +424,43 @@ function routeManifest() {
|
|
|
366
424
|
const routes = [];
|
|
367
425
|
const intercepts = [];
|
|
368
426
|
for (const name of names) {
|
|
369
|
-
if (name.endsWith(
|
|
427
|
+
if (name.endsWith("/page") && isIntercept(name)) {
|
|
370
428
|
const slot = slotOf(name);
|
|
371
429
|
if (slot) {
|
|
372
|
-
const marker = name
|
|
373
|
-
|
|
430
|
+
const marker = name
|
|
431
|
+
.split("/")
|
|
432
|
+
.find((p) => /^\(\.{1,3}\)/.test(p))
|
|
433
|
+
?.match(/^\(\.{1,3}\)/)?.[0] ?? "(.)";
|
|
434
|
+
intercepts.push({
|
|
435
|
+
component: name,
|
|
436
|
+
slot,
|
|
437
|
+
segments: urlSegments(name),
|
|
438
|
+
marker,
|
|
439
|
+
});
|
|
374
440
|
}
|
|
375
441
|
continue;
|
|
376
442
|
}
|
|
377
|
-
if (!name.endsWith(
|
|
443
|
+
if (!name.endsWith("/page") || slotOf(name))
|
|
378
444
|
continue;
|
|
379
445
|
const slots = {};
|
|
380
446
|
for (const candidate of names) {
|
|
381
447
|
const slot = slotOf(candidate);
|
|
382
448
|
// A slot belongs to the layout in the directory that declares it, so it
|
|
383
449
|
// applies to a page only if that directory is on the page's path.
|
|
384
|
-
if (!slot || isIntercept(candidate) || !candidate.endsWith(
|
|
450
|
+
if (!slot || isIntercept(candidate) || !candidate.endsWith("/default"))
|
|
385
451
|
continue;
|
|
386
|
-
if (isUnder(dirOf(name), candidate.split(
|
|
452
|
+
if (isUnder(dirOf(name), candidate.split("/@")[0]))
|
|
387
453
|
slots[slot] = candidate;
|
|
388
454
|
}
|
|
389
455
|
routes.push({
|
|
390
456
|
component: name,
|
|
391
457
|
segments: urlSegments(name),
|
|
392
|
-
layouts: ancestors(name,
|
|
393
|
-
loadings: ancestors(name,
|
|
394
|
-
errors: ancestors(name,
|
|
395
|
-
middleware: ancestors(name,
|
|
458
|
+
layouts: ancestors(name, "layout").map((n) => n),
|
|
459
|
+
loadings: ancestors(name, "loading").map((n) => n),
|
|
460
|
+
errors: ancestors(name, "error"),
|
|
461
|
+
middleware: ancestors(name, "middleware").map((n) => n),
|
|
396
462
|
slots,
|
|
397
|
-
sections: names.filter((n) => SECTION_FILE.test(n +
|
|
463
|
+
sections: names.filter((n) => SECTION_FILE.test(n + ".tsx") && dirOf(n) === dirOf(name)),
|
|
398
464
|
config: configIn(join(sourceDir, dirOf(name))),
|
|
399
465
|
hostMiddleware: hostMiddleware(dirOf(name)),
|
|
400
466
|
ancestorConfigs: ancestorConfigs(dirOf(name)),
|
|
@@ -412,7 +478,7 @@ function routeManifest() {
|
|
|
412
478
|
name,
|
|
413
479
|
segments: urlSegments(name),
|
|
414
480
|
methods,
|
|
415
|
-
middleware: ancestors(name,
|
|
481
|
+
middleware: ancestors(name, "middleware"),
|
|
416
482
|
})),
|
|
417
483
|
};
|
|
418
484
|
}
|
|
@@ -432,17 +498,17 @@ function routeManifest() {
|
|
|
432
498
|
* a line of output; silent costs the types.
|
|
433
499
|
*/
|
|
434
500
|
function warnIfTypesUnreachable() {
|
|
435
|
-
const config = join(projectRoot,
|
|
501
|
+
const config = join(projectRoot, "tsconfig.json");
|
|
436
502
|
if (!existsSync(config))
|
|
437
503
|
return;
|
|
438
504
|
try {
|
|
439
505
|
// Comments are legal in a tsconfig and JSON.parse does not take them.
|
|
440
|
-
const text = readFileSync(config,
|
|
506
|
+
const text = readFileSync(config, "utf-8").replace(/\/\*[\s\S]*?\*\/|(^|\s)\/\/.*$/gm, "$1");
|
|
441
507
|
const include = JSON.parse(text).include;
|
|
442
508
|
if (!Array.isArray(include))
|
|
443
509
|
return;
|
|
444
|
-
const covers = (what) => include.some((entry) => typeof entry ===
|
|
445
|
-
if (!covers(
|
|
510
|
+
const covers = (what) => include.some((entry) => typeof entry === "string" && entry.includes(what));
|
|
511
|
+
if (!covers(".rsc-kit")) {
|
|
446
512
|
log(`tsconfig.json does not include .rsc-kit, where the generated types are written.\n` +
|
|
447
513
|
` Add ".rsc-kit/**/*" to "include", or typed routes and rpc() fall back to string.`);
|
|
448
514
|
}
|
|
@@ -479,17 +545,17 @@ function writeHostBindings(manifest) {
|
|
|
479
545
|
// The global is installed at runtime, so nothing in app source declares it
|
|
480
546
|
// and a typecheck cannot see it. Written whether or not there are actions:
|
|
481
547
|
// server components call it directly too.
|
|
482
|
-
writeFileSync(join(typesDir,
|
|
548
|
+
writeFileSync(join(typesDir, "rsc-env.d.ts"), renderHostGlobalTypes());
|
|
483
549
|
// The urls this build found, so a link to a page that does not exist fails
|
|
484
550
|
// the typecheck instead of the browser.
|
|
485
|
-
writeFileSync(join(typesDir,
|
|
551
|
+
writeFileSync(join(typesDir, "rsc-routes.d.ts"), renderRouteTypes(manifest));
|
|
486
552
|
// The bundle the host imports is generated, so nothing declares it. Written
|
|
487
553
|
// here rather than left to the app: every app needs the identical file, and
|
|
488
554
|
// an app-authored one goes stale — the first version named only RscEngine,
|
|
489
555
|
// which typechecks a server and fails a prerender script.
|
|
490
|
-
writeFileSync(join(typesDir,
|
|
556
|
+
writeFileSync(join(typesDir, "rsc-engine.d.ts"), ENGINE_TYPES);
|
|
491
557
|
warnIfTypesUnreachable();
|
|
492
|
-
const target = join(sourceDir,
|
|
558
|
+
const target = join(sourceDir, "server-actions.generated.ts");
|
|
493
559
|
// A host with no functions of its own leaves no file behind: kept, its
|
|
494
560
|
// stubs would go on naming targets the host has stopped answering for.
|
|
495
561
|
if (Object.keys(hostActions).length === 0) {
|
|
@@ -538,8 +604,8 @@ function reportAllDynamic() {
|
|
|
538
604
|
// The pattern rather than a url: nothing was rendered, so there are no
|
|
539
605
|
// params and inventing one would name a page that may not exist.
|
|
540
606
|
const path = route.segments
|
|
541
|
-
.map((segment) =>
|
|
542
|
-
.join(
|
|
607
|
+
.map((segment) => segment.type === "static" ? segment.value : `[${segment.value}]`)
|
|
608
|
+
.join("/");
|
|
543
609
|
console.log(` \u0192 /${path}`);
|
|
544
610
|
}
|
|
545
611
|
console.log(`
|
|
@@ -607,7 +673,7 @@ ${swExtra
|
|
|
607
673
|
// before anything here can call respondWith on it.
|
|
608
674
|
self.importScripts(${JSON.stringify(swExtra)})
|
|
609
675
|
`
|
|
610
|
-
:
|
|
676
|
+
: ""}
|
|
611
677
|
|
|
612
678
|
self.addEventListener('install', (event) => {
|
|
613
679
|
// The new worker takes over rather than waiting for every tab to close.
|
|
@@ -848,11 +914,11 @@ self.addEventListener('fetch', (event) => {
|
|
|
848
914
|
* it. The file is a literal object by contract, which is all that has to parse.
|
|
849
915
|
*/
|
|
850
916
|
export function declaredManifest(appDir) {
|
|
851
|
-
for (const extension of [
|
|
917
|
+
for (const extension of ["ts", "tsx", "js", "mjs"]) {
|
|
852
918
|
const file = join(appDir, `manifest.${extension}`);
|
|
853
919
|
if (!existsSync(file))
|
|
854
920
|
continue;
|
|
855
|
-
const source = readFileSync(file,
|
|
921
|
+
const source = readFileSync(file, "utf-8");
|
|
856
922
|
// The object literal after `export default`, with a `satisfies` or `as`
|
|
857
923
|
// annotation tolerated after it — which is how anyone who wants the type
|
|
858
924
|
// checked will actually write the file, and getting that wrong would refuse
|
|
@@ -864,8 +930,8 @@ export function declaredManifest(appDir) {
|
|
|
864
930
|
const match = /export\s+default\s+(\{[\s\S]*\})(?:\s+(?:satisfies|as)\s+[\w.<>\[\]| ]+)?\s*;?\s*$/.exec(source.trim());
|
|
865
931
|
if (!match) {
|
|
866
932
|
throw new Error(`[rsc-kit] app/manifest.${extension} must default-export an object literal.\n` +
|
|
867
|
-
|
|
868
|
-
|
|
933
|
+
"It is read at build time, before there is a module graph to evaluate it in, so it " +
|
|
934
|
+
"cannot be computed or imported from elsewhere.");
|
|
869
935
|
}
|
|
870
936
|
try {
|
|
871
937
|
// Function rather than JSON.parse: the file is TypeScript source with
|
|
@@ -890,19 +956,13 @@ export function declaredManifest(appDir) {
|
|
|
890
956
|
function copyAppAssets(clientDir) {
|
|
891
957
|
if (!existsSync(clientDir))
|
|
892
958
|
return;
|
|
893
|
-
const all =
|
|
894
|
-
...(foundAssets.favicon ? [foundAssets.favicon] : []),
|
|
895
|
-
...foundAssets.icons,
|
|
896
|
-
...(foundAssets.appleIcon ? [foundAssets.appleIcon] : []),
|
|
897
|
-
...(foundAssets.openGraph ? [foundAssets.openGraph] : []),
|
|
898
|
-
...(foundAssets.twitter ? [foundAssets.twitter] : []),
|
|
899
|
-
];
|
|
959
|
+
const all = allAppAssets(foundAssets);
|
|
900
960
|
if (all.length === 0)
|
|
901
961
|
return;
|
|
902
962
|
for (const asset of all) {
|
|
903
963
|
const to = join(clientDir, asset.href.slice(1));
|
|
904
964
|
mkdirSync(dirname(to), { recursive: true });
|
|
905
|
-
copyFileSync(join(sourceDir,
|
|
965
|
+
copyFileSync(join(sourceDir, "app", asset.file), to);
|
|
906
966
|
}
|
|
907
967
|
log(`icons: ${all.length} copied from app/`);
|
|
908
968
|
}
|
|
@@ -926,20 +986,20 @@ function writeWebManifest(clientDir, options) {
|
|
|
926
986
|
* which read did it.
|
|
927
987
|
*/
|
|
928
988
|
function offlineFallback(frozen, results) {
|
|
929
|
-
const found = results.find((r) => r.url ===
|
|
989
|
+
const found = results.find((r) => r.url === "/offline");
|
|
930
990
|
if (!found)
|
|
931
991
|
return null;
|
|
932
|
-
if (!frozen.includes(
|
|
992
|
+
if (!frozen.includes("/offline")) {
|
|
933
993
|
// The reason already reads "dynamic — called cookies()", and this sentence
|
|
934
994
|
// has said "not stored" by the time it gets there, so the prefix would say
|
|
935
995
|
// it twice with a dash in the middle of both.
|
|
936
|
-
const why = found.reason?.replace(/^dynamic — /,
|
|
937
|
-
log(
|
|
938
|
-
(why ? `, because it ${why}` :
|
|
939
|
-
|
|
996
|
+
const why = found.reason?.replace(/^dynamic — /, "") ?? null;
|
|
997
|
+
log("offline: /offline cannot be the fallback" +
|
|
998
|
+
(why ? `, because it ${why}` : "") +
|
|
999
|
+
". A fallback has to be servable with no network at all.");
|
|
940
1000
|
return null;
|
|
941
1001
|
}
|
|
942
|
-
return
|
|
1002
|
+
return "/offline";
|
|
943
1003
|
}
|
|
944
1004
|
/**
|
|
945
1005
|
* The app's own service worker code, if it wrote any.
|
|
@@ -955,11 +1015,11 @@ function offlineFallback(frozen, results) {
|
|
|
955
1015
|
* file is there or it is not.
|
|
956
1016
|
*/
|
|
957
1017
|
function copyServiceWorkerExtra(clientDir) {
|
|
958
|
-
const source = join(sourceDir,
|
|
1018
|
+
const source = join(sourceDir, "app", "sw.js");
|
|
959
1019
|
if (!existsSync(source))
|
|
960
1020
|
return null;
|
|
961
|
-
copyFileSync(source, join(clientDir,
|
|
962
|
-
return
|
|
1021
|
+
copyFileSync(source, join(clientDir, "sw-app.js"));
|
|
1022
|
+
return "/sw-app.js";
|
|
963
1023
|
}
|
|
964
1024
|
function writeServiceWorker(clientDir, frozen = [], offlineUrl = null) {
|
|
965
1025
|
if (!existsSync(clientDir))
|
|
@@ -974,7 +1034,7 @@ function writeServiceWorker(clientDir, frozen = [], offlineUrl = null) {
|
|
|
974
1034
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
975
1035
|
// Not itself, and not a map: a debugger asks for those, a visitor does
|
|
976
1036
|
// not, and precaching them doubles what an install costs.
|
|
977
|
-
if (entry.name ===
|
|
1037
|
+
if (entry.name === "sw.js" || entry.name.endsWith(".map"))
|
|
978
1038
|
continue;
|
|
979
1039
|
const path = join(dir, entry.name);
|
|
980
1040
|
if (entry.isDirectory())
|
|
@@ -983,13 +1043,16 @@ function writeServiceWorker(clientDir, frozen = [], offlineUrl = null) {
|
|
|
983
1043
|
files.push(`${prefix}${entry.name}`);
|
|
984
1044
|
}
|
|
985
1045
|
};
|
|
986
|
-
walk(clientDir,
|
|
987
|
-
const precache = [
|
|
988
|
-
const version = createHash(
|
|
989
|
-
|
|
1046
|
+
walk(clientDir, "");
|
|
1047
|
+
const precache = ["/", ...files.map((file) => `/${file}`)].sort();
|
|
1048
|
+
const version = createHash("sha256")
|
|
1049
|
+
.update(precache.join("\n"))
|
|
1050
|
+
.digest("hex")
|
|
1051
|
+
.slice(0, 12);
|
|
1052
|
+
writeFileSync(join(clientDir, "sw.js"), SERVICE_WORKER(version, precache, frozen, offlineUrl, extra));
|
|
990
1053
|
log(`offline: ${precache.length} files precached as rsc-kit-${version}` +
|
|
991
|
-
(offlineUrl ? `, falling back to ${offlineUrl}` :
|
|
992
|
-
(extra ?
|
|
1054
|
+
(offlineUrl ? `, falling back to ${offlineUrl}` : "") +
|
|
1055
|
+
(extra ? ", with app/sw.js" : ""));
|
|
993
1056
|
}
|
|
994
1057
|
/**
|
|
995
1058
|
* The rsc bundle the build just wrote, whatever it decided to call it.
|
|
@@ -1003,7 +1066,7 @@ function writeServiceWorker(clientDir, frozen = [], offlineUrl = null) {
|
|
|
1003
1066
|
* prints.
|
|
1004
1067
|
*/
|
|
1005
1068
|
function resolveRscBundle(dir) {
|
|
1006
|
-
for (const name of [
|
|
1069
|
+
for (const name of ["index.js", "index.mjs"]) {
|
|
1007
1070
|
const candidate = join(dir, name);
|
|
1008
1071
|
if (existsSync(candidate))
|
|
1009
1072
|
return candidate;
|
|
@@ -1023,9 +1086,9 @@ function knownActionsOf(config, root) {
|
|
|
1023
1086
|
return [];
|
|
1024
1087
|
const out = [];
|
|
1025
1088
|
for (const meta of metaMap.values()) {
|
|
1026
|
-
if (meta.importId.includes(
|
|
1089
|
+
if (meta.importId.includes("/node_modules/"))
|
|
1027
1090
|
continue;
|
|
1028
|
-
const file = relative(root, meta.importId.split(
|
|
1091
|
+
const file = relative(root, meta.importId.split("?")[0] ?? meta.importId);
|
|
1029
1092
|
for (const name of meta.exportNames) {
|
|
1030
1093
|
out.push({ id: `${meta.referenceKey}#${name}`, name, file });
|
|
1031
1094
|
}
|
|
@@ -1033,12 +1096,15 @@ function knownActionsOf(config, root) {
|
|
|
1033
1096
|
return out;
|
|
1034
1097
|
}
|
|
1035
1098
|
async function auditActions(engine, known) {
|
|
1036
|
-
const audit = engine
|
|
1037
|
-
.auditActions;
|
|
1099
|
+
const audit = engine.auditActions;
|
|
1038
1100
|
if (!audit || known.length === 0)
|
|
1039
1101
|
return [];
|
|
1040
1102
|
const byId = new Map(known.map((k) => [k.id, k]));
|
|
1041
|
-
return (await audit(known.map((k) => k.id))).map((a) => ({
|
|
1103
|
+
return (await audit(known.map((k) => k.id))).map((a) => ({
|
|
1104
|
+
...byId.get(a.id),
|
|
1105
|
+
client: a.client,
|
|
1106
|
+
query: a.query,
|
|
1107
|
+
}));
|
|
1042
1108
|
}
|
|
1043
1109
|
/**
|
|
1044
1110
|
* Inlined into a page with no runtime when it is at most this big on the
|
|
@@ -1050,12 +1116,16 @@ async function auditActions(engine, known) {
|
|
|
1050
1116
|
*/
|
|
1051
1117
|
const INLINE_STYLESHEET_LIMIT = 10 * 1024;
|
|
1052
1118
|
export function smallStylesheetReader(assetsDir, setting) {
|
|
1053
|
-
const limit = setting === true
|
|
1119
|
+
const limit = setting === true
|
|
1120
|
+
? Infinity
|
|
1121
|
+
: setting === "auto"
|
|
1122
|
+
? INLINE_STYLESHEET_LIMIT
|
|
1123
|
+
: setting;
|
|
1054
1124
|
return (href) => {
|
|
1055
|
-
if (!href.startsWith(
|
|
1125
|
+
if (!href.startsWith("/") || href.includes(".."))
|
|
1056
1126
|
return null;
|
|
1057
1127
|
try {
|
|
1058
|
-
const css = readFileSync(join(assetsDir, href),
|
|
1128
|
+
const css = readFileSync(join(assetsDir, href), "utf-8");
|
|
1059
1129
|
return gzipSync(css).length > limit ? null : css;
|
|
1060
1130
|
}
|
|
1061
1131
|
catch {
|
|
@@ -1072,21 +1142,25 @@ async function prerenderAfterBundles(bundle, staticDir, assetsDir, knownActions
|
|
|
1072
1142
|
// visitor. The caller resolves the path from the environment now, so this is
|
|
1073
1143
|
// back to being the impossible case it reads as.
|
|
1074
1144
|
if (!bundle) {
|
|
1075
|
-
throw new Error(
|
|
1076
|
-
|
|
1077
|
-
'Build with prerender: false to render every page on demand instead.');
|
|
1145
|
+
throw new Error("[rsc-kit] The build produced no rsc bundle to prerender from.\n" +
|
|
1146
|
+
"Prerendering renders the app, so it needs the bundle the build just wrote.");
|
|
1078
1147
|
}
|
|
1079
|
-
const [{ prerender, NotPrerenderable, summary, legend, notes, clientJsSize, pathKey: pathKeyOf }, { writeTo }, { prerenderApiRoutes },] = await Promise.all([
|
|
1080
|
-
import(
|
|
1081
|
-
import(
|
|
1082
|
-
import(
|
|
1148
|
+
const [{ prerender, NotPrerenderable, summary, legend, notes, clientJsSize, pathKey: pathKeyOf, }, { writeTo }, { prerenderApiRoutes },] = await Promise.all([
|
|
1149
|
+
import("./prerender.js"),
|
|
1150
|
+
import("./files.js"),
|
|
1151
|
+
import("./apiPrerender.js"),
|
|
1083
1152
|
]);
|
|
1084
1153
|
// Cleared first: a route that changes classification between builds
|
|
1085
1154
|
// otherwise leaves its old shell on disk and the host goes on serving it.
|
|
1086
1155
|
// Nothing warns — the page loads, with content from the previous build.
|
|
1087
1156
|
rmSync(staticDir, { recursive: true, force: true });
|
|
1088
1157
|
const engine = (await import(pathToFileURL(bundle).href));
|
|
1089
|
-
const mark = {
|
|
1158
|
+
const mark = {
|
|
1159
|
+
frozen: "○",
|
|
1160
|
+
shell: "◐",
|
|
1161
|
+
blocked: "✗",
|
|
1162
|
+
error: "✗",
|
|
1163
|
+
};
|
|
1090
1164
|
let failed = 0;
|
|
1091
1165
|
// Weighed from the page the prerenderer just wrote, so the column is what
|
|
1092
1166
|
// that page actually loads rather than a total every route is charged for.
|
|
@@ -1109,20 +1183,22 @@ async function prerenderAfterBundles(bundle, staticDir, assetsDir, knownActions
|
|
|
1109
1183
|
engine,
|
|
1110
1184
|
write: writeTo(staticDir),
|
|
1111
1185
|
serviceWorker: offline,
|
|
1112
|
-
stylesheet: inlineStylesheets === false
|
|
1186
|
+
stylesheet: inlineStylesheets === false
|
|
1187
|
+
? undefined
|
|
1188
|
+
: smallStylesheetReader(assetsDir, inlineStylesheets),
|
|
1113
1189
|
onResult: (r) => {
|
|
1114
1190
|
collected.push(r);
|
|
1115
|
-
if (r.type ===
|
|
1191
|
+
if (r.type === "error" || r.type === "blocked")
|
|
1116
1192
|
failed++;
|
|
1117
1193
|
const key = pathKeyOf(r.url);
|
|
1118
1194
|
const file = [`${key}.html`, `${key}.ppr.html`]
|
|
1119
1195
|
.map((name) => join(staticDir, name))
|
|
1120
1196
|
.find((path) => existsSync(path));
|
|
1121
|
-
const bytes = file ? weigh(readFileSync(file,
|
|
1197
|
+
const bytes = file ? weigh(readFileSync(file, "utf-8")) : null;
|
|
1122
1198
|
if (bytes !== null)
|
|
1123
1199
|
sized.set(r.url, bytes);
|
|
1124
1200
|
pending.push({
|
|
1125
|
-
line: ` ${mark[r.type] ??
|
|
1201
|
+
line: ` ${mark[r.type] ?? " "} ${r.url}`,
|
|
1126
1202
|
bytes,
|
|
1127
1203
|
extra: [
|
|
1128
1204
|
...(r.reason ? [` ${r.reason}`] : []),
|
|
@@ -1147,16 +1223,19 @@ async function prerenderAfterBundles(bundle, staticDir, assetsDir, knownActions
|
|
|
1147
1223
|
const apis = await prerenderApiRoutes(engine, manifest, writeTo(staticDir));
|
|
1148
1224
|
for (const api of apis) {
|
|
1149
1225
|
pending.push({
|
|
1150
|
-
line: ` ${api.type ===
|
|
1226
|
+
line: ` ${api.type === "frozen" ? "○" : "ƒ"} ${api.url}`,
|
|
1151
1227
|
bytes: null,
|
|
1152
|
-
extra:
|
|
1228
|
+
extra: [
|
|
1229
|
+
...(api.reason ? [` ${api.reason}`] : []),
|
|
1230
|
+
...(api.warning ? [` ⚠ ${api.warning}`] : []),
|
|
1231
|
+
],
|
|
1153
1232
|
});
|
|
1154
1233
|
}
|
|
1155
1234
|
// Printed together rather than as each route lands, because a column has to
|
|
1156
1235
|
// line up and the widest url is not known until the last one is in.
|
|
1157
1236
|
const column = Math.max(...pending.map((p) => p.line.length)) + 2;
|
|
1158
1237
|
for (const row of pending) {
|
|
1159
|
-
const size = row.bytes === null ?
|
|
1238
|
+
const size = row.bytes === null ? "" : clientJsSize(row.bytes);
|
|
1160
1239
|
console.log(size ? row.line.padEnd(column) + size : row.line);
|
|
1161
1240
|
for (const line of row.extra)
|
|
1162
1241
|
console.log(line);
|
|
@@ -1172,10 +1251,12 @@ async function prerenderAfterBundles(bundle, staticDir, assetsDir, knownActions
|
|
|
1172
1251
|
const byFile = new Map();
|
|
1173
1252
|
for (const a of bare)
|
|
1174
1253
|
byFile.set(a.file, [...(byFile.get(a.file) ?? []), a.name]);
|
|
1175
|
-
console.log(`\n \u26a0 ${bare.length} ${bare.length === 1 ?
|
|
1176
|
-
[...byFile]
|
|
1177
|
-
|
|
1178
|
-
|
|
1254
|
+
console.log(`\n \u26a0 ${bare.length} ${bare.length === 1 ? "action runs" : "actions run"} no middleware: ` +
|
|
1255
|
+
[...byFile]
|
|
1256
|
+
.map(([file, names]) => `${names.join(", ")} (${file})`)
|
|
1257
|
+
.join("; "));
|
|
1258
|
+
console.log(" Nothing checks who calls them. Fine for a public one; otherwise build it\n" +
|
|
1259
|
+
" from an action client, so the check cannot be forgotten.");
|
|
1179
1260
|
}
|
|
1180
1261
|
// Written from the rows that were just printed rather than recomputed: the
|
|
1181
1262
|
// report and the terminal must not be able to disagree about what happened.
|
|
@@ -1187,22 +1268,29 @@ async function prerenderAfterBundles(bundle, staticDir, assetsDir, knownActions
|
|
|
1187
1268
|
warning: r.warning ?? null,
|
|
1188
1269
|
note: r.note ?? null,
|
|
1189
1270
|
clientJs: sized.get(r.url) ?? null,
|
|
1190
|
-
})), apis.map((a) => ({
|
|
1271
|
+
})), apis.map((a) => ({
|
|
1272
|
+
url: a.url,
|
|
1273
|
+
name: a.name,
|
|
1274
|
+
type: a.type,
|
|
1275
|
+
reason: a.reason,
|
|
1276
|
+
warning: a.warning ?? null,
|
|
1277
|
+
})), audited));
|
|
1191
1278
|
const note = notes(results);
|
|
1192
1279
|
const counted = [...results, ...apis];
|
|
1193
1280
|
console.log(`
|
|
1194
1281
|
${legend(counted)}
|
|
1195
1282
|
|
|
1196
|
-
${summary(counted)}${note ? `\n\n${note}` :
|
|
1283
|
+
${summary(counted)}${note ? `\n\n${note}` : ""}`);
|
|
1197
1284
|
// The table and the report are on disk. Now the refusal, in its own words.
|
|
1198
1285
|
if (refusal)
|
|
1199
1286
|
throw new Error(refusal.message);
|
|
1200
1287
|
if (failed > 0) {
|
|
1201
|
-
throw new Error(`[rsc-kit] ${failed} route${failed === 1 ?
|
|
1202
|
-
|
|
1203
|
-
|
|
1288
|
+
throw new Error(`[rsc-kit] ${failed} route${failed === 1 ? "" : "s"} failed to render.\n` +
|
|
1289
|
+
"Prerendering runs your app: whatever those pages need at render time has to be\n" +
|
|
1290
|
+
"reachable from the build. Give it that, or mark the read with `await connection()`\n" +
|
|
1291
|
+
"so the page renders per request and the rest of it is still stored.");
|
|
1204
1292
|
}
|
|
1205
|
-
if (output ===
|
|
1293
|
+
if (output === "export")
|
|
1206
1294
|
await exportAfterPrerender(results, staticDir, assetsDir);
|
|
1207
1295
|
// The urls whose answer cannot change until the next build. The service
|
|
1208
1296
|
// worker serves these from its cache first rather than asking the network
|
|
@@ -1221,8 +1309,8 @@ ${legend(counted)}
|
|
|
1221
1309
|
const guarded = new Set(manifest.routes.filter((r) => r.middleware?.length).map((r) => r.component));
|
|
1222
1310
|
return {
|
|
1223
1311
|
frozen: results
|
|
1224
|
-
.filter((r) => r.type ===
|
|
1225
|
-
.filter((r) => existsSync(join(staticDir, pathKeyOf(r.url) +
|
|
1312
|
+
.filter((r) => r.type === "frozen" && !guarded.has(r.component))
|
|
1313
|
+
.filter((r) => existsSync(join(staticDir, pathKeyOf(r.url) + ".html")))
|
|
1226
1314
|
.map((r) => r.url),
|
|
1227
1315
|
results,
|
|
1228
1316
|
};
|
|
@@ -1249,7 +1337,9 @@ function weighClientJs(assetsDir) {
|
|
|
1249
1337
|
if (cached !== undefined)
|
|
1250
1338
|
return cached;
|
|
1251
1339
|
const file = join(assetsDir, asset);
|
|
1252
|
-
const bytes = existsSync(file)
|
|
1340
|
+
const bytes = existsSync(file)
|
|
1341
|
+
? gzipSync(readFileSync(file)).byteLength
|
|
1342
|
+
: 0;
|
|
1253
1343
|
weighed.set(asset, bytes);
|
|
1254
1344
|
return bytes;
|
|
1255
1345
|
};
|
|
@@ -1259,10 +1349,10 @@ function weighClientJs(assetsDir) {
|
|
|
1259
1349
|
const named = new Set();
|
|
1260
1350
|
// Split rather than matched: /assets/name.js is the only shape written, and
|
|
1261
1351
|
// a regex over a whole document is the slower half of this function.
|
|
1262
|
-
for (const piece of html.split(
|
|
1352
|
+
for (const piece of html.split("/assets/").slice(1)) {
|
|
1263
1353
|
const name = piece.split(/["'\s)]/)[0];
|
|
1264
|
-
if (name.endsWith(
|
|
1265
|
-
named.add(
|
|
1354
|
+
if (name.endsWith(".js"))
|
|
1355
|
+
named.add("assets/" + name);
|
|
1266
1356
|
}
|
|
1267
1357
|
let total = 0;
|
|
1268
1358
|
for (const asset of named)
|
|
@@ -1286,22 +1376,19 @@ function weighClientJs(assetsDir) {
|
|
|
1286
1376
|
* page that loads and then stays empty forever.
|
|
1287
1377
|
*/
|
|
1288
1378
|
async function exportAfterPrerender(results, staticDir, assetsDir) {
|
|
1289
|
-
const [{ exportSite, NotExportable }, { writeTo, prerenderedFrom, copyAssets }] = await Promise.all([
|
|
1290
|
-
import('./export.js'),
|
|
1291
|
-
import('./files.js'),
|
|
1292
|
-
]);
|
|
1379
|
+
const [{ exportSite, NotExportable }, { writeTo, prerenderedFrom, copyAssets },] = await Promise.all([import("./export.js"), import("./files.js")]);
|
|
1293
1380
|
try {
|
|
1294
1381
|
const { pages, refused } = await exportSite({
|
|
1295
1382
|
results,
|
|
1296
1383
|
read: prerenderedFrom(staticDir),
|
|
1297
1384
|
write: writeTo(exportPath),
|
|
1298
1385
|
manifest: routeManifest(),
|
|
1299
|
-
assets: copyAssets(join(assetsDir,
|
|
1300
|
-
force: process.env.RSC_EXPORT_FORCE ===
|
|
1386
|
+
assets: copyAssets(join(assetsDir, "assets"), exportPath, "/assets/"),
|
|
1387
|
+
force: process.env.RSC_EXPORT_FORCE === "1",
|
|
1301
1388
|
});
|
|
1302
|
-
console.log(`\n Exported ${pages} page${pages === 1 ?
|
|
1389
|
+
console.log(`\n Exported ${pages} page${pages === 1 ? "" : "s"} to ${relative(projectRoot, exportPath)}`);
|
|
1303
1390
|
if (refused.length > 0) {
|
|
1304
|
-
console.log(` Left out ${refused.length}: ${refused.map((r) => r.url).join(
|
|
1391
|
+
console.log(` Left out ${refused.length}: ${refused.map((r) => r.url).join(", ")}`);
|
|
1305
1392
|
}
|
|
1306
1393
|
}
|
|
1307
1394
|
catch (error) {
|
|
@@ -1313,15 +1400,15 @@ async function exportAfterPrerender(results, staticDir, assetsDir) {
|
|
|
1313
1400
|
/** `/posts/[slug]` — the pattern, in the shape the app writes its links in. */
|
|
1314
1401
|
function patternOf(segments) {
|
|
1315
1402
|
if (segments.length === 0)
|
|
1316
|
-
return
|
|
1317
|
-
return (
|
|
1403
|
+
return "/";
|
|
1404
|
+
return ("/" +
|
|
1318
1405
|
segments
|
|
1319
|
-
.map((segment) => segment.type ===
|
|
1406
|
+
.map((segment) => segment.type === "static"
|
|
1320
1407
|
? segment.value
|
|
1321
|
-
: segment.type ===
|
|
1408
|
+
: segment.type === "catchAll"
|
|
1322
1409
|
? `[...${segment.value}]`
|
|
1323
1410
|
: `[${segment.value}]`)
|
|
1324
|
-
.join(
|
|
1411
|
+
.join("/"));
|
|
1325
1412
|
}
|
|
1326
1413
|
/**
|
|
1327
1414
|
* The app's routes as a union, for `@rsc-kit/core/routes` to derive from.
|
|
@@ -1335,8 +1422,12 @@ function patternOf(segments) {
|
|
|
1335
1422
|
* the union twice and imply you could link to a modal.
|
|
1336
1423
|
*/
|
|
1337
1424
|
function renderRouteTypes(manifest) {
|
|
1338
|
-
const patterns = [
|
|
1339
|
-
|
|
1425
|
+
const patterns = [
|
|
1426
|
+
...new Set(manifest.routes.map((route) => patternOf(route.segments))),
|
|
1427
|
+
].sort();
|
|
1428
|
+
const apis = [
|
|
1429
|
+
...new Set((manifest.apis ?? []).map((route) => patternOf(route.segments))),
|
|
1430
|
+
].sort();
|
|
1340
1431
|
// Each pattern to the page module that answers it, as a type-only import
|
|
1341
1432
|
// from the generated file's own directory. SearchExportOf reads the page's
|
|
1342
1433
|
// `searchParams` export, or undefined when there is none, so nothing here
|
|
@@ -1347,82 +1438,114 @@ function renderRouteTypes(manifest) {
|
|
|
1347
1438
|
const pattern = patternOf(route.segments);
|
|
1348
1439
|
if (search.has(pattern))
|
|
1349
1440
|
continue;
|
|
1350
|
-
const target = relative(typesDir, join(sourceDir, route.component)).replace(/\\/g,
|
|
1351
|
-
search.set(pattern, target.startsWith(
|
|
1441
|
+
const target = relative(typesDir, join(sourceDir, route.component)).replace(/\\/g, "/");
|
|
1442
|
+
search.set(pattern, target.startsWith(".") ? target : "./" + target);
|
|
1352
1443
|
}
|
|
1353
1444
|
return [
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1445
|
+
"// @generated — do not edit. Written by the RSC build from the route tree.",
|
|
1446
|
+
"//",
|
|
1447
|
+
"// Turns Link, navigate() and route() into typed apis: an href that no route",
|
|
1448
|
+
"// answers stops compiling. Delete this file and they fall back to `string`,",
|
|
1449
|
+
"// which is what a project that has not built yet gets.",
|
|
1450
|
+
"",
|
|
1451
|
+
"// `export {}` is load-bearing: in a file with no import or export,",
|
|
1452
|
+
"// `declare module` *replaces* the real module rather than augmenting it,",
|
|
1453
|
+
"// and Href and route() vanish from it with no error to explain why.",
|
|
1454
|
+
"export {}",
|
|
1455
|
+
"",
|
|
1365
1456
|
"declare module '@rsc-kit/core/routes' {",
|
|
1366
|
-
|
|
1457
|
+
" interface Register {",
|
|
1367
1458
|
patterns.length > 0
|
|
1368
|
-
?
|
|
1369
|
-
|
|
1459
|
+
? " routes:\n" +
|
|
1460
|
+
patterns.map((p) => " | " + JSON.stringify(p)).join("\n")
|
|
1461
|
+
: " // No routes found under the source directory.\n routes: never",
|
|
1370
1462
|
// The searchParams schema each page exports, read off the module's type.
|
|
1371
1463
|
// This is what types Link's `search` prop and href() per route.
|
|
1372
|
-
|
|
1373
|
-
...[...search]
|
|
1374
|
-
|
|
1375
|
-
|
|
1464
|
+
" search: {",
|
|
1465
|
+
...[...search]
|
|
1466
|
+
.sort()
|
|
1467
|
+
.map(([pattern, target]) => " " +
|
|
1468
|
+
JSON.stringify(pattern) +
|
|
1469
|
+
": SearchExportOf<typeof import(" +
|
|
1470
|
+
JSON.stringify(target) +
|
|
1471
|
+
")>"),
|
|
1472
|
+
" }",
|
|
1473
|
+
" }",
|
|
1376
1474
|
// Api routes are a separate union, so Link refuses an api url and apiUrl()
|
|
1377
1475
|
// refuses a page. Linking to an api route navigates the browser away to a
|
|
1378
1476
|
// json document, which is the mistake worth catching.
|
|
1379
|
-
|
|
1477
|
+
" interface RegisterApi {",
|
|
1380
1478
|
apis.length > 0
|
|
1381
|
-
?
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1479
|
+
? " apis:\n" +
|
|
1480
|
+
apis.map((p) => " | " + JSON.stringify(p)).join("\n")
|
|
1481
|
+
: " // No route.ts files found under the source directory.\n apis: never",
|
|
1482
|
+
" }",
|
|
1483
|
+
"}",
|
|
1484
|
+
"",
|
|
1485
|
+
].join("\n");
|
|
1387
1486
|
}
|
|
1388
1487
|
/** The "use server" module exposing each host function as a plain async call. */
|
|
1389
1488
|
function renderHostActions() {
|
|
1390
1489
|
const lines = [
|
|
1391
1490
|
'"use server";',
|
|
1392
|
-
|
|
1393
|
-
|
|
1491
|
+
"// @generated — do not edit. Written by the RSC build from the host action map.",
|
|
1492
|
+
"",
|
|
1394
1493
|
];
|
|
1395
1494
|
for (const [name, target] of Object.entries(hostActions)) {
|
|
1396
|
-
lines.push(
|
|
1397
|
-
lines.push(
|
|
1398
|
-
|
|
1399
|
-
|
|
1495
|
+
lines.push("export async function " + name + "(...args: unknown[]) {");
|
|
1496
|
+
lines.push(" return await (globalThis as any)." +
|
|
1497
|
+
hostGlobal +
|
|
1498
|
+
"(" +
|
|
1499
|
+
JSON.stringify(target) +
|
|
1500
|
+
", ...args);");
|
|
1501
|
+
lines.push("}");
|
|
1502
|
+
lines.push("");
|
|
1400
1503
|
}
|
|
1401
|
-
return lines.join(
|
|
1504
|
+
return lines.join("\n");
|
|
1402
1505
|
}
|
|
1403
1506
|
/** Ambient declaration for the host global, written beside the app's source. */
|
|
1404
1507
|
function renderHostGlobalTypes() {
|
|
1405
1508
|
return [
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1509
|
+
"// @generated — do not edit.",
|
|
1510
|
+
"//",
|
|
1511
|
+
"// " +
|
|
1512
|
+
hostGlobal +
|
|
1513
|
+
"() is installed on globalThis by the RSC worker, so it has no",
|
|
1514
|
+
"// import to resolve. This declares it for the typechecker; run",
|
|
1515
|
+
"// `tsc --noEmit` to catch calls to a host global that no longer exists.",
|
|
1516
|
+
"//",
|
|
1517
|
+
"// Deliberately not a module — no import/export — so the declaration is",
|
|
1518
|
+
"// global to the project without every file having to reference it.",
|
|
1519
|
+
"",
|
|
1520
|
+
"declare function " +
|
|
1521
|
+
hostGlobal +
|
|
1522
|
+
"<T = unknown>(name: string, ...args: unknown[]): Promise<T>;",
|
|
1523
|
+
"",
|
|
1524
|
+
].join("\n");
|
|
1418
1525
|
}
|
|
1419
1526
|
// ── Discovery ────────────────────────────────────────────────────────────────
|
|
1420
|
-
const ROUTE_FILES = [
|
|
1527
|
+
const ROUTE_FILES = [
|
|
1528
|
+
"page",
|
|
1529
|
+
"layout",
|
|
1530
|
+
"loading",
|
|
1531
|
+
"error",
|
|
1532
|
+
"not-found",
|
|
1533
|
+
"default",
|
|
1534
|
+
"middleware",
|
|
1535
|
+
];
|
|
1421
1536
|
/** The methods a route.ts may export. HEAD and OPTIONS are answered for you. */
|
|
1422
|
-
const API_METHODS = [
|
|
1537
|
+
const API_METHODS = [
|
|
1538
|
+
"GET",
|
|
1539
|
+
"POST",
|
|
1540
|
+
"PUT",
|
|
1541
|
+
"PATCH",
|
|
1542
|
+
"DELETE",
|
|
1543
|
+
"HEAD",
|
|
1544
|
+
"OPTIONS",
|
|
1545
|
+
];
|
|
1423
1546
|
/** `orders.section.tsx` — a region of a page that can be refreshed by name. */
|
|
1424
1547
|
const SECTION_FILE = /\.section\.(tsx|jsx|ts|js)$/;
|
|
1425
|
-
const EXTS = [
|
|
1548
|
+
const EXTS = ["tsx", "jsx", "ts", "js"];
|
|
1426
1549
|
function findRouteFile(dir, base) {
|
|
1427
1550
|
for (const ext of EXTS) {
|
|
1428
1551
|
const p = join(dir, `${base}.${ext}`);
|
|
@@ -1432,11 +1555,11 @@ function findRouteFile(dir, base) {
|
|
|
1432
1555
|
return null;
|
|
1433
1556
|
}
|
|
1434
1557
|
function componentName(absPath) {
|
|
1435
|
-
const rel = relative(sourceDir, absPath).replace(/\\/g,
|
|
1436
|
-
return rel.replace(/\.(tsx|jsx|ts|js)$/,
|
|
1558
|
+
const rel = relative(sourceDir, absPath).replace(/\\/g, "/");
|
|
1559
|
+
return rel.replace(/\.(tsx|jsx|ts|js)$/, "");
|
|
1437
1560
|
}
|
|
1438
1561
|
function toAlias(name) {
|
|
1439
|
-
return
|
|
1562
|
+
return "_c_" + name.replace(/[^a-zA-Z0-9]/g, "_");
|
|
1440
1563
|
}
|
|
1441
1564
|
const components = new Map();
|
|
1442
1565
|
/**
|
|
@@ -1467,8 +1590,8 @@ function register(absPath) {
|
|
|
1467
1590
|
*/
|
|
1468
1591
|
function registerApiRoute(absPath) {
|
|
1469
1592
|
const name = componentName(absPath);
|
|
1470
|
-
const source = readFileSync(absPath,
|
|
1471
|
-
const methods = API_METHODS.filter((method) => new RegExp(`^\\s*export\\s+(?:async\\s+function|function|const|let|var)\\s+${method}\\b`,
|
|
1593
|
+
const source = readFileSync(absPath, "utf-8");
|
|
1594
|
+
const methods = API_METHODS.filter((method) => new RegExp(`^\\s*export\\s+(?:async\\s+function|function|const|let|var)\\s+${method}\\b`, "m").test(source));
|
|
1472
1595
|
if (methods.length === 0) {
|
|
1473
1596
|
// `route.ts` is also where a host declares the guards for everything below
|
|
1474
1597
|
// it — the file predates api routes and is still read that way. One that
|
|
@@ -1478,7 +1601,7 @@ function registerApiRoute(absPath) {
|
|
|
1478
1601
|
return;
|
|
1479
1602
|
throw new Error(`[rsc-kit] ${relative(projectRoot, absPath)} exports no request methods.\n` +
|
|
1480
1603
|
` Export one named for the method it answers — export function GET(request: Request) — ` +
|
|
1481
|
-
`or delete the file. One of: ${API_METHODS.join(
|
|
1604
|
+
`or delete the file. One of: ${API_METHODS.join(", ")}.`);
|
|
1482
1605
|
}
|
|
1483
1606
|
apiRoutes.set(name, { name, absPath, methods });
|
|
1484
1607
|
}
|
|
@@ -1491,7 +1614,7 @@ function discover(dir) {
|
|
|
1491
1614
|
// route.ts — an api endpoint, colocated with the pages it sits among. Read
|
|
1492
1615
|
// for its method exports here rather than at request time, so a route that
|
|
1493
1616
|
// exports nothing callable is a build error instead of a 404 nobody explains.
|
|
1494
|
-
const api = findRouteFile(dir,
|
|
1617
|
+
const api = findRouteFile(dir, "route");
|
|
1495
1618
|
if (api)
|
|
1496
1619
|
registerApiRoute(api);
|
|
1497
1620
|
// Named regions. Registered like any other component so the generated entry
|
|
@@ -1517,7 +1640,7 @@ function discover(dir) {
|
|
|
1517
1640
|
* for almost every route in an app.
|
|
1518
1641
|
*/
|
|
1519
1642
|
function metadataExports(absPath) {
|
|
1520
|
-
const src = readFileSync(absPath,
|
|
1643
|
+
const src = readFileSync(absPath, "utf-8");
|
|
1521
1644
|
return {
|
|
1522
1645
|
static: /export\s+const\s+metadata\b/.test(src),
|
|
1523
1646
|
generate: /export\s+(async\s+)?function\s+generateMetadata\b/.test(src),
|
|
@@ -1533,7 +1656,7 @@ function metadataExports(absPath) {
|
|
|
1533
1656
|
* inferred because there is nothing to infer it from.
|
|
1534
1657
|
*/
|
|
1535
1658
|
function hasStaticParams(absPath) {
|
|
1536
|
-
const src = readFileSync(absPath,
|
|
1659
|
+
const src = readFileSync(absPath, "utf-8");
|
|
1537
1660
|
return /export\s+((async\s+)?function\s+generateStaticParams|const\s+generateStaticParams)/.test(src);
|
|
1538
1661
|
}
|
|
1539
1662
|
/**
|
|
@@ -1549,7 +1672,7 @@ function hasStaticParams(absPath) {
|
|
|
1549
1672
|
* import for something that can never validate.
|
|
1550
1673
|
*/
|
|
1551
1674
|
function urlSchemaExports(absPath) {
|
|
1552
|
-
const src = readFileSync(absPath,
|
|
1675
|
+
const src = readFileSync(absPath, "utf-8");
|
|
1553
1676
|
return {
|
|
1554
1677
|
params: /export\s+const\s+params\s*[=:]/.test(src),
|
|
1555
1678
|
searchParams: /export\s+const\s+searchParams\s*[=:]/.test(src),
|
|
@@ -1671,7 +1794,7 @@ const FALLBACK_BODY = ` const answer = await devHandler(request)
|
|
|
1671
1794
|
* the failure is silent: the server finds no directory, decides nothing was
|
|
1672
1795
|
* frozen, and renders every page live exactly as it did before.
|
|
1673
1796
|
*/
|
|
1674
|
-
const NITRO_STATIC_DIR =
|
|
1797
|
+
const NITRO_STATIC_DIR = "rsc-static";
|
|
1675
1798
|
/**
|
|
1676
1799
|
* Serve what the build froze.
|
|
1677
1800
|
*
|
|
@@ -1710,15 +1833,15 @@ function installHostCallsOnce(): void {
|
|
|
1710
1833
|
}
|
|
1711
1834
|
|
|
1712
1835
|
`;
|
|
1713
|
-
function generateEntryRsc(fallbackOrigin =
|
|
1836
|
+
function generateEntryRsc(fallbackOrigin = "") {
|
|
1714
1837
|
// The 404 page, if the app has one, and the layouts it renders inside.
|
|
1715
1838
|
// Computed here rather than looked up at runtime: not-found is not a route,
|
|
1716
1839
|
// so the manifest has no entry to read its chain from.
|
|
1717
|
-
const notFoundComponent = [...components.keys()].find((name) => name.endsWith(
|
|
1840
|
+
const notFoundComponent = [...components.keys()].find((name) => name.endsWith("/not-found"));
|
|
1718
1841
|
const notFoundLayouts = notFoundComponent
|
|
1719
1842
|
? [...components.keys()]
|
|
1720
|
-
.filter((name) => name.endsWith(
|
|
1721
|
-
notFoundComponent.startsWith(name.slice(0, -
|
|
1843
|
+
.filter((name) => name.endsWith("/layout") &&
|
|
1844
|
+
notFoundComponent.startsWith(name.slice(0, -"layout".length)))
|
|
1722
1845
|
.sort((a, b) => a.length - b.length)
|
|
1723
1846
|
: [];
|
|
1724
1847
|
const imports = [];
|
|
@@ -1743,7 +1866,7 @@ function generateEntryRsc(fallbackOrigin = '') {
|
|
|
1743
1866
|
meta.static ? `static: ${c.alias}_meta.metadata` : null,
|
|
1744
1867
|
meta.generate ? `generate: ${c.alias}_meta.generateMetadata` : null,
|
|
1745
1868
|
].filter(Boolean);
|
|
1746
|
-
metaEntries.push(` ${JSON.stringify(c.name)}: { ${fields.join(
|
|
1869
|
+
metaEntries.push(` ${JSON.stringify(c.name)}: { ${fields.join(", ")} },`);
|
|
1747
1870
|
}
|
|
1748
1871
|
if (hasStaticParams(c.absPath)) {
|
|
1749
1872
|
// The namespace import may already be in place for metadata; a second
|
|
@@ -1756,9 +1879,11 @@ function generateEntryRsc(fallbackOrigin = '') {
|
|
|
1756
1879
|
imports.push(`import * as ${c.alias}_schema from ${JSON.stringify(c.absPath)}`);
|
|
1757
1880
|
const fields = [
|
|
1758
1881
|
urlSchemas.params ? `params: ${c.alias}_schema.params` : null,
|
|
1759
|
-
urlSchemas.searchParams
|
|
1882
|
+
urlSchemas.searchParams
|
|
1883
|
+
? `searchParams: ${c.alias}_schema.searchParams`
|
|
1884
|
+
: null,
|
|
1760
1885
|
].filter(Boolean);
|
|
1761
|
-
schemaEntries.push(` ${JSON.stringify(c.name)}: { ${fields.join(
|
|
1886
|
+
schemaEntries.push(` ${JSON.stringify(c.name)}: { ${fields.join(", ")} },`);
|
|
1762
1887
|
}
|
|
1763
1888
|
}
|
|
1764
1889
|
// The engine's own modules are named without an extension: this plugin runs
|
|
@@ -1777,21 +1902,21 @@ import { notFoundDigest, isNotFoundSignal } from ${JSON.stringify(join(packageDi
|
|
|
1777
1902
|
import { noteRequestRead } from ${JSON.stringify(join(packageDir, "request"))}
|
|
1778
1903
|
import { redirectDigest } from ${JSON.stringify(join(packageDir, "redirectDigest"))}
|
|
1779
1904
|
import { createRscHandler } from ${JSON.stringify(join(packageDir, "host"))}
|
|
1780
|
-
import { httpHostCalls } from ${JSON.stringify(join(packageDir,
|
|
1781
|
-
import { prerenderedBeside } from ${JSON.stringify(join(packageDir,
|
|
1905
|
+
import { httpHostCalls } from ${JSON.stringify(join(packageDir, "hostCalls"))}
|
|
1906
|
+
import { prerenderedBeside } from ${JSON.stringify(join(packageDir, "files"))}
|
|
1782
1907
|
import { renderToReadableStream, decodeReply, loadServerAction } from '@vitejs/plugin-rsc/rsc'
|
|
1783
|
-
import { isQuery, queryCacheControl, isQueryValidationError } from ${JSON.stringify(join(packageDir,
|
|
1784
|
-
import { isActionValidationError, isClientBuilt } from ${JSON.stringify(join(packageDir,
|
|
1908
|
+
import { isQuery, queryCacheControl, isQueryValidationError } from ${JSON.stringify(join(packageDir, "query"))}
|
|
1909
|
+
import { isActionValidationError, isClientBuilt } from ${JSON.stringify(join(packageDir, "action"))}
|
|
1785
1910
|
import { Suspense, createElement, Fragment } from 'react'
|
|
1786
1911
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
1787
|
-
${imports.join(
|
|
1912
|
+
${imports.join("\n")}
|
|
1788
1913
|
|
|
1789
1914
|
type HostFn = (name: string, ...args: unknown[]) => Promise<unknown>
|
|
1790
1915
|
type LayoutEntry = { component: string; props?: Record<string, unknown> }
|
|
1791
1916
|
type SlotOverride = { component: string; props?: Record<string, unknown> }
|
|
1792
1917
|
|
|
1793
1918
|
const components: Record<string, any> = {
|
|
1794
|
-
${mapEntries.join(
|
|
1919
|
+
${mapEntries.join("\n")}
|
|
1795
1920
|
}
|
|
1796
1921
|
|
|
1797
1922
|
/**
|
|
@@ -1811,16 +1936,21 @@ ${mapEntries.join('\n')}
|
|
|
1811
1936
|
const APP_HEAD: { tag: any; props: Record<string, string> }[] = ${JSON.stringify(headTags(foundAssets))}
|
|
1812
1937
|
|
|
1813
1938
|
const WEB_MANIFEST: { href: string; themeColor?: string } | null = ${JSON.stringify(webManifestOptions
|
|
1814
|
-
? {
|
|
1939
|
+
? {
|
|
1940
|
+
href: MANIFEST_PATH,
|
|
1941
|
+
...(webManifestOptions.themeColor
|
|
1942
|
+
? { themeColor: webManifestOptions.themeColor }
|
|
1943
|
+
: {}),
|
|
1944
|
+
}
|
|
1815
1945
|
: null)}
|
|
1816
1946
|
|
|
1817
1947
|
const urlSchemas: Record<string, { params?: any; searchParams?: any }> = {
|
|
1818
|
-
${schemaEntries.join(
|
|
1948
|
+
${schemaEntries.join("\n")}
|
|
1819
1949
|
}
|
|
1820
1950
|
|
|
1821
1951
|
/** route.ts modules, by the name the manifest matched. */
|
|
1822
1952
|
const apiRoutes: Record<string, any> = {
|
|
1823
|
-
${apiEntries.join(
|
|
1953
|
+
${apiEntries.join("\n")}
|
|
1824
1954
|
}
|
|
1825
1955
|
|
|
1826
1956
|
/**
|
|
@@ -1956,11 +2086,11 @@ export async function handleApiRoute(
|
|
|
1956
2086
|
}
|
|
1957
2087
|
|
|
1958
2088
|
const metadataMap: Record<string, { static?: any; generate?: (p: any) => any }> = {
|
|
1959
|
-
${metaEntries.join(
|
|
2089
|
+
${metaEntries.join("\n")}
|
|
1960
2090
|
}
|
|
1961
2091
|
|
|
1962
2092
|
const staticParamsMap: Record<string, () => any> = {
|
|
1963
|
-
${paramEntries.join(
|
|
2093
|
+
${paramEntries.join("\n")}
|
|
1964
2094
|
}
|
|
1965
2095
|
|
|
1966
2096
|
/**
|
|
@@ -3490,7 +3620,7 @@ export async function handleRscPprShell(
|
|
|
3490
3620
|
* server rendering live pages it had already frozen. The gate is the mode, not
|
|
3491
3621
|
* the file.
|
|
3492
3622
|
*/
|
|
3493
|
-
${fallbackOrigin ? FALLBACK_CONSTS.replace(
|
|
3623
|
+
${fallbackOrigin ? FALLBACK_CONSTS.replace("__ORIGIN__", JSON.stringify(fallbackOrigin)) : ""}${NITRO_HOST_CALLS}
|
|
3494
3624
|
let devHandler: ((request: Request) => Promise<Response | null>) | null = null
|
|
3495
3625
|
|
|
3496
3626
|
export default async function handler(request: Request): Promise<Response> {
|
|
@@ -3514,7 +3644,8 @@ export default async function handler(request: Request): Promise<Response> {
|
|
|
3514
3644
|
resolveMetadata,
|
|
3515
3645
|
runRouteMiddleware,
|
|
3516
3646
|
} as never,
|
|
3517
|
-
${NITRO_HANDLER_OPTIONS}${NITRO_PRERENDERED}
|
|
3647
|
+
${NITRO_HANDLER_OPTIONS}${NITRO_PRERENDERED} maxActionBody: ${maxActionBody === undefined ? "undefined" : String(maxActionBody)},
|
|
3648
|
+
})
|
|
3518
3649
|
|
|
3519
3650
|
${fallbackOrigin ? FALLBACK_BODY : " return (await devHandler(request)) ?? (await notFound())\n"}}
|
|
3520
3651
|
|
|
@@ -3528,7 +3659,8 @@ ${fallbackOrigin ? FALLBACK_BODY : " return (await devHandler(request)) ?? (awa
|
|
|
3528
3659
|
* Without a not-found.tsx this is the string it always was.
|
|
3529
3660
|
*/
|
|
3530
3661
|
async function notFound(): Promise<Response> {
|
|
3531
|
-
${notFoundComponent
|
|
3662
|
+
${notFoundComponent
|
|
3663
|
+
? `
|
|
3532
3664
|
try {
|
|
3533
3665
|
const { htmlStream } = await handleRscHtmlStream(
|
|
3534
3666
|
${JSON.stringify(notFoundComponent)},
|
|
@@ -3549,13 +3681,14 @@ async function notFound(): Promise<Response> {
|
|
|
3549
3681
|
// A 404 page that throws is still a 404. Falling back rather than
|
|
3550
3682
|
// answering 500 keeps the status honest about what happened.
|
|
3551
3683
|
}
|
|
3552
|
-
`
|
|
3684
|
+
`
|
|
3685
|
+
: ""}
|
|
3553
3686
|
return new Response('Not found', { status: 404 })
|
|
3554
3687
|
}
|
|
3555
3688
|
`;
|
|
3556
3689
|
}
|
|
3557
3690
|
function generateEntrySsr() {
|
|
3558
|
-
const devUrls = join(packageDir,
|
|
3691
|
+
const devUrls = join(packageDir, "devUrls");
|
|
3559
3692
|
return `// GENERATED by rscKit() — do not edit.
|
|
3560
3693
|
import { createFromReadableStream } from '@vitejs/plugin-rsc/ssr'
|
|
3561
3694
|
import { renderToReadableStream, resume } from 'react-dom/server.edge'
|
|
@@ -3588,7 +3721,19 @@ export async function handleSsr(
|
|
|
3588
3721
|
const html = await renderToReadableStream(root as any, {
|
|
3589
3722
|
bootstrapScriptContent,
|
|
3590
3723
|
nonce,
|
|
3591
|
-
|
|
3724
|
+
// The query-string fallback is the designed path for a stored page, and
|
|
3725
|
+
// its digest is what lets the client tell it from a fault on hydration;
|
|
3726
|
+
// returned here so React writes it into the document.
|
|
3727
|
+
onError: onError ?? ((error: unknown) => {
|
|
3728
|
+
const digest = (error as { digest?: string } | null)?.digest
|
|
3729
|
+
if (digest === 'rsc-kit:search-params-fallback') {
|
|
3730
|
+
// One line, not a stack: the line is the dev server saying which
|
|
3731
|
+
// boundary the build will want.
|
|
3732
|
+
console.error('[rsc-kit] ' + String((error as Error).message))
|
|
3733
|
+
return digest
|
|
3734
|
+
}
|
|
3735
|
+
console.error('[rsc-kit:ssr]', error)
|
|
3736
|
+
}),
|
|
3592
3737
|
})
|
|
3593
3738
|
|
|
3594
3739
|
return DEV_ORIGIN ? rewriteViteDevUrlStream(html, DEV_ORIGIN) : html
|
|
@@ -3682,7 +3827,7 @@ export default {
|
|
|
3682
3827
|
}
|
|
3683
3828
|
`;
|
|
3684
3829
|
function generateEntryBrowser() {
|
|
3685
|
-
const clientBootstrap = join(packageDir,
|
|
3830
|
+
const clientBootstrap = join(packageDir, "js/createViteRscApp");
|
|
3686
3831
|
// Only for an exported build, and only what the client needs to work out how
|
|
3687
3832
|
// much of a page to ask for: a file server sends no headers, so without this
|
|
3688
3833
|
// every navigation takes the whole document and replaces the root. Inlined
|
|
@@ -3690,9 +3835,12 @@ function generateEntryBrowser() {
|
|
|
3690
3835
|
// a server answers this, and shipping a route table to every browser for
|
|
3691
3836
|
// nothing is a page-weight cost with no benefit.
|
|
3692
3837
|
const routesForClient = staticPayloads
|
|
3693
|
-
? routeManifest().routes.map((route) => ({
|
|
3838
|
+
? routeManifest().routes.map((route) => ({
|
|
3839
|
+
segments: route.segments,
|
|
3840
|
+
layouts: route.layouts,
|
|
3841
|
+
}))
|
|
3694
3842
|
: null;
|
|
3695
|
-
const refreshModule = join(packageDir,
|
|
3843
|
+
const refreshModule = join(packageDir, "js/navigate");
|
|
3696
3844
|
return `// GENERATED by rscKit() — do not edit.
|
|
3697
3845
|
import { createViteRscApp } from ${JSON.stringify(clientBootstrap)}
|
|
3698
3846
|
import { refresh } from ${JSON.stringify(refreshModule)}
|
|
@@ -3738,7 +3886,7 @@ if ('serviceWorker' in navigator && import.meta.env.PROD) {
|
|
|
3738
3886
|
})
|
|
3739
3887
|
}
|
|
3740
3888
|
`
|
|
3741
|
-
:
|
|
3889
|
+
: ""}`;
|
|
3742
3890
|
}
|
|
3743
3891
|
/**
|
|
3744
3892
|
* Intercepted URL patterns, published by the host before the build.
|
|
@@ -3760,10 +3908,10 @@ if ('serviceWorker' in navigator && import.meta.env.PROD) {
|
|
|
3760
3908
|
function interceptManifest() {
|
|
3761
3909
|
return routeManifest().intercepts.map((entry) => ({
|
|
3762
3910
|
// The client writes [id] where a Laravel route writes {id}.
|
|
3763
|
-
urlPattern:
|
|
3911
|
+
urlPattern: "/" +
|
|
3764
3912
|
entry.segments
|
|
3765
|
-
.map((seg) =>
|
|
3766
|
-
.join(
|
|
3913
|
+
.map((seg) => seg.type === "static" ? seg.value : "[" + seg.value + "]")
|
|
3914
|
+
.join("/"),
|
|
3767
3915
|
slot: entry.slot,
|
|
3768
3916
|
}));
|
|
3769
3917
|
}
|
|
@@ -3784,9 +3932,9 @@ function defaultExportBody(source) {
|
|
|
3784
3932
|
const start = match.index + match[0].length - 1;
|
|
3785
3933
|
for (let i = start; i < source.length; i++) {
|
|
3786
3934
|
const ch = source[i];
|
|
3787
|
-
if (ch ===
|
|
3935
|
+
if (ch === "{")
|
|
3788
3936
|
depth++;
|
|
3789
|
-
else if (ch ===
|
|
3937
|
+
else if (ch === "}" && --depth === 0)
|
|
3790
3938
|
return source.slice(start, i + 1);
|
|
3791
3939
|
}
|
|
3792
3940
|
return null;
|
|
@@ -3809,7 +3957,7 @@ function pageBlocksOnHostCall(source) {
|
|
|
3809
3957
|
const body = defaultExportBody(source);
|
|
3810
3958
|
// Matches `await rpc(`, and the explicitly-qualified forms a typed codebase
|
|
3811
3959
|
// may use: `await globalThis.rpc(` / `await (globalThis as any).rpc(`.
|
|
3812
|
-
const qualifier =
|
|
3960
|
+
const qualifier = "(?:\\(\\s*globalThis[^)]*\\)\\s*\\.\\s*|globalThis\\s*\\.\\s*)?";
|
|
3813
3961
|
const awaited = new RegExp(`\\bawait\\s+${qualifier}${hostGlobal}\\s*[<(]`);
|
|
3814
3962
|
return body !== null && awaited.test(body);
|
|
3815
3963
|
}
|
|
@@ -3817,7 +3965,7 @@ function pageBlocksOnHostCall(source) {
|
|
|
3817
3965
|
function hasLoadingInChain(pageDir) {
|
|
3818
3966
|
let dir = pageDir;
|
|
3819
3967
|
while (dir.startsWith(appDir)) {
|
|
3820
|
-
if (findRouteFile(dir,
|
|
3968
|
+
if (findRouteFile(dir, "loading"))
|
|
3821
3969
|
return true;
|
|
3822
3970
|
if (dir === appDir)
|
|
3823
3971
|
break;
|
|
@@ -3844,9 +3992,9 @@ function hasLoadingInChain(pageDir) {
|
|
|
3844
3992
|
function validateErrorBoundaries() {
|
|
3845
3993
|
const wrong = [];
|
|
3846
3994
|
for (const c of components.values()) {
|
|
3847
|
-
if (!c.name.endsWith(
|
|
3995
|
+
if (!c.name.endsWith("/error"))
|
|
3848
3996
|
continue;
|
|
3849
|
-
const source = readFileSync(c.absPath,
|
|
3997
|
+
const source = readFileSync(c.absPath, "utf-8");
|
|
3850
3998
|
if (!/^\s*['"]use client['"]/m.test(source)) {
|
|
3851
3999
|
wrong.push(` ${relative(projectRoot, c.absPath)}`);
|
|
3852
4000
|
}
|
|
@@ -3856,17 +4004,20 @@ function validateErrorBoundaries() {
|
|
|
3856
4004
|
function validateLoadingBoundaries() {
|
|
3857
4005
|
const errors = [];
|
|
3858
4006
|
for (const c of components.values()) {
|
|
3859
|
-
if (!c.name.endsWith(
|
|
4007
|
+
if (!c.name.endsWith("/page") && c.name !== "app/page")
|
|
3860
4008
|
continue;
|
|
3861
4009
|
const pageDir = dirname(c.absPath);
|
|
3862
|
-
const source = readFileSync(c.absPath,
|
|
4010
|
+
const source = readFileSync(c.absPath, "utf-8");
|
|
3863
4011
|
let reason = null;
|
|
3864
4012
|
if (pageBlocksOnHostCall(source)) {
|
|
3865
4013
|
reason = `its default export awaits ${hostGlobal}()`;
|
|
3866
4014
|
}
|
|
3867
4015
|
else {
|
|
3868
4016
|
const configPath = routeConfig ? join(pageDir, routeConfig.file) : null;
|
|
3869
|
-
if (routeConfig &&
|
|
4017
|
+
if (routeConfig &&
|
|
4018
|
+
configPath &&
|
|
4019
|
+
existsSync(configPath) &&
|
|
4020
|
+
routeConfig.dynamicPattern.test(readFileSync(configPath, "utf-8"))) {
|
|
3870
4021
|
reason = `${routeConfig.file} resolves props dynamically`;
|
|
3871
4022
|
}
|
|
3872
4023
|
}
|
|
@@ -3879,10 +4030,42 @@ function validateLoadingBoundaries() {
|
|
|
3879
4030
|
// ── Plugin ───────────────────────────────────────────────────────────────────
|
|
3880
4031
|
/** Names of plugins that transform JSX and must run after rsc() has split it. */
|
|
3881
4032
|
const JSX_PLUGIN_PATTERN = /react|babel|oxc/i;
|
|
4033
|
+
/**
|
|
4034
|
+
* The server-side stand-in for a client export, made extendable.
|
|
4035
|
+
*
|
|
4036
|
+
* In the rsc environment plugin-rsc replaces every export of a "use client"
|
|
4037
|
+
* module with a stub that throws when called. It emits the stub as an arrow
|
|
4038
|
+
* function, and an arrow function has no prototype, so a server module that
|
|
4039
|
+
* does `class NullStore extends ReactStore` - base-ui does, with only the
|
|
4040
|
+
* base class marked "use client" - dies at the class declaration with
|
|
4041
|
+
* "The superclass is not a constructor", on a line nobody wrote, whenever a
|
|
4042
|
+
* server component imports the library directly. Next.js allows that import;
|
|
4043
|
+
* React's own client references are proxies over a real function.
|
|
4044
|
+
*
|
|
4045
|
+
* A `function` stub has a prototype, so the declaration succeeds, and the
|
|
4046
|
+
* stub still throws - now from `super()`, with its own message - if the
|
|
4047
|
+
* class is ever constructed on the server.
|
|
4048
|
+
*/
|
|
4049
|
+
const CLIENT_STUB = /registerClientReference\(\s*\(\) => \{ throw new Error\("Unexpectedly client reference export '"/g;
|
|
4050
|
+
function extendableClientReferences() {
|
|
4051
|
+
return {
|
|
4052
|
+
name: "rsc-kit:extendable-client-references",
|
|
4053
|
+
enforce: "post",
|
|
4054
|
+
applyToEnvironment: (environment) => environment.name === "rsc",
|
|
4055
|
+
transform(code) {
|
|
4056
|
+
if (!code.includes("Unexpectedly client reference export"))
|
|
4057
|
+
return;
|
|
4058
|
+
return {
|
|
4059
|
+
code: code.replace(CLIENT_STUB, 'registerClientReference(function () { throw new Error("Unexpectedly client reference export \'"'),
|
|
4060
|
+
map: null,
|
|
4061
|
+
};
|
|
4062
|
+
},
|
|
4063
|
+
};
|
|
4064
|
+
}
|
|
3882
4065
|
export function rscKit(options = {}) {
|
|
3883
4066
|
resolvePaths(options);
|
|
3884
4067
|
const routesPlugin = {
|
|
3885
|
-
name:
|
|
4068
|
+
name: "rsc-kit",
|
|
3886
4069
|
config(_config, env) {
|
|
3887
4070
|
if (!existsSync(appDir)) {
|
|
3888
4071
|
throw new Error(`[rsc-kit] No app directory at ${appDir} — nothing to build.`);
|
|
@@ -3901,24 +4084,24 @@ export function rscKit(options = {}) {
|
|
|
3901
4084
|
// editing is a state to pass through, not to be thrown out of.
|
|
3902
4085
|
if (components.size === 0) {
|
|
3903
4086
|
const message = `No routes under ${appDir}. A directory with a page.tsx in it is a route; ` +
|
|
3904
|
-
|
|
3905
|
-
if (env.command ===
|
|
4087
|
+
"without one there is nothing to serve.";
|
|
4088
|
+
if (env.command === "build")
|
|
3906
4089
|
throw new Error(`[rsc-kit] ${message}`);
|
|
3907
4090
|
log(message);
|
|
3908
4091
|
}
|
|
3909
4092
|
const notClient = validateErrorBoundaries();
|
|
3910
4093
|
if (notClient.length) {
|
|
3911
|
-
throw new Error(
|
|
3912
|
-
notClient.join(
|
|
4094
|
+
throw new Error("[rsc-kit] An error.tsx must be a client component.\n\n" +
|
|
4095
|
+
notClient.join("\n") +
|
|
3913
4096
|
"\n\nAdd 'use client' at the top. It is rendered inside an error boundary and is\n" +
|
|
3914
|
-
|
|
4097
|
+
"handed a reset() callback to call, neither of which a server component can do.");
|
|
3915
4098
|
}
|
|
3916
4099
|
const loadingErrors = validateLoadingBoundaries();
|
|
3917
4100
|
if (loadingErrors.length) {
|
|
3918
|
-
throw new Error(
|
|
3919
|
-
loadingErrors.join(
|
|
3920
|
-
|
|
3921
|
-
|
|
4101
|
+
throw new Error("[rsc-kit] A page that blocks before it can paint needs a loading.tsx boundary.\n\n" +
|
|
4102
|
+
loadingErrors.join("\n") +
|
|
4103
|
+
"\n\nAdd loading.tsx in the page directory (or a parent), or move the slow work\n" +
|
|
4104
|
+
"into a child component wrapped in its own <Suspense> so the page can paint.");
|
|
3922
4105
|
}
|
|
3923
4106
|
// Before the entries, because the app's own source imports these and the
|
|
3924
4107
|
// module graph is walked as soon as this hook returns.
|
|
@@ -3931,7 +4114,7 @@ export function rscKit(options = {}) {
|
|
|
3931
4114
|
// host calls resolve their endpoint, from the app's own .env, so the two
|
|
3932
4115
|
// cannot end up pointing at different backends. Development only: a
|
|
3933
4116
|
// build's server.ts decides this for itself.
|
|
3934
|
-
const backendEnv = loadEnv(env.mode, projectRoot,
|
|
4117
|
+
const backendEnv = loadEnv(env.mode, projectRoot, "");
|
|
3935
4118
|
// Only a backend this server could actually call. The shared secret is
|
|
3936
4119
|
// what makes one a backend rather than a url that happens to be in the
|
|
3937
4120
|
// environment — APP_URL is not a Laravel-only name, and a JavaScript
|
|
@@ -3943,16 +4126,19 @@ export function rscKit(options = {}) {
|
|
|
3943
4126
|
// the address down means it.
|
|
3944
4127
|
const backendSecret = hostCallOptions?.secret ?? backendEnv.RSC_HOST_CALL_SECRET;
|
|
3945
4128
|
const detected = backendSecret
|
|
3946
|
-
? (hostCallOptions?.endpoint ??
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
4129
|
+
? (hostCallOptions?.endpoint ??
|
|
4130
|
+
backendEnv.RSC_BACKEND ??
|
|
4131
|
+
backendEnv.APP_URL ??
|
|
4132
|
+
"")
|
|
4133
|
+
: "";
|
|
4134
|
+
const fallbackOrigin = options.devFallback === false ? "" : (options.devFallback ?? detected);
|
|
4135
|
+
writeFileSync(join(genDir, "entry.rsc.tsx"), generateEntryRsc(fallbackOrigin));
|
|
4136
|
+
writeFileSync(join(genDir, "entry.ssr.tsx"), generateEntrySsr());
|
|
4137
|
+
writeFileSync(join(genDir, "entry.browser.tsx"), generateEntryBrowser());
|
|
3952
4138
|
// Written beside the entries, for a host to read instead of walking the
|
|
3953
4139
|
// route tree itself. Laravel scans it a second time today; a JS host
|
|
3954
4140
|
// would otherwise have to write a third walk of the same directories.
|
|
3955
|
-
writeFileSync(join(outDir,
|
|
4141
|
+
writeFileSync(join(outDir, "routes.json"), JSON.stringify(manifest, null, 2));
|
|
3956
4142
|
return {
|
|
3957
4143
|
// Off, not merely unused: Vite warns when publicDir sits inside outDir,
|
|
3958
4144
|
// and assetsDir is normally a directory under the build output. An app
|
|
@@ -3973,7 +4159,7 @@ export function rscKit(options = {}) {
|
|
|
3973
4159
|
* answer travels with the bundle instead of with whoever starts it.
|
|
3974
4160
|
*/
|
|
3975
4161
|
define: {
|
|
3976
|
-
|
|
4162
|
+
"process.env.NODE_ENV": JSON.stringify(env.mode === "development" ? "development" : "production"),
|
|
3977
4163
|
// A constant, so the boundary and its import fall out of the bundle
|
|
3978
4164
|
// entirely when this is off rather than shipping a branch nobody takes.
|
|
3979
4165
|
__RSC_VIEW_TRANSITIONS__: JSON.stringify(viewTransitions),
|
|
@@ -4010,7 +4196,7 @@ export function rscKit(options = {}) {
|
|
|
4010
4196
|
// "The server is configured with a public base URL", which reads as a
|
|
4011
4197
|
// routing bug rather than as this line. In dev the pages are the root;
|
|
4012
4198
|
// the assets come from the same origin either way.
|
|
4013
|
-
base:
|
|
4199
|
+
base: "/",
|
|
4014
4200
|
// Where the generated entries live, so Vite resolves them as its own
|
|
4015
4201
|
// source. A build concern only.
|
|
4016
4202
|
//
|
|
@@ -4030,7 +4216,7 @@ export function rscKit(options = {}) {
|
|
|
4030
4216
|
// built bundles reference it zero times — deduping it was a no-op left
|
|
4031
4217
|
// over from the hand-rolled engine.
|
|
4032
4218
|
resolve: {
|
|
4033
|
-
dedupe: [
|
|
4219
|
+
dedupe: ["react", "react-dom", "@vitejs/plugin-rsc"],
|
|
4034
4220
|
// `import Link from '<packageAlias>/Link'` resolves to the client
|
|
4035
4221
|
// runtime shipped here, for hosts that vendor this package outside
|
|
4036
4222
|
// node_modules. Installed from npm the name resolves on its own.
|
|
@@ -4038,15 +4224,35 @@ export function rscKit(options = {}) {
|
|
|
4038
4224
|
},
|
|
4039
4225
|
build: { emptyOutDir: true },
|
|
4040
4226
|
environments: {
|
|
4041
|
-
// Server bundles — stay under the (non-public) out dir.
|
|
4042
|
-
|
|
4043
|
-
|
|
4227
|
+
// Server bundles — stay under the (non-public) out dir. `bun` and
|
|
4228
|
+
// `bun:*` are the runtime's own modules, like `node:*`: nothing to
|
|
4229
|
+
// bundle, and a build under Node has nothing to resolve them to.
|
|
4230
|
+
// Left as imports for the runtime that has them.
|
|
4231
|
+
rsc: {
|
|
4232
|
+
build: {
|
|
4233
|
+
rollupOptions: {
|
|
4234
|
+
input: { index: join(genDir, "entry.rsc.tsx") },
|
|
4235
|
+
external: RUNTIME_BUILTINS,
|
|
4236
|
+
},
|
|
4237
|
+
},
|
|
4238
|
+
},
|
|
4239
|
+
ssr: {
|
|
4240
|
+
build: {
|
|
4241
|
+
rollupOptions: {
|
|
4242
|
+
input: { index: join(genDir, "entry.ssr.tsx") },
|
|
4243
|
+
external: RUNTIME_BUILTINS,
|
|
4244
|
+
},
|
|
4245
|
+
},
|
|
4246
|
+
},
|
|
4044
4247
|
// Client bundle — emitted into public/ for the web server to serve.
|
|
4045
4248
|
client: {
|
|
4046
4249
|
build: {
|
|
4047
4250
|
outDir: publicAssetsDir,
|
|
4048
4251
|
emptyOutDir: true,
|
|
4049
|
-
rollupOptions: {
|
|
4252
|
+
rollupOptions: {
|
|
4253
|
+
input: { index: join(genDir, "entry.browser.tsx") },
|
|
4254
|
+
output: { chunkFileNames: clientChunkFileName },
|
|
4255
|
+
},
|
|
4050
4256
|
},
|
|
4051
4257
|
},
|
|
4052
4258
|
},
|
|
@@ -4066,6 +4272,25 @@ export function rscKit(options = {}) {
|
|
|
4066
4272
|
* nothing.
|
|
4067
4273
|
*/
|
|
4068
4274
|
configureServer(server) {
|
|
4275
|
+
// The icons and share images live in app/, which nothing serves; the
|
|
4276
|
+
// build copies them beside the client output. There is no output while
|
|
4277
|
+
// developing, so the same hrefs the head tags carry are answered from
|
|
4278
|
+
// app/ here - and the web manifest with them - or the tab has no icon
|
|
4279
|
+
// and the console a 404 for a file that is right there.
|
|
4280
|
+
server.middlewares.use((req, res, next) => {
|
|
4281
|
+
const url = (req.url ?? "").split("?")[0];
|
|
4282
|
+
if (webManifestOptions && url === MANIFEST_PATH) {
|
|
4283
|
+
res.setHeader("Content-Type", "application/manifest+json");
|
|
4284
|
+
res.end(webManifest(webManifestOptions));
|
|
4285
|
+
return;
|
|
4286
|
+
}
|
|
4287
|
+
const asset = allAppAssets(foundAssets).find((a) => a.href === url);
|
|
4288
|
+
const file = asset ? join(sourceDir, "app", asset.file) : null;
|
|
4289
|
+
if (!asset || !file || !existsSync(file))
|
|
4290
|
+
return next();
|
|
4291
|
+
res.setHeader("Content-Type", typeOf(asset.file));
|
|
4292
|
+
createReadStream(file).pipe(res);
|
|
4293
|
+
});
|
|
4069
4294
|
// rpc() has to reach the backend while the dev server is serving.
|
|
4070
4295
|
//
|
|
4071
4296
|
// A built deployment installs this itself: the server running
|
|
@@ -4073,22 +4298,24 @@ export function rscKit(options = {}) {
|
|
|
4073
4298
|
// no host installed every rpc() is refused — so a page whose data comes
|
|
4074
4299
|
// from the backend renders its shell and then blanks, which reads as a
|
|
4075
4300
|
// hydration bug rather than a missing wire.
|
|
4076
|
-
server.httpServer?.once(
|
|
4301
|
+
server.httpServer?.once("listening", async () => {
|
|
4077
4302
|
const env = server.environments?.rsc;
|
|
4078
4303
|
if (!env?.runner)
|
|
4079
4304
|
return;
|
|
4080
4305
|
// The app's own .env, unprefixed. A Laravel app already has both of
|
|
4081
4306
|
// these, which is what makes this need no configuring: APP_URL is the
|
|
4082
4307
|
// backend and RSC_HOST_CALL_SECRET is the secret it checks.
|
|
4083
|
-
const fromEnv = loadEnv(server.config.mode, projectRoot,
|
|
4308
|
+
const fromEnv = loadEnv(server.config.mode, projectRoot, "");
|
|
4084
4309
|
const secret = hostCallOptions?.secret ?? fromEnv.RSC_HOST_CALL_SECRET;
|
|
4085
4310
|
const origin = hostCallOptions?.endpoint ?? fromEnv.RSC_BACKEND ?? fromEnv.APP_URL;
|
|
4086
4311
|
if (!secret || !origin)
|
|
4087
4312
|
return;
|
|
4088
|
-
const path = hostCallOptions?.path ??
|
|
4089
|
-
|
|
4313
|
+
const path = hostCallOptions?.path ??
|
|
4314
|
+
fromEnv.RSC_HOST_CALL_PATH ??
|
|
4315
|
+
"/__rsc/host-call";
|
|
4316
|
+
const endpoint = origin.replace(/\/$/, "") + path;
|
|
4090
4317
|
try {
|
|
4091
|
-
const entry = await env.runner.import(join(genDir,
|
|
4318
|
+
const entry = await env.runner.import(join(genDir, "entry.rsc.tsx"));
|
|
4092
4319
|
const install = entry.installHostFn;
|
|
4093
4320
|
install?.(httpHostCalls({ endpoint, secret }));
|
|
4094
4321
|
}
|
|
@@ -4111,7 +4338,7 @@ export function rscKit(options = {}) {
|
|
|
4111
4338
|
}
|
|
4112
4339
|
catch { }
|
|
4113
4340
|
};
|
|
4114
|
-
server.httpServer?.once(
|
|
4341
|
+
server.httpServer?.once("listening", () => {
|
|
4115
4342
|
// The url Vite resolved, not one built from the port. A dev server
|
|
4116
4343
|
// whose port is already taken on IPv4 binds IPv6 only and keeps the
|
|
4117
4344
|
// number — so http://127.0.0.1:<port> is a reachable-looking address
|
|
@@ -4120,25 +4347,25 @@ export function rscKit(options = {}) {
|
|
|
4120
4347
|
const resolved = server.resolvedUrls?.local?.[0];
|
|
4121
4348
|
const address = server.httpServer?.address();
|
|
4122
4349
|
const url = resolved ??
|
|
4123
|
-
(typeof address ===
|
|
4124
|
-
? `http://${address.family ===
|
|
4350
|
+
(typeof address === "object" && address
|
|
4351
|
+
? `http://${address.family === "IPv6" ? `[${address.address}]` : address.address}:${address.port}`
|
|
4125
4352
|
: null);
|
|
4126
4353
|
if (!url)
|
|
4127
4354
|
return;
|
|
4128
4355
|
mkdirSync(dirname(hotFile), { recursive: true });
|
|
4129
|
-
writeFileSync(hotFile, url.replace(/\/$/,
|
|
4356
|
+
writeFileSync(hotFile, url.replace(/\/$/, ""));
|
|
4130
4357
|
});
|
|
4131
|
-
for (const signal of [
|
|
4358
|
+
for (const signal of ["SIGINT", "SIGTERM", "exit"]) {
|
|
4132
4359
|
process.once(signal, remove);
|
|
4133
4360
|
}
|
|
4134
|
-
server.httpServer?.once(
|
|
4361
|
+
server.httpServer?.once("close", remove);
|
|
4135
4362
|
}
|
|
4136
4363
|
const shapes = new Set(ROUTE_FILES.map((name) => name));
|
|
4137
4364
|
const affectsRouting = (file) => {
|
|
4138
4365
|
if (!file.startsWith(sourceDir))
|
|
4139
4366
|
return false;
|
|
4140
|
-
const base = file.split(
|
|
4141
|
-
const stem = base.replace(/\.(tsx|jsx|ts|js)$/,
|
|
4367
|
+
const base = file.split("/").pop() ?? "";
|
|
4368
|
+
const stem = base.replace(/\.(tsx|jsx|ts|js)$/, "");
|
|
4142
4369
|
// The host's route-config file, whatever it named it. Hardcoding one
|
|
4143
4370
|
// here would put a backend's convention back into a plugin that is
|
|
4144
4371
|
// published without any — generic-host.test.ts fails if it reappears.
|
|
@@ -4155,8 +4382,8 @@ export function rscKit(options = {}) {
|
|
|
4155
4382
|
// Watched explicitly: the Vite root is the *out* directory, so the app's
|
|
4156
4383
|
// source tree is outside it and nothing would report a file appearing.
|
|
4157
4384
|
server.watcher.add(sourceDir);
|
|
4158
|
-
server.watcher.on(
|
|
4159
|
-
server.watcher.on(
|
|
4385
|
+
server.watcher.on("add", restart);
|
|
4386
|
+
server.watcher.on("unlink", restart);
|
|
4160
4387
|
},
|
|
4161
4388
|
/**
|
|
4162
4389
|
* Freeze what can be frozen, once every bundle exists.
|
|
@@ -4192,7 +4419,8 @@ export function rscKit(options = {}) {
|
|
|
4192
4419
|
// differ — <outDir>/dist/rsc on its own, node_modules/.nitro/… under
|
|
4193
4420
|
// Nitro — and hard-coding the first is what silently skipped the second.
|
|
4194
4421
|
// Both environments emit `index.js`, so this is one path, not a branch.
|
|
4195
|
-
const rscOut = builder?.environments?.rsc?.config?.build?.outDir ??
|
|
4422
|
+
const rscOut = builder?.environments?.rsc?.config?.build?.outDir ??
|
|
4423
|
+
join(outDir, "dist/rsc");
|
|
4196
4424
|
const bundle = resolveRscBundle(rscOut);
|
|
4197
4425
|
// Where the frozen pages go, which is not the same question.
|
|
4198
4426
|
//
|
|
@@ -4204,7 +4432,7 @@ export function rscKit(options = {}) {
|
|
|
4204
4432
|
// they are in place by the time it does.
|
|
4205
4433
|
const clientOut = builder?.environments?.client?.config?.build?.outDir;
|
|
4206
4434
|
const staticDir = clientOut
|
|
4207
|
-
? join(dirname(clientOut),
|
|
4435
|
+
? join(dirname(clientOut), "server", NITRO_STATIC_DIR)
|
|
4208
4436
|
: join(outDir, NITRO_STATIC_DIR);
|
|
4209
4437
|
const { frozen, results } = await prerenderAfterBundles(bundle, staticDir, clientOut ?? publicAssetsDir, builder ? knownActionsOf(builder.config, projectRoot) : []);
|
|
4210
4438
|
// The same pages once more, as a module. A Worker has no filesystem, and
|
|
@@ -4214,7 +4442,7 @@ export function rscKit(options = {}) {
|
|
|
4214
4442
|
// when the directory is not there. Only under Nitro, whose presets are
|
|
4215
4443
|
// the ones without a disk; on its own the plugin serves from outDir.
|
|
4216
4444
|
if (clientOut && existsSync(staticDir)) {
|
|
4217
|
-
const { inlineModuleName, inlineModuleSource } = await import(
|
|
4445
|
+
const { inlineModuleName, inlineModuleSource } = await import("./files.js");
|
|
4218
4446
|
writeFileSync(join(dirname(staticDir), inlineModuleName(NITRO_STATIC_DIR)), await inlineModuleSource(staticDir));
|
|
4219
4447
|
}
|
|
4220
4448
|
// Manifest first. The service worker precaches whatever it finds in this
|
|
@@ -4230,11 +4458,12 @@ export function rscKit(options = {}) {
|
|
|
4230
4458
|
},
|
|
4231
4459
|
configResolved(config) {
|
|
4232
4460
|
isWatch = config.build?.watch != null;
|
|
4233
|
-
|
|
4461
|
+
resolvedConfig = config;
|
|
4462
|
+
const nitroMain = config.plugins.find((p) => p.name === "nitro:main");
|
|
4234
4463
|
if (nitroMain?.hotUpdate) {
|
|
4235
4464
|
const original = nitroMain.hotUpdate;
|
|
4236
4465
|
nitroMain.hotUpdate = function (ctx) {
|
|
4237
|
-
if (this.environment?.name ===
|
|
4466
|
+
if (this.environment?.name === "rsc")
|
|
4238
4467
|
return;
|
|
4239
4468
|
return original.call(this, ctx);
|
|
4240
4469
|
};
|
|
@@ -4243,12 +4472,12 @@ export function rscKit(options = {}) {
|
|
|
4243
4472
|
// placed ahead of it sees the wrong graph and fails in ways that are hard
|
|
4244
4473
|
// to trace back here. Cheaper to refuse than to let it through.
|
|
4245
4474
|
const names = config.plugins.map((p) => p.name);
|
|
4246
|
-
const rscAt = names.findIndex((n) => n ===
|
|
4475
|
+
const rscAt = names.findIndex((n) => n === "rsc" || n.startsWith("rsc:"));
|
|
4247
4476
|
const jsxAt = names.findIndex((n) => JSX_PLUGIN_PATTERN.test(n));
|
|
4248
4477
|
if (rscAt !== -1 && jsxAt !== -1 && jsxAt < rscAt) {
|
|
4249
4478
|
throw new Error(`[rsc-kit] Plugin "${names[jsxAt]}" is resolved ahead of rsc(), so it would ` +
|
|
4250
|
-
|
|
4251
|
-
|
|
4479
|
+
"transform JSX before the client/server split.\n" +
|
|
4480
|
+
"Put rscKit() first in your plugins array. If it already is, that plugin " +
|
|
4252
4481
|
"sets enforce: 'pre' and needs to be moved after rsc() explicitly.");
|
|
4253
4482
|
}
|
|
4254
4483
|
},
|
|
@@ -4277,6 +4506,7 @@ export function rscKit(options = {}) {
|
|
|
4277
4506
|
clientChunks: (meta) => meta.normalizedId,
|
|
4278
4507
|
...actionEncryptionKey(),
|
|
4279
4508
|
}),
|
|
4509
|
+
extendableClientReferences(),
|
|
4280
4510
|
routesPlugin,
|
|
4281
4511
|
];
|
|
4282
4512
|
}
|
|
@@ -4325,14 +4555,14 @@ function actionEncryptionKey() {
|
|
|
4325
4555
|
// the key is read, so what ships is the lookup rather than the secret. A
|
|
4326
4556
|
// literal here would put the key in the bundle, which is the thing being
|
|
4327
4557
|
// avoided.
|
|
4328
|
-
return { defineEncryptionKey:
|
|
4558
|
+
return { defineEncryptionKey: "process.env.RSC_ACTION_ENCRYPTION_KEY" };
|
|
4329
4559
|
}
|
|
4330
4560
|
async function appPluginRsc(options = {}) {
|
|
4331
4561
|
try {
|
|
4332
4562
|
// Resolved against a file *in* the root, since a directory specifier
|
|
4333
4563
|
// resolves relative to its parent.
|
|
4334
|
-
const fromApp = createRequire(join(projectRoot,
|
|
4335
|
-
const entry = fromApp.resolve(
|
|
4564
|
+
const fromApp = createRequire(join(projectRoot, "package.json"));
|
|
4565
|
+
const entry = fromApp.resolve("@vitejs/plugin-rsc");
|
|
4336
4566
|
const mod = (await import(pathToFileURL(entry).href));
|
|
4337
4567
|
return (mod.default ?? rsc)(options);
|
|
4338
4568
|
}
|