@rsc-kit/core 0.16.0 → 0.16.2
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/README.md +1 -1
- package/dist/apiPrerender.d.ts +2 -0
- package/dist/apiPrerender.js +111 -81
- package/dist/apiPrerender.js.map +1 -1
- package/dist/buildReport.d.ts +2 -0
- package/dist/buildReport.js.map +1 -1
- package/dist/files.d.ts +7 -0
- package/dist/files.js +113 -17
- package/dist/files.js.map +1 -1
- package/dist/host.d.ts +11 -0
- package/dist/host.js +62 -6
- package/dist/host.js.map +1 -1
- 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/manifest.d.ts +0 -14
- package/dist/manifest.js.map +1 -1
- package/dist/prerender.d.ts +18 -2
- package/dist/prerender.js +216 -138
- 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 +27 -10
- package/dist/vite.js +550 -323
- package/dist/vite.js.map +1 -1
- package/package.json +2 -2
package/dist/vite.js
CHANGED
|
@@ -12,22 +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, } 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, appAssets, headTags } 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 = "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;
|
|
31
67
|
let outDir;
|
|
32
68
|
let appDir;
|
|
33
69
|
let genDir;
|
|
@@ -62,7 +98,13 @@ let viewTransitions = false;
|
|
|
62
98
|
/** Whether a service worker is generated and registered — see options. */
|
|
63
99
|
let offline = false;
|
|
64
100
|
let webManifestOptions = null;
|
|
65
|
-
let foundAssets = {
|
|
101
|
+
let foundAssets = {
|
|
102
|
+
favicon: null,
|
|
103
|
+
icons: [],
|
|
104
|
+
appleIcon: null,
|
|
105
|
+
openGraph: null,
|
|
106
|
+
twitter: null,
|
|
107
|
+
};
|
|
66
108
|
/** Host functions to generate stubs for — see RscKitOptions.hostActions. */
|
|
67
109
|
let hostActions;
|
|
68
110
|
/**
|
|
@@ -77,11 +119,11 @@ let hostActions;
|
|
|
77
119
|
/** This package's name, for excluding it from dep optimization. */
|
|
78
120
|
const PACKAGE_NAME = (() => {
|
|
79
121
|
try {
|
|
80
|
-
const manifest = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)),
|
|
81
|
-
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";
|
|
82
124
|
}
|
|
83
125
|
catch {
|
|
84
|
-
return
|
|
126
|
+
return "@rsc-kit/core";
|
|
85
127
|
}
|
|
86
128
|
})();
|
|
87
129
|
function thisDir() {
|
|
@@ -96,7 +138,7 @@ function envRouteConfig() {
|
|
|
96
138
|
return { file, dynamicPattern: new RegExp(pattern) };
|
|
97
139
|
}
|
|
98
140
|
/** The file a backend writes its action names into. */
|
|
99
|
-
const HOST_ACTIONS_FILE =
|
|
141
|
+
const HOST_ACTIONS_FILE = "rsc-host-actions.json";
|
|
100
142
|
/**
|
|
101
143
|
* Host actions, read from a file the backend wrote.
|
|
102
144
|
*
|
|
@@ -117,8 +159,10 @@ function fileHostActions(root) {
|
|
|
117
159
|
if (!existsSync(path))
|
|
118
160
|
return {};
|
|
119
161
|
try {
|
|
120
|
-
const parsed = JSON.parse(readFileSync(path,
|
|
121
|
-
if (parsed === null ||
|
|
162
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
163
|
+
if (parsed === null ||
|
|
164
|
+
typeof parsed !== "object" ||
|
|
165
|
+
Array.isArray(parsed)) {
|
|
122
166
|
throw new Error('expected an object of { jsName: "Class.method" }');
|
|
123
167
|
}
|
|
124
168
|
return parsed;
|
|
@@ -143,23 +187,23 @@ function fileHostActions(root) {
|
|
|
143
187
|
function aliasEntries() {
|
|
144
188
|
if (!packageAlias)
|
|
145
189
|
return [];
|
|
146
|
-
if (existsSync(join(projectRoot,
|
|
190
|
+
if (existsSync(join(projectRoot, "node_modules", packageAlias)))
|
|
147
191
|
return [];
|
|
148
192
|
return [
|
|
149
193
|
{
|
|
150
|
-
find: new RegExp(
|
|
151
|
-
replacement: join(packageDir,
|
|
194
|
+
find: new RegExp("^" + packageAlias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "/(.*)$"),
|
|
195
|
+
replacement: join(packageDir, "js") + "/$1",
|
|
152
196
|
},
|
|
153
197
|
];
|
|
154
198
|
}
|
|
155
199
|
function resolvePaths(options) {
|
|
156
200
|
projectRoot = resolve(options.projectRoot || process.env.RSC_PROJECT_ROOT || process.cwd());
|
|
157
|
-
sourceDir = resolve(options.sourceDir || process.env.RSC_SOURCE_DIR || join(projectRoot,
|
|
158
|
-
outDir = resolve(options.outDir || process.env.RSC_OUT_DIR || join(projectRoot,
|
|
159
|
-
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");
|
|
160
204
|
// Generated entries live under the (in-project) out dir so module resolution
|
|
161
205
|
// can walk up to the project's node_modules (@vitejs/plugin-rsc, react, ...).
|
|
162
|
-
genDir = join(outDir,
|
|
206
|
+
genDir = join(outDir, ".gen");
|
|
163
207
|
// Not a migration guard — those are not worth carrying in a pre-release.
|
|
164
208
|
// This is the silent-failure guard the rest of this file is written to be.
|
|
165
209
|
//
|
|
@@ -171,31 +215,35 @@ function resolvePaths(options) {
|
|
|
171
215
|
//
|
|
172
216
|
// There is deliberately no check for `nitro`. It is gone from the type, and
|
|
173
217
|
// a config still passing `nitro: true` is asking for exactly what it gets.
|
|
174
|
-
const removed = [
|
|
218
|
+
const removed = ["assetsDir", "assetsUrl"].filter((key) => options[key] !== undefined);
|
|
175
219
|
if (removed.length > 0) {
|
|
176
|
-
throw new Error(`[rsc-kit] ${removed.join(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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.");
|
|
180
224
|
}
|
|
181
225
|
// Vite's own default. Nitro overrides it with .output/public, which is why
|
|
182
226
|
// there is nothing here to configure.
|
|
183
|
-
publicAssetsDir = resolve(join(projectRoot,
|
|
184
|
-
typesDir = join(projectRoot,
|
|
185
|
-
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 || "";
|
|
186
230
|
hostCallOptions = options.hostCall;
|
|
187
231
|
packageDir = resolve(options.packageDir || process.env.RSC_PACKAGE_DIR || thisDir());
|
|
188
|
-
hostGlobal = options.hostGlobal || process.env.RSC_HOST_GLOBAL ||
|
|
189
|
-
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"));
|
|
190
236
|
packageAlias = options.packageAlias || process.env.RSC_PACKAGE_ALIAS || null;
|
|
191
|
-
devOrigin = options.devOrigin || process.env.RSC_DEV_ORIGIN ||
|
|
192
|
-
output = options.output || process.env.RSC_OUTPUT ||
|
|
193
|
-
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";
|
|
194
240
|
// An export decides this for itself: the client has to ask for payloads by
|
|
195
241
|
// url because there is no server to read a header, and the name it asks for
|
|
196
242
|
// is the one the export writes.
|
|
197
243
|
staticPayloads =
|
|
198
|
-
options.staticPayloads ||
|
|
244
|
+
options.staticPayloads ||
|
|
245
|
+
process.env.RSC_STATIC_PAYLOADS ||
|
|
246
|
+
(output === "export" ? "index.rsc" : "");
|
|
199
247
|
// No default: which file marks a route dynamic is the host's convention, and
|
|
200
248
|
// guessing one here would bake a particular backend into a generic plugin.
|
|
201
249
|
// The env pair exists so a host driving the build out of process can pass it
|
|
@@ -203,18 +251,28 @@ function resolvePaths(options) {
|
|
|
203
251
|
routeConfig = options.routeConfig ?? envRouteConfig();
|
|
204
252
|
// A host driving the build out of process cannot pass an option, and may
|
|
205
253
|
// prerender itself afterwards with paths only it knows.
|
|
206
|
-
|
|
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";
|
|
207
261
|
viewTransitions = options.viewTransitions === true;
|
|
208
262
|
offline = options.offline === true;
|
|
263
|
+
inlineStylesheets = options.inlineStylesheets ?? "auto";
|
|
264
|
+
maxActionBody = options.maxActionBody;
|
|
209
265
|
// One place, and it is the file. A plugin option as well would be the same
|
|
210
266
|
// thing sayable in two places, which is the problem the file was moved to
|
|
211
267
|
// solve rather than a convenience to keep beside it.
|
|
212
|
-
webManifestOptions = declaredManifest(join(sourceDir,
|
|
213
|
-
foundAssets = appAssets(join(sourceDir,
|
|
268
|
+
webManifestOptions = declaredManifest(join(sourceDir, "app"));
|
|
269
|
+
foundAssets = appAssets(join(sourceDir, "app"));
|
|
214
270
|
// An app that put icons where they could be found has already listed them.
|
|
215
271
|
// Writing them again in the manifest is the same set in two places, and the
|
|
216
272
|
// one that goes stale is the one nobody looks at.
|
|
217
|
-
if (webManifestOptions &&
|
|
273
|
+
if (webManifestOptions &&
|
|
274
|
+
!webManifestOptions.icons?.length &&
|
|
275
|
+
foundAssets.icons.length) {
|
|
218
276
|
webManifestOptions = {
|
|
219
277
|
...webManifestOptions,
|
|
220
278
|
icons: foundAssets.icons.map((icon) => icon.href),
|
|
@@ -223,7 +281,7 @@ function resolvePaths(options) {
|
|
|
223
281
|
hostActions = options.hostActions ?? fileHostActions(projectRoot);
|
|
224
282
|
}
|
|
225
283
|
function log(...args) {
|
|
226
|
-
console.error(
|
|
284
|
+
console.error("[rsc-kit]", ...args);
|
|
227
285
|
}
|
|
228
286
|
// ── The route manifest ───────────────────────────────────────────────────────
|
|
229
287
|
/**
|
|
@@ -241,37 +299,37 @@ function log(...args) {
|
|
|
241
299
|
*/
|
|
242
300
|
/** `[...path]` → catchAll, `[id]` → param, `(group)` → nothing at all. */
|
|
243
301
|
function urlSegments(componentName) {
|
|
244
|
-
const parts = componentName.split(
|
|
302
|
+
const parts = componentName.split("/").slice(1, -1);
|
|
245
303
|
const segments = [];
|
|
246
304
|
for (const part of parts) {
|
|
247
305
|
// A route group organises files without appearing in the url.
|
|
248
|
-
if (part.startsWith(
|
|
306
|
+
if (part.startsWith("(") && part.endsWith(")"))
|
|
249
307
|
continue;
|
|
250
308
|
// A slot directory is not part of its page's url either.
|
|
251
|
-
if (part.startsWith(
|
|
309
|
+
if (part.startsWith("@"))
|
|
252
310
|
continue;
|
|
253
|
-
if (part.startsWith(
|
|
254
|
-
segments.push({ type:
|
|
311
|
+
if (part.startsWith("[...") && part.endsWith("]")) {
|
|
312
|
+
segments.push({ type: "catchAll", value: part.slice(4, -1) });
|
|
255
313
|
continue;
|
|
256
314
|
}
|
|
257
|
-
if (part.startsWith(
|
|
258
|
-
segments.push({ type:
|
|
315
|
+
if (part.startsWith("[") && part.endsWith("]")) {
|
|
316
|
+
segments.push({ type: "param", value: part.slice(1, -1) });
|
|
259
317
|
continue;
|
|
260
318
|
}
|
|
261
319
|
// An interception marker says which url this replaces, not what it is
|
|
262
320
|
// called: (.)photo intercepts the sibling /photo. Left in place the
|
|
263
321
|
// manifest would claim a route at /(.)photo, which nothing can navigate to.
|
|
264
|
-
segments.push({ type:
|
|
322
|
+
segments.push({ type: "static", value: part.replace(/^\(\.{1,3}\)/, "") });
|
|
265
323
|
}
|
|
266
324
|
return segments;
|
|
267
325
|
}
|
|
268
326
|
/** Whether a component sits under an interception marker: (.) (..) (...) */
|
|
269
327
|
function isIntercept(componentName) {
|
|
270
|
-
return componentName.split(
|
|
328
|
+
return componentName.split("/").some((part) => /^\(\.{1,3}\)/.test(part));
|
|
271
329
|
}
|
|
272
330
|
/** The slot directory a component lives under, if any. */
|
|
273
331
|
function slotOf(componentName) {
|
|
274
|
-
const part = componentName.split(
|
|
332
|
+
const part = componentName.split("/").find((p) => p.startsWith("@"));
|
|
275
333
|
return part ? part.slice(1) : null;
|
|
276
334
|
}
|
|
277
335
|
/**
|
|
@@ -282,15 +340,17 @@ function slotOf(componentName) {
|
|
|
282
340
|
*/
|
|
283
341
|
function routeManifest() {
|
|
284
342
|
const names = [...components.keys()];
|
|
285
|
-
const dirOf = (name) => name.split(
|
|
343
|
+
const dirOf = (name) => name.split("/").slice(0, -1).join("/");
|
|
286
344
|
// By path, not by string: 'app/slow3' begins with 'app/slow' as text and is
|
|
287
345
|
// not inside it, which would hand /slow3 the loading state of /slow.
|
|
288
|
-
const isUnder = (dir, ancestor) => dir === ancestor || dir.startsWith(ancestor +
|
|
346
|
+
const isUnder = (dir, ancestor) => dir === ancestor || dir.startsWith(ancestor + "/");
|
|
289
347
|
const ancestors = (name, base) => names
|
|
290
|
-
.filter((n) => n.endsWith(
|
|
348
|
+
.filter((n) => n.endsWith("/" + base) &&
|
|
349
|
+
!isIntercept(n) &&
|
|
350
|
+
isUnder(dirOf(name), dirOf(n)))
|
|
291
351
|
.sort((a, b) => a.length - b.length);
|
|
292
352
|
/** Project-root-relative, posix — the same string on every machine. */
|
|
293
|
-
const fromRoot = (abs) => relative(projectRoot, abs).replace(/\\/g,
|
|
353
|
+
const fromRoot = (abs) => relative(projectRoot, abs).replace(/\\/g, "/");
|
|
294
354
|
/** The host's config file in a directory, if the host named one and it exists. */
|
|
295
355
|
const configIn = (absDir) => {
|
|
296
356
|
if (!routeConfig)
|
|
@@ -301,9 +361,9 @@ function routeManifest() {
|
|
|
301
361
|
/** Ancestor configs, outermost first, excluding the page's own directory. */
|
|
302
362
|
const ancestorConfigs = (dir) => {
|
|
303
363
|
const found = [];
|
|
304
|
-
const parts = dir.split(
|
|
364
|
+
const parts = dir.split("/").slice(0, -1);
|
|
305
365
|
while (parts.length > 0) {
|
|
306
|
-
const path = configIn(join(sourceDir, parts.join(
|
|
366
|
+
const path = configIn(join(sourceDir, parts.join("/")));
|
|
307
367
|
if (path)
|
|
308
368
|
found.unshift(path);
|
|
309
369
|
parts.pop();
|
|
@@ -325,11 +385,11 @@ function routeManifest() {
|
|
|
325
385
|
* to whoever knows what they mean.
|
|
326
386
|
*/
|
|
327
387
|
const middlewareIn = (absDir) => {
|
|
328
|
-
for (const file of [
|
|
388
|
+
for (const file of ["route.ts", "route.tsx"]) {
|
|
329
389
|
const path = join(absDir, file);
|
|
330
390
|
if (!existsSync(path))
|
|
331
391
|
continue;
|
|
332
|
-
const match = readFileSync(path,
|
|
392
|
+
const match = readFileSync(path, "utf-8").match(/export\s+const\s+middleware\s*(?::[^=]+)?=\s*\[([^\]]*)\]/);
|
|
333
393
|
if (!match)
|
|
334
394
|
continue;
|
|
335
395
|
// Each quoted literal, rather than splitting the list on commas: a
|
|
@@ -351,7 +411,7 @@ function routeManifest() {
|
|
|
351
411
|
* outermost point it was asked for.
|
|
352
412
|
*/
|
|
353
413
|
const hostMiddleware = (dir) => {
|
|
354
|
-
const parts = dir.split(
|
|
414
|
+
const parts = dir.split("/").filter(Boolean);
|
|
355
415
|
const found = [];
|
|
356
416
|
for (let depth = 0; depth <= parts.length; depth++) {
|
|
357
417
|
for (const name of middlewareIn(join(sourceDir, ...parts.slice(0, depth)))) {
|
|
@@ -364,40 +424,47 @@ function routeManifest() {
|
|
|
364
424
|
const routes = [];
|
|
365
425
|
const intercepts = [];
|
|
366
426
|
for (const name of names) {
|
|
367
|
-
if (name.endsWith(
|
|
427
|
+
if (name.endsWith("/page") && isIntercept(name)) {
|
|
368
428
|
const slot = slotOf(name);
|
|
369
429
|
if (slot) {
|
|
370
|
-
const marker = name
|
|
371
|
-
|
|
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
|
+
});
|
|
372
440
|
}
|
|
373
441
|
continue;
|
|
374
442
|
}
|
|
375
|
-
if (!name.endsWith(
|
|
443
|
+
if (!name.endsWith("/page") || slotOf(name))
|
|
376
444
|
continue;
|
|
377
445
|
const slots = {};
|
|
378
446
|
for (const candidate of names) {
|
|
379
447
|
const slot = slotOf(candidate);
|
|
380
448
|
// A slot belongs to the layout in the directory that declares it, so it
|
|
381
449
|
// applies to a page only if that directory is on the page's path.
|
|
382
|
-
if (!slot || isIntercept(candidate) || !candidate.endsWith(
|
|
450
|
+
if (!slot || isIntercept(candidate) || !candidate.endsWith("/default"))
|
|
383
451
|
continue;
|
|
384
|
-
if (isUnder(dirOf(name), candidate.split(
|
|
452
|
+
if (isUnder(dirOf(name), candidate.split("/@")[0]))
|
|
385
453
|
slots[slot] = candidate;
|
|
386
454
|
}
|
|
387
455
|
routes.push({
|
|
388
456
|
component: name,
|
|
389
457
|
segments: urlSegments(name),
|
|
390
|
-
layouts: ancestors(name,
|
|
391
|
-
loadings: ancestors(name,
|
|
392
|
-
errors: ancestors(name,
|
|
393
|
-
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),
|
|
394
462
|
slots,
|
|
395
|
-
sections: names.filter((n) => SECTION_FILE.test(n +
|
|
463
|
+
sections: names.filter((n) => SECTION_FILE.test(n + ".tsx") && dirOf(n) === dirOf(name)),
|
|
396
464
|
config: configIn(join(sourceDir, dirOf(name))),
|
|
397
465
|
hostMiddleware: hostMiddleware(dirOf(name)),
|
|
398
466
|
ancestorConfigs: ancestorConfigs(dirOf(name)),
|
|
399
467
|
staticParams: hasStaticParams(components.get(name).absPath),
|
|
400
|
-
clientJs: shipsClientJs(components.get(name).absPath),
|
|
401
468
|
});
|
|
402
469
|
}
|
|
403
470
|
// What the build decided, for a host that has to act on it afterwards —
|
|
@@ -411,7 +478,7 @@ function routeManifest() {
|
|
|
411
478
|
name,
|
|
412
479
|
segments: urlSegments(name),
|
|
413
480
|
methods,
|
|
414
|
-
middleware: ancestors(name,
|
|
481
|
+
middleware: ancestors(name, "middleware"),
|
|
415
482
|
})),
|
|
416
483
|
};
|
|
417
484
|
}
|
|
@@ -431,17 +498,17 @@ function routeManifest() {
|
|
|
431
498
|
* a line of output; silent costs the types.
|
|
432
499
|
*/
|
|
433
500
|
function warnIfTypesUnreachable() {
|
|
434
|
-
const config = join(projectRoot,
|
|
501
|
+
const config = join(projectRoot, "tsconfig.json");
|
|
435
502
|
if (!existsSync(config))
|
|
436
503
|
return;
|
|
437
504
|
try {
|
|
438
505
|
// Comments are legal in a tsconfig and JSON.parse does not take them.
|
|
439
|
-
const text = readFileSync(config,
|
|
506
|
+
const text = readFileSync(config, "utf-8").replace(/\/\*[\s\S]*?\*\/|(^|\s)\/\/.*$/gm, "$1");
|
|
440
507
|
const include = JSON.parse(text).include;
|
|
441
508
|
if (!Array.isArray(include))
|
|
442
509
|
return;
|
|
443
|
-
const covers = (what) => include.some((entry) => typeof entry ===
|
|
444
|
-
if (!covers(
|
|
510
|
+
const covers = (what) => include.some((entry) => typeof entry === "string" && entry.includes(what));
|
|
511
|
+
if (!covers(".rsc-kit")) {
|
|
445
512
|
log(`tsconfig.json does not include .rsc-kit, where the generated types are written.\n` +
|
|
446
513
|
` Add ".rsc-kit/**/*" to "include", or typed routes and rpc() fall back to string.`);
|
|
447
514
|
}
|
|
@@ -478,17 +545,17 @@ function writeHostBindings(manifest) {
|
|
|
478
545
|
// The global is installed at runtime, so nothing in app source declares it
|
|
479
546
|
// and a typecheck cannot see it. Written whether or not there are actions:
|
|
480
547
|
// server components call it directly too.
|
|
481
|
-
writeFileSync(join(typesDir,
|
|
548
|
+
writeFileSync(join(typesDir, "rsc-env.d.ts"), renderHostGlobalTypes());
|
|
482
549
|
// The urls this build found, so a link to a page that does not exist fails
|
|
483
550
|
// the typecheck instead of the browser.
|
|
484
|
-
writeFileSync(join(typesDir,
|
|
551
|
+
writeFileSync(join(typesDir, "rsc-routes.d.ts"), renderRouteTypes(manifest));
|
|
485
552
|
// The bundle the host imports is generated, so nothing declares it. Written
|
|
486
553
|
// here rather than left to the app: every app needs the identical file, and
|
|
487
554
|
// an app-authored one goes stale — the first version named only RscEngine,
|
|
488
555
|
// which typechecks a server and fails a prerender script.
|
|
489
|
-
writeFileSync(join(typesDir,
|
|
556
|
+
writeFileSync(join(typesDir, "rsc-engine.d.ts"), ENGINE_TYPES);
|
|
490
557
|
warnIfTypesUnreachable();
|
|
491
|
-
const target = join(sourceDir,
|
|
558
|
+
const target = join(sourceDir, "server-actions.generated.ts");
|
|
492
559
|
// A host with no functions of its own leaves no file behind: kept, its
|
|
493
560
|
// stubs would go on naming targets the host has stopped answering for.
|
|
494
561
|
if (Object.keys(hostActions).length === 0) {
|
|
@@ -537,8 +604,8 @@ function reportAllDynamic() {
|
|
|
537
604
|
// The pattern rather than a url: nothing was rendered, so there are no
|
|
538
605
|
// params and inventing one would name a page that may not exist.
|
|
539
606
|
const path = route.segments
|
|
540
|
-
.map((segment) =>
|
|
541
|
-
.join(
|
|
607
|
+
.map((segment) => segment.type === "static" ? segment.value : `[${segment.value}]`)
|
|
608
|
+
.join("/");
|
|
542
609
|
console.log(` \u0192 /${path}`);
|
|
543
610
|
}
|
|
544
611
|
console.log(`
|
|
@@ -606,7 +673,7 @@ ${swExtra
|
|
|
606
673
|
// before anything here can call respondWith on it.
|
|
607
674
|
self.importScripts(${JSON.stringify(swExtra)})
|
|
608
675
|
`
|
|
609
|
-
:
|
|
676
|
+
: ""}
|
|
610
677
|
|
|
611
678
|
self.addEventListener('install', (event) => {
|
|
612
679
|
// The new worker takes over rather than waiting for every tab to close.
|
|
@@ -847,11 +914,11 @@ self.addEventListener('fetch', (event) => {
|
|
|
847
914
|
* it. The file is a literal object by contract, which is all that has to parse.
|
|
848
915
|
*/
|
|
849
916
|
export function declaredManifest(appDir) {
|
|
850
|
-
for (const extension of [
|
|
917
|
+
for (const extension of ["ts", "tsx", "js", "mjs"]) {
|
|
851
918
|
const file = join(appDir, `manifest.${extension}`);
|
|
852
919
|
if (!existsSync(file))
|
|
853
920
|
continue;
|
|
854
|
-
const source = readFileSync(file,
|
|
921
|
+
const source = readFileSync(file, "utf-8");
|
|
855
922
|
// The object literal after `export default`, with a `satisfies` or `as`
|
|
856
923
|
// annotation tolerated after it — which is how anyone who wants the type
|
|
857
924
|
// checked will actually write the file, and getting that wrong would refuse
|
|
@@ -863,8 +930,8 @@ export function declaredManifest(appDir) {
|
|
|
863
930
|
const match = /export\s+default\s+(\{[\s\S]*\})(?:\s+(?:satisfies|as)\s+[\w.<>\[\]| ]+)?\s*;?\s*$/.exec(source.trim());
|
|
864
931
|
if (!match) {
|
|
865
932
|
throw new Error(`[rsc-kit] app/manifest.${extension} must default-export an object literal.\n` +
|
|
866
|
-
|
|
867
|
-
|
|
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.");
|
|
868
935
|
}
|
|
869
936
|
try {
|
|
870
937
|
// Function rather than JSON.parse: the file is TypeScript source with
|
|
@@ -901,7 +968,7 @@ function copyAppAssets(clientDir) {
|
|
|
901
968
|
for (const asset of all) {
|
|
902
969
|
const to = join(clientDir, asset.href.slice(1));
|
|
903
970
|
mkdirSync(dirname(to), { recursive: true });
|
|
904
|
-
copyFileSync(join(sourceDir,
|
|
971
|
+
copyFileSync(join(sourceDir, "app", asset.file), to);
|
|
905
972
|
}
|
|
906
973
|
log(`icons: ${all.length} copied from app/`);
|
|
907
974
|
}
|
|
@@ -925,20 +992,20 @@ function writeWebManifest(clientDir, options) {
|
|
|
925
992
|
* which read did it.
|
|
926
993
|
*/
|
|
927
994
|
function offlineFallback(frozen, results) {
|
|
928
|
-
const found = results.find((r) => r.url ===
|
|
995
|
+
const found = results.find((r) => r.url === "/offline");
|
|
929
996
|
if (!found)
|
|
930
997
|
return null;
|
|
931
|
-
if (!frozen.includes(
|
|
998
|
+
if (!frozen.includes("/offline")) {
|
|
932
999
|
// The reason already reads "dynamic — called cookies()", and this sentence
|
|
933
1000
|
// has said "not stored" by the time it gets there, so the prefix would say
|
|
934
1001
|
// it twice with a dash in the middle of both.
|
|
935
|
-
const why = found.reason?.replace(/^dynamic — /,
|
|
936
|
-
log(
|
|
937
|
-
(why ? `, because it ${why}` :
|
|
938
|
-
|
|
1002
|
+
const why = found.reason?.replace(/^dynamic — /, "") ?? null;
|
|
1003
|
+
log("offline: /offline cannot be the fallback" +
|
|
1004
|
+
(why ? `, because it ${why}` : "") +
|
|
1005
|
+
". A fallback has to be servable with no network at all.");
|
|
939
1006
|
return null;
|
|
940
1007
|
}
|
|
941
|
-
return
|
|
1008
|
+
return "/offline";
|
|
942
1009
|
}
|
|
943
1010
|
/**
|
|
944
1011
|
* The app's own service worker code, if it wrote any.
|
|
@@ -954,11 +1021,11 @@ function offlineFallback(frozen, results) {
|
|
|
954
1021
|
* file is there or it is not.
|
|
955
1022
|
*/
|
|
956
1023
|
function copyServiceWorkerExtra(clientDir) {
|
|
957
|
-
const source = join(sourceDir,
|
|
1024
|
+
const source = join(sourceDir, "app", "sw.js");
|
|
958
1025
|
if (!existsSync(source))
|
|
959
1026
|
return null;
|
|
960
|
-
copyFileSync(source, join(clientDir,
|
|
961
|
-
return
|
|
1027
|
+
copyFileSync(source, join(clientDir, "sw-app.js"));
|
|
1028
|
+
return "/sw-app.js";
|
|
962
1029
|
}
|
|
963
1030
|
function writeServiceWorker(clientDir, frozen = [], offlineUrl = null) {
|
|
964
1031
|
if (!existsSync(clientDir))
|
|
@@ -973,7 +1040,7 @@ function writeServiceWorker(clientDir, frozen = [], offlineUrl = null) {
|
|
|
973
1040
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
974
1041
|
// Not itself, and not a map: a debugger asks for those, a visitor does
|
|
975
1042
|
// not, and precaching them doubles what an install costs.
|
|
976
|
-
if (entry.name ===
|
|
1043
|
+
if (entry.name === "sw.js" || entry.name.endsWith(".map"))
|
|
977
1044
|
continue;
|
|
978
1045
|
const path = join(dir, entry.name);
|
|
979
1046
|
if (entry.isDirectory())
|
|
@@ -982,13 +1049,16 @@ function writeServiceWorker(clientDir, frozen = [], offlineUrl = null) {
|
|
|
982
1049
|
files.push(`${prefix}${entry.name}`);
|
|
983
1050
|
}
|
|
984
1051
|
};
|
|
985
|
-
walk(clientDir,
|
|
986
|
-
const precache = [
|
|
987
|
-
const version = createHash(
|
|
988
|
-
|
|
1052
|
+
walk(clientDir, "");
|
|
1053
|
+
const precache = ["/", ...files.map((file) => `/${file}`)].sort();
|
|
1054
|
+
const version = createHash("sha256")
|
|
1055
|
+
.update(precache.join("\n"))
|
|
1056
|
+
.digest("hex")
|
|
1057
|
+
.slice(0, 12);
|
|
1058
|
+
writeFileSync(join(clientDir, "sw.js"), SERVICE_WORKER(version, precache, frozen, offlineUrl, extra));
|
|
989
1059
|
log(`offline: ${precache.length} files precached as rsc-kit-${version}` +
|
|
990
|
-
(offlineUrl ? `, falling back to ${offlineUrl}` :
|
|
991
|
-
(extra ?
|
|
1060
|
+
(offlineUrl ? `, falling back to ${offlineUrl}` : "") +
|
|
1061
|
+
(extra ? ", with app/sw.js" : ""));
|
|
992
1062
|
}
|
|
993
1063
|
/**
|
|
994
1064
|
* The rsc bundle the build just wrote, whatever it decided to call it.
|
|
@@ -1002,7 +1072,7 @@ function writeServiceWorker(clientDir, frozen = [], offlineUrl = null) {
|
|
|
1002
1072
|
* prints.
|
|
1003
1073
|
*/
|
|
1004
1074
|
function resolveRscBundle(dir) {
|
|
1005
|
-
for (const name of [
|
|
1075
|
+
for (const name of ["index.js", "index.mjs"]) {
|
|
1006
1076
|
const candidate = join(dir, name);
|
|
1007
1077
|
if (existsSync(candidate))
|
|
1008
1078
|
return candidate;
|
|
@@ -1022,9 +1092,9 @@ function knownActionsOf(config, root) {
|
|
|
1022
1092
|
return [];
|
|
1023
1093
|
const out = [];
|
|
1024
1094
|
for (const meta of metaMap.values()) {
|
|
1025
|
-
if (meta.importId.includes(
|
|
1095
|
+
if (meta.importId.includes("/node_modules/"))
|
|
1026
1096
|
continue;
|
|
1027
|
-
const file = relative(root, meta.importId.split(
|
|
1097
|
+
const file = relative(root, meta.importId.split("?")[0] ?? meta.importId);
|
|
1028
1098
|
for (const name of meta.exportNames) {
|
|
1029
1099
|
out.push({ id: `${meta.referenceKey}#${name}`, name, file });
|
|
1030
1100
|
}
|
|
@@ -1032,12 +1102,42 @@ function knownActionsOf(config, root) {
|
|
|
1032
1102
|
return out;
|
|
1033
1103
|
}
|
|
1034
1104
|
async function auditActions(engine, known) {
|
|
1035
|
-
const audit = engine
|
|
1036
|
-
.auditActions;
|
|
1105
|
+
const audit = engine.auditActions;
|
|
1037
1106
|
if (!audit || known.length === 0)
|
|
1038
1107
|
return [];
|
|
1039
1108
|
const byId = new Map(known.map((k) => [k.id, k]));
|
|
1040
|
-
return (await audit(known.map((k) => k.id))).map((a) => ({
|
|
1109
|
+
return (await audit(known.map((k) => k.id))).map((a) => ({
|
|
1110
|
+
...byId.get(a.id),
|
|
1111
|
+
client: a.client,
|
|
1112
|
+
query: a.query,
|
|
1113
|
+
}));
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Inlined into a page with no runtime when it is at most this big on the
|
|
1117
|
+
* wire. The point is the round trip, not the bytes: a stylesheet that
|
|
1118
|
+
* compresses to a few KB costs a whole trip before anything paints, while a
|
|
1119
|
+
* large one is better fetched once and cached across pages. Measured
|
|
1120
|
+
* gzipped, because that is what travels - a sheet of @font-face
|
|
1121
|
+
* declarations is long and compresses to almost nothing.
|
|
1122
|
+
*/
|
|
1123
|
+
const INLINE_STYLESHEET_LIMIT = 10 * 1024;
|
|
1124
|
+
export function smallStylesheetReader(assetsDir, setting) {
|
|
1125
|
+
const limit = setting === true
|
|
1126
|
+
? Infinity
|
|
1127
|
+
: setting === "auto"
|
|
1128
|
+
? INLINE_STYLESHEET_LIMIT
|
|
1129
|
+
: setting;
|
|
1130
|
+
return (href) => {
|
|
1131
|
+
if (!href.startsWith("/") || href.includes(".."))
|
|
1132
|
+
return null;
|
|
1133
|
+
try {
|
|
1134
|
+
const css = readFileSync(join(assetsDir, href), "utf-8");
|
|
1135
|
+
return gzipSync(css).length > limit ? null : css;
|
|
1136
|
+
}
|
|
1137
|
+
catch {
|
|
1138
|
+
return null;
|
|
1139
|
+
}
|
|
1140
|
+
};
|
|
1041
1141
|
}
|
|
1042
1142
|
async function prerenderAfterBundles(bundle, staticDir, assetsDir, knownActions = []) {
|
|
1043
1143
|
// A missing bundle used to be a silent `return`, and under Nitro it was the
|
|
@@ -1048,21 +1148,25 @@ async function prerenderAfterBundles(bundle, staticDir, assetsDir, knownActions
|
|
|
1048
1148
|
// visitor. The caller resolves the path from the environment now, so this is
|
|
1049
1149
|
// back to being the impossible case it reads as.
|
|
1050
1150
|
if (!bundle) {
|
|
1051
|
-
throw new Error(
|
|
1052
|
-
|
|
1053
|
-
'Build with prerender: false to render every page on demand instead.');
|
|
1151
|
+
throw new Error("[rsc-kit] The build produced no rsc bundle to prerender from.\n" +
|
|
1152
|
+
"Prerendering renders the app, so it needs the bundle the build just wrote.");
|
|
1054
1153
|
}
|
|
1055
|
-
const [{ prerender, NotPrerenderable, summary, legend, notes, clientJsSize, pathKey: pathKeyOf }, { writeTo }, { prerenderApiRoutes },] = await Promise.all([
|
|
1056
|
-
import(
|
|
1057
|
-
import(
|
|
1058
|
-
import(
|
|
1154
|
+
const [{ prerender, NotPrerenderable, summary, legend, notes, clientJsSize, pathKey: pathKeyOf, }, { writeTo }, { prerenderApiRoutes },] = await Promise.all([
|
|
1155
|
+
import("./prerender.js"),
|
|
1156
|
+
import("./files.js"),
|
|
1157
|
+
import("./apiPrerender.js"),
|
|
1059
1158
|
]);
|
|
1060
1159
|
// Cleared first: a route that changes classification between builds
|
|
1061
1160
|
// otherwise leaves its old shell on disk and the host goes on serving it.
|
|
1062
1161
|
// Nothing warns — the page loads, with content from the previous build.
|
|
1063
1162
|
rmSync(staticDir, { recursive: true, force: true });
|
|
1064
1163
|
const engine = (await import(pathToFileURL(bundle).href));
|
|
1065
|
-
const mark = {
|
|
1164
|
+
const mark = {
|
|
1165
|
+
frozen: "○",
|
|
1166
|
+
shell: "◐",
|
|
1167
|
+
blocked: "✗",
|
|
1168
|
+
error: "✗",
|
|
1169
|
+
};
|
|
1066
1170
|
let failed = 0;
|
|
1067
1171
|
// Weighed from the page the prerenderer just wrote, so the column is what
|
|
1068
1172
|
// that page actually loads rather than a total every route is charged for.
|
|
@@ -1085,19 +1189,22 @@ async function prerenderAfterBundles(bundle, staticDir, assetsDir, knownActions
|
|
|
1085
1189
|
engine,
|
|
1086
1190
|
write: writeTo(staticDir),
|
|
1087
1191
|
serviceWorker: offline,
|
|
1192
|
+
stylesheet: inlineStylesheets === false
|
|
1193
|
+
? undefined
|
|
1194
|
+
: smallStylesheetReader(assetsDir, inlineStylesheets),
|
|
1088
1195
|
onResult: (r) => {
|
|
1089
1196
|
collected.push(r);
|
|
1090
|
-
if (r.type ===
|
|
1197
|
+
if (r.type === "error" || r.type === "blocked")
|
|
1091
1198
|
failed++;
|
|
1092
1199
|
const key = pathKeyOf(r.url);
|
|
1093
1200
|
const file = [`${key}.html`, `${key}.ppr.html`]
|
|
1094
1201
|
.map((name) => join(staticDir, name))
|
|
1095
1202
|
.find((path) => existsSync(path));
|
|
1096
|
-
const bytes = file ? weigh(readFileSync(file,
|
|
1203
|
+
const bytes = file ? weigh(readFileSync(file, "utf-8")) : null;
|
|
1097
1204
|
if (bytes !== null)
|
|
1098
1205
|
sized.set(r.url, bytes);
|
|
1099
1206
|
pending.push({
|
|
1100
|
-
line: ` ${mark[r.type] ??
|
|
1207
|
+
line: ` ${mark[r.type] ?? " "} ${r.url}`,
|
|
1101
1208
|
bytes,
|
|
1102
1209
|
extra: [
|
|
1103
1210
|
...(r.reason ? [` ${r.reason}`] : []),
|
|
@@ -1122,16 +1229,19 @@ async function prerenderAfterBundles(bundle, staticDir, assetsDir, knownActions
|
|
|
1122
1229
|
const apis = await prerenderApiRoutes(engine, manifest, writeTo(staticDir));
|
|
1123
1230
|
for (const api of apis) {
|
|
1124
1231
|
pending.push({
|
|
1125
|
-
line: ` ${api.type ===
|
|
1232
|
+
line: ` ${api.type === "frozen" ? "○" : "ƒ"} ${api.url}`,
|
|
1126
1233
|
bytes: null,
|
|
1127
|
-
extra:
|
|
1234
|
+
extra: [
|
|
1235
|
+
...(api.reason ? [` ${api.reason}`] : []),
|
|
1236
|
+
...(api.warning ? [` ⚠ ${api.warning}`] : []),
|
|
1237
|
+
],
|
|
1128
1238
|
});
|
|
1129
1239
|
}
|
|
1130
1240
|
// Printed together rather than as each route lands, because a column has to
|
|
1131
1241
|
// line up and the widest url is not known until the last one is in.
|
|
1132
1242
|
const column = Math.max(...pending.map((p) => p.line.length)) + 2;
|
|
1133
1243
|
for (const row of pending) {
|
|
1134
|
-
const size = row.bytes === null ?
|
|
1244
|
+
const size = row.bytes === null ? "" : clientJsSize(row.bytes);
|
|
1135
1245
|
console.log(size ? row.line.padEnd(column) + size : row.line);
|
|
1136
1246
|
for (const line of row.extra)
|
|
1137
1247
|
console.log(line);
|
|
@@ -1147,10 +1257,12 @@ async function prerenderAfterBundles(bundle, staticDir, assetsDir, knownActions
|
|
|
1147
1257
|
const byFile = new Map();
|
|
1148
1258
|
for (const a of bare)
|
|
1149
1259
|
byFile.set(a.file, [...(byFile.get(a.file) ?? []), a.name]);
|
|
1150
|
-
console.log(`\n \u26a0 ${bare.length} ${bare.length === 1 ?
|
|
1151
|
-
[...byFile]
|
|
1152
|
-
|
|
1153
|
-
|
|
1260
|
+
console.log(`\n \u26a0 ${bare.length} ${bare.length === 1 ? "action runs" : "actions run"} no middleware: ` +
|
|
1261
|
+
[...byFile]
|
|
1262
|
+
.map(([file, names]) => `${names.join(", ")} (${file})`)
|
|
1263
|
+
.join("; "));
|
|
1264
|
+
console.log(" Nothing checks who calls them. Fine for a public one; otherwise build it\n" +
|
|
1265
|
+
" from an action client, so the check cannot be forgotten.");
|
|
1154
1266
|
}
|
|
1155
1267
|
// Written from the rows that were just printed rather than recomputed: the
|
|
1156
1268
|
// report and the terminal must not be able to disagree about what happened.
|
|
@@ -1162,22 +1274,29 @@ async function prerenderAfterBundles(bundle, staticDir, assetsDir, knownActions
|
|
|
1162
1274
|
warning: r.warning ?? null,
|
|
1163
1275
|
note: r.note ?? null,
|
|
1164
1276
|
clientJs: sized.get(r.url) ?? null,
|
|
1165
|
-
})), apis.map((a) => ({
|
|
1277
|
+
})), apis.map((a) => ({
|
|
1278
|
+
url: a.url,
|
|
1279
|
+
name: a.name,
|
|
1280
|
+
type: a.type,
|
|
1281
|
+
reason: a.reason,
|
|
1282
|
+
warning: a.warning ?? null,
|
|
1283
|
+
})), audited));
|
|
1166
1284
|
const note = notes(results);
|
|
1167
1285
|
const counted = [...results, ...apis];
|
|
1168
1286
|
console.log(`
|
|
1169
1287
|
${legend(counted)}
|
|
1170
1288
|
|
|
1171
|
-
${summary(counted)}${note ? `\n\n${note}` :
|
|
1289
|
+
${summary(counted)}${note ? `\n\n${note}` : ""}`);
|
|
1172
1290
|
// The table and the report are on disk. Now the refusal, in its own words.
|
|
1173
1291
|
if (refusal)
|
|
1174
1292
|
throw new Error(refusal.message);
|
|
1175
1293
|
if (failed > 0) {
|
|
1176
|
-
throw new Error(`[rsc-kit] ${failed} route${failed === 1 ?
|
|
1177
|
-
|
|
1178
|
-
|
|
1294
|
+
throw new Error(`[rsc-kit] ${failed} route${failed === 1 ? "" : "s"} failed to render.\n` +
|
|
1295
|
+
"Prerendering runs your app: whatever those pages need at render time has to be\n" +
|
|
1296
|
+
"reachable from the build. Give it that, or mark the read with `await connection()`\n" +
|
|
1297
|
+
"so the page renders per request and the rest of it is still stored.");
|
|
1179
1298
|
}
|
|
1180
|
-
if (output ===
|
|
1299
|
+
if (output === "export")
|
|
1181
1300
|
await exportAfterPrerender(results, staticDir, assetsDir);
|
|
1182
1301
|
// The urls whose answer cannot change until the next build. The service
|
|
1183
1302
|
// worker serves these from its cache first rather than asking the network
|
|
@@ -1196,8 +1315,8 @@ ${legend(counted)}
|
|
|
1196
1315
|
const guarded = new Set(manifest.routes.filter((r) => r.middleware?.length).map((r) => r.component));
|
|
1197
1316
|
return {
|
|
1198
1317
|
frozen: results
|
|
1199
|
-
.filter((r) => r.type ===
|
|
1200
|
-
.filter((r) => existsSync(join(staticDir, pathKeyOf(r.url) +
|
|
1318
|
+
.filter((r) => r.type === "frozen" && !guarded.has(r.component))
|
|
1319
|
+
.filter((r) => existsSync(join(staticDir, pathKeyOf(r.url) + ".html")))
|
|
1201
1320
|
.map((r) => r.url),
|
|
1202
1321
|
results,
|
|
1203
1322
|
};
|
|
@@ -1224,7 +1343,9 @@ function weighClientJs(assetsDir) {
|
|
|
1224
1343
|
if (cached !== undefined)
|
|
1225
1344
|
return cached;
|
|
1226
1345
|
const file = join(assetsDir, asset);
|
|
1227
|
-
const bytes = existsSync(file)
|
|
1346
|
+
const bytes = existsSync(file)
|
|
1347
|
+
? gzipSync(readFileSync(file)).byteLength
|
|
1348
|
+
: 0;
|
|
1228
1349
|
weighed.set(asset, bytes);
|
|
1229
1350
|
return bytes;
|
|
1230
1351
|
};
|
|
@@ -1234,10 +1355,10 @@ function weighClientJs(assetsDir) {
|
|
|
1234
1355
|
const named = new Set();
|
|
1235
1356
|
// Split rather than matched: /assets/name.js is the only shape written, and
|
|
1236
1357
|
// a regex over a whole document is the slower half of this function.
|
|
1237
|
-
for (const piece of html.split(
|
|
1358
|
+
for (const piece of html.split("/assets/").slice(1)) {
|
|
1238
1359
|
const name = piece.split(/["'\s)]/)[0];
|
|
1239
|
-
if (name.endsWith(
|
|
1240
|
-
named.add(
|
|
1360
|
+
if (name.endsWith(".js"))
|
|
1361
|
+
named.add("assets/" + name);
|
|
1241
1362
|
}
|
|
1242
1363
|
let total = 0;
|
|
1243
1364
|
for (const asset of named)
|
|
@@ -1261,22 +1382,19 @@ function weighClientJs(assetsDir) {
|
|
|
1261
1382
|
* page that loads and then stays empty forever.
|
|
1262
1383
|
*/
|
|
1263
1384
|
async function exportAfterPrerender(results, staticDir, assetsDir) {
|
|
1264
|
-
const [{ exportSite, NotExportable }, { writeTo, prerenderedFrom, copyAssets }] = await Promise.all([
|
|
1265
|
-
import('./export.js'),
|
|
1266
|
-
import('./files.js'),
|
|
1267
|
-
]);
|
|
1385
|
+
const [{ exportSite, NotExportable }, { writeTo, prerenderedFrom, copyAssets },] = await Promise.all([import("./export.js"), import("./files.js")]);
|
|
1268
1386
|
try {
|
|
1269
1387
|
const { pages, refused } = await exportSite({
|
|
1270
1388
|
results,
|
|
1271
1389
|
read: prerenderedFrom(staticDir),
|
|
1272
1390
|
write: writeTo(exportPath),
|
|
1273
1391
|
manifest: routeManifest(),
|
|
1274
|
-
assets: copyAssets(join(assetsDir,
|
|
1275
|
-
force: process.env.RSC_EXPORT_FORCE ===
|
|
1392
|
+
assets: copyAssets(join(assetsDir, "assets"), exportPath, "/assets/"),
|
|
1393
|
+
force: process.env.RSC_EXPORT_FORCE === "1",
|
|
1276
1394
|
});
|
|
1277
|
-
console.log(`\n Exported ${pages} page${pages === 1 ?
|
|
1395
|
+
console.log(`\n Exported ${pages} page${pages === 1 ? "" : "s"} to ${relative(projectRoot, exportPath)}`);
|
|
1278
1396
|
if (refused.length > 0) {
|
|
1279
|
-
console.log(` Left out ${refused.length}: ${refused.map((r) => r.url).join(
|
|
1397
|
+
console.log(` Left out ${refused.length}: ${refused.map((r) => r.url).join(", ")}`);
|
|
1280
1398
|
}
|
|
1281
1399
|
}
|
|
1282
1400
|
catch (error) {
|
|
@@ -1288,15 +1406,15 @@ async function exportAfterPrerender(results, staticDir, assetsDir) {
|
|
|
1288
1406
|
/** `/posts/[slug]` — the pattern, in the shape the app writes its links in. */
|
|
1289
1407
|
function patternOf(segments) {
|
|
1290
1408
|
if (segments.length === 0)
|
|
1291
|
-
return
|
|
1292
|
-
return (
|
|
1409
|
+
return "/";
|
|
1410
|
+
return ("/" +
|
|
1293
1411
|
segments
|
|
1294
|
-
.map((segment) => segment.type ===
|
|
1412
|
+
.map((segment) => segment.type === "static"
|
|
1295
1413
|
? segment.value
|
|
1296
|
-
: segment.type ===
|
|
1414
|
+
: segment.type === "catchAll"
|
|
1297
1415
|
? `[...${segment.value}]`
|
|
1298
1416
|
: `[${segment.value}]`)
|
|
1299
|
-
.join(
|
|
1417
|
+
.join("/"));
|
|
1300
1418
|
}
|
|
1301
1419
|
/**
|
|
1302
1420
|
* The app's routes as a union, for `@rsc-kit/core/routes` to derive from.
|
|
@@ -1310,8 +1428,12 @@ function patternOf(segments) {
|
|
|
1310
1428
|
* the union twice and imply you could link to a modal.
|
|
1311
1429
|
*/
|
|
1312
1430
|
function renderRouteTypes(manifest) {
|
|
1313
|
-
const patterns = [
|
|
1314
|
-
|
|
1431
|
+
const patterns = [
|
|
1432
|
+
...new Set(manifest.routes.map((route) => patternOf(route.segments))),
|
|
1433
|
+
].sort();
|
|
1434
|
+
const apis = [
|
|
1435
|
+
...new Set((manifest.apis ?? []).map((route) => patternOf(route.segments))),
|
|
1436
|
+
].sort();
|
|
1315
1437
|
// Each pattern to the page module that answers it, as a type-only import
|
|
1316
1438
|
// from the generated file's own directory. SearchExportOf reads the page's
|
|
1317
1439
|
// `searchParams` export, or undefined when there is none, so nothing here
|
|
@@ -1322,82 +1444,114 @@ function renderRouteTypes(manifest) {
|
|
|
1322
1444
|
const pattern = patternOf(route.segments);
|
|
1323
1445
|
if (search.has(pattern))
|
|
1324
1446
|
continue;
|
|
1325
|
-
const target = relative(typesDir, join(sourceDir, route.component)).replace(/\\/g,
|
|
1326
|
-
search.set(pattern, target.startsWith(
|
|
1447
|
+
const target = relative(typesDir, join(sourceDir, route.component)).replace(/\\/g, "/");
|
|
1448
|
+
search.set(pattern, target.startsWith(".") ? target : "./" + target);
|
|
1327
1449
|
}
|
|
1328
1450
|
return [
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1451
|
+
"// @generated — do not edit. Written by the RSC build from the route tree.",
|
|
1452
|
+
"//",
|
|
1453
|
+
"// Turns Link, navigate() and route() into typed apis: an href that no route",
|
|
1454
|
+
"// answers stops compiling. Delete this file and they fall back to `string`,",
|
|
1455
|
+
"// which is what a project that has not built yet gets.",
|
|
1456
|
+
"",
|
|
1457
|
+
"// `export {}` is load-bearing: in a file with no import or export,",
|
|
1458
|
+
"// `declare module` *replaces* the real module rather than augmenting it,",
|
|
1459
|
+
"// and Href and route() vanish from it with no error to explain why.",
|
|
1460
|
+
"export {}",
|
|
1461
|
+
"",
|
|
1340
1462
|
"declare module '@rsc-kit/core/routes' {",
|
|
1341
|
-
|
|
1463
|
+
" interface Register {",
|
|
1342
1464
|
patterns.length > 0
|
|
1343
|
-
?
|
|
1344
|
-
|
|
1465
|
+
? " routes:\n" +
|
|
1466
|
+
patterns.map((p) => " | " + JSON.stringify(p)).join("\n")
|
|
1467
|
+
: " // No routes found under the source directory.\n routes: never",
|
|
1345
1468
|
// The searchParams schema each page exports, read off the module's type.
|
|
1346
1469
|
// This is what types Link's `search` prop and href() per route.
|
|
1347
|
-
|
|
1348
|
-
...[...search]
|
|
1349
|
-
|
|
1350
|
-
|
|
1470
|
+
" search: {",
|
|
1471
|
+
...[...search]
|
|
1472
|
+
.sort()
|
|
1473
|
+
.map(([pattern, target]) => " " +
|
|
1474
|
+
JSON.stringify(pattern) +
|
|
1475
|
+
": SearchExportOf<typeof import(" +
|
|
1476
|
+
JSON.stringify(target) +
|
|
1477
|
+
")>"),
|
|
1478
|
+
" }",
|
|
1479
|
+
" }",
|
|
1351
1480
|
// Api routes are a separate union, so Link refuses an api url and apiUrl()
|
|
1352
1481
|
// refuses a page. Linking to an api route navigates the browser away to a
|
|
1353
1482
|
// json document, which is the mistake worth catching.
|
|
1354
|
-
|
|
1483
|
+
" interface RegisterApi {",
|
|
1355
1484
|
apis.length > 0
|
|
1356
|
-
?
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1485
|
+
? " apis:\n" +
|
|
1486
|
+
apis.map((p) => " | " + JSON.stringify(p)).join("\n")
|
|
1487
|
+
: " // No route.ts files found under the source directory.\n apis: never",
|
|
1488
|
+
" }",
|
|
1489
|
+
"}",
|
|
1490
|
+
"",
|
|
1491
|
+
].join("\n");
|
|
1362
1492
|
}
|
|
1363
1493
|
/** The "use server" module exposing each host function as a plain async call. */
|
|
1364
1494
|
function renderHostActions() {
|
|
1365
1495
|
const lines = [
|
|
1366
1496
|
'"use server";',
|
|
1367
|
-
|
|
1368
|
-
|
|
1497
|
+
"// @generated — do not edit. Written by the RSC build from the host action map.",
|
|
1498
|
+
"",
|
|
1369
1499
|
];
|
|
1370
1500
|
for (const [name, target] of Object.entries(hostActions)) {
|
|
1371
|
-
lines.push(
|
|
1372
|
-
lines.push(
|
|
1373
|
-
|
|
1374
|
-
|
|
1501
|
+
lines.push("export async function " + name + "(...args: unknown[]) {");
|
|
1502
|
+
lines.push(" return await (globalThis as any)." +
|
|
1503
|
+
hostGlobal +
|
|
1504
|
+
"(" +
|
|
1505
|
+
JSON.stringify(target) +
|
|
1506
|
+
", ...args);");
|
|
1507
|
+
lines.push("}");
|
|
1508
|
+
lines.push("");
|
|
1375
1509
|
}
|
|
1376
|
-
return lines.join(
|
|
1510
|
+
return lines.join("\n");
|
|
1377
1511
|
}
|
|
1378
1512
|
/** Ambient declaration for the host global, written beside the app's source. */
|
|
1379
1513
|
function renderHostGlobalTypes() {
|
|
1380
1514
|
return [
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1515
|
+
"// @generated — do not edit.",
|
|
1516
|
+
"//",
|
|
1517
|
+
"// " +
|
|
1518
|
+
hostGlobal +
|
|
1519
|
+
"() is installed on globalThis by the RSC worker, so it has no",
|
|
1520
|
+
"// import to resolve. This declares it for the typechecker; run",
|
|
1521
|
+
"// `tsc --noEmit` to catch calls to a host global that no longer exists.",
|
|
1522
|
+
"//",
|
|
1523
|
+
"// Deliberately not a module — no import/export — so the declaration is",
|
|
1524
|
+
"// global to the project without every file having to reference it.",
|
|
1525
|
+
"",
|
|
1526
|
+
"declare function " +
|
|
1527
|
+
hostGlobal +
|
|
1528
|
+
"<T = unknown>(name: string, ...args: unknown[]): Promise<T>;",
|
|
1529
|
+
"",
|
|
1530
|
+
].join("\n");
|
|
1393
1531
|
}
|
|
1394
1532
|
// ── Discovery ────────────────────────────────────────────────────────────────
|
|
1395
|
-
const ROUTE_FILES = [
|
|
1533
|
+
const ROUTE_FILES = [
|
|
1534
|
+
"page",
|
|
1535
|
+
"layout",
|
|
1536
|
+
"loading",
|
|
1537
|
+
"error",
|
|
1538
|
+
"not-found",
|
|
1539
|
+
"default",
|
|
1540
|
+
"middleware",
|
|
1541
|
+
];
|
|
1396
1542
|
/** The methods a route.ts may export. HEAD and OPTIONS are answered for you. */
|
|
1397
|
-
const API_METHODS = [
|
|
1543
|
+
const API_METHODS = [
|
|
1544
|
+
"GET",
|
|
1545
|
+
"POST",
|
|
1546
|
+
"PUT",
|
|
1547
|
+
"PATCH",
|
|
1548
|
+
"DELETE",
|
|
1549
|
+
"HEAD",
|
|
1550
|
+
"OPTIONS",
|
|
1551
|
+
];
|
|
1398
1552
|
/** `orders.section.tsx` — a region of a page that can be refreshed by name. */
|
|
1399
1553
|
const SECTION_FILE = /\.section\.(tsx|jsx|ts|js)$/;
|
|
1400
|
-
const EXTS = [
|
|
1554
|
+
const EXTS = ["tsx", "jsx", "ts", "js"];
|
|
1401
1555
|
function findRouteFile(dir, base) {
|
|
1402
1556
|
for (const ext of EXTS) {
|
|
1403
1557
|
const p = join(dir, `${base}.${ext}`);
|
|
@@ -1407,11 +1561,11 @@ function findRouteFile(dir, base) {
|
|
|
1407
1561
|
return null;
|
|
1408
1562
|
}
|
|
1409
1563
|
function componentName(absPath) {
|
|
1410
|
-
const rel = relative(sourceDir, absPath).replace(/\\/g,
|
|
1411
|
-
return rel.replace(/\.(tsx|jsx|ts|js)$/,
|
|
1564
|
+
const rel = relative(sourceDir, absPath).replace(/\\/g, "/");
|
|
1565
|
+
return rel.replace(/\.(tsx|jsx|ts|js)$/, "");
|
|
1412
1566
|
}
|
|
1413
1567
|
function toAlias(name) {
|
|
1414
|
-
return
|
|
1568
|
+
return "_c_" + name.replace(/[^a-zA-Z0-9]/g, "_");
|
|
1415
1569
|
}
|
|
1416
1570
|
const components = new Map();
|
|
1417
1571
|
/**
|
|
@@ -1442,8 +1596,8 @@ function register(absPath) {
|
|
|
1442
1596
|
*/
|
|
1443
1597
|
function registerApiRoute(absPath) {
|
|
1444
1598
|
const name = componentName(absPath);
|
|
1445
|
-
const source = readFileSync(absPath,
|
|
1446
|
-
const methods = API_METHODS.filter((method) => new RegExp(`^\\s*export\\s+(?:async\\s+function|function|const|let|var)\\s+${method}\\b`,
|
|
1599
|
+
const source = readFileSync(absPath, "utf-8");
|
|
1600
|
+
const methods = API_METHODS.filter((method) => new RegExp(`^\\s*export\\s+(?:async\\s+function|function|const|let|var)\\s+${method}\\b`, "m").test(source));
|
|
1447
1601
|
if (methods.length === 0) {
|
|
1448
1602
|
// `route.ts` is also where a host declares the guards for everything below
|
|
1449
1603
|
// it — the file predates api routes and is still read that way. One that
|
|
@@ -1453,7 +1607,7 @@ function registerApiRoute(absPath) {
|
|
|
1453
1607
|
return;
|
|
1454
1608
|
throw new Error(`[rsc-kit] ${relative(projectRoot, absPath)} exports no request methods.\n` +
|
|
1455
1609
|
` Export one named for the method it answers — export function GET(request: Request) — ` +
|
|
1456
|
-
`or delete the file. One of: ${API_METHODS.join(
|
|
1610
|
+
`or delete the file. One of: ${API_METHODS.join(", ")}.`);
|
|
1457
1611
|
}
|
|
1458
1612
|
apiRoutes.set(name, { name, absPath, methods });
|
|
1459
1613
|
}
|
|
@@ -1466,7 +1620,7 @@ function discover(dir) {
|
|
|
1466
1620
|
// route.ts — an api endpoint, colocated with the pages it sits among. Read
|
|
1467
1621
|
// for its method exports here rather than at request time, so a route that
|
|
1468
1622
|
// exports nothing callable is a build error instead of a 404 nobody explains.
|
|
1469
|
-
const api = findRouteFile(dir,
|
|
1623
|
+
const api = findRouteFile(dir, "route");
|
|
1470
1624
|
if (api)
|
|
1471
1625
|
registerApiRoute(api);
|
|
1472
1626
|
// Named regions. Registered like any other component so the generated entry
|
|
@@ -1492,25 +1646,12 @@ function discover(dir) {
|
|
|
1492
1646
|
* for almost every route in an app.
|
|
1493
1647
|
*/
|
|
1494
1648
|
function metadataExports(absPath) {
|
|
1495
|
-
const src = readFileSync(absPath,
|
|
1649
|
+
const src = readFileSync(absPath, "utf-8");
|
|
1496
1650
|
return {
|
|
1497
1651
|
static: /export\s+const\s+metadata\b/.test(src),
|
|
1498
1652
|
generate: /export\s+(async\s+)?function\s+generateMetadata\b/.test(src),
|
|
1499
1653
|
};
|
|
1500
1654
|
}
|
|
1501
|
-
/**
|
|
1502
|
-
* Whether a route ships the client runtime at all.
|
|
1503
|
-
*
|
|
1504
|
-
* Opting out buys back everything React costs on a page that has nothing to
|
|
1505
|
-
* hydrate — react-dom alone is most of it. Declared rather than inferred: a
|
|
1506
|
-
* page with no client components today may gain one tomorrow, and the build
|
|
1507
|
-
* refusing that is the point.
|
|
1508
|
-
*/
|
|
1509
|
-
function shipsClientJs(absPath) {
|
|
1510
|
-
const src = readFileSync(absPath, 'utf-8');
|
|
1511
|
-
const declared = /export\s+const\s+clientJs\s*(:[^=]+)?=\s*(true|false)/.exec(src);
|
|
1512
|
-
return declared ? declared[2] === 'true' : 'auto';
|
|
1513
|
-
}
|
|
1514
1655
|
/**
|
|
1515
1656
|
* Which urls exist for a parameterised route.
|
|
1516
1657
|
*
|
|
@@ -1521,7 +1662,7 @@ function shipsClientJs(absPath) {
|
|
|
1521
1662
|
* inferred because there is nothing to infer it from.
|
|
1522
1663
|
*/
|
|
1523
1664
|
function hasStaticParams(absPath) {
|
|
1524
|
-
const src = readFileSync(absPath,
|
|
1665
|
+
const src = readFileSync(absPath, "utf-8");
|
|
1525
1666
|
return /export\s+((async\s+)?function\s+generateStaticParams|const\s+generateStaticParams)/.test(src);
|
|
1526
1667
|
}
|
|
1527
1668
|
/**
|
|
@@ -1537,7 +1678,7 @@ function hasStaticParams(absPath) {
|
|
|
1537
1678
|
* import for something that can never validate.
|
|
1538
1679
|
*/
|
|
1539
1680
|
function urlSchemaExports(absPath) {
|
|
1540
|
-
const src = readFileSync(absPath,
|
|
1681
|
+
const src = readFileSync(absPath, "utf-8");
|
|
1541
1682
|
return {
|
|
1542
1683
|
params: /export\s+const\s+params\s*[=:]/.test(src),
|
|
1543
1684
|
searchParams: /export\s+const\s+searchParams\s*[=:]/.test(src),
|
|
@@ -1659,7 +1800,7 @@ const FALLBACK_BODY = ` const answer = await devHandler(request)
|
|
|
1659
1800
|
* the failure is silent: the server finds no directory, decides nothing was
|
|
1660
1801
|
* frozen, and renders every page live exactly as it did before.
|
|
1661
1802
|
*/
|
|
1662
|
-
const NITRO_STATIC_DIR =
|
|
1803
|
+
const NITRO_STATIC_DIR = "rsc-static";
|
|
1663
1804
|
/**
|
|
1664
1805
|
* Serve what the build froze.
|
|
1665
1806
|
*
|
|
@@ -1698,15 +1839,15 @@ function installHostCallsOnce(): void {
|
|
|
1698
1839
|
}
|
|
1699
1840
|
|
|
1700
1841
|
`;
|
|
1701
|
-
function generateEntryRsc(fallbackOrigin =
|
|
1842
|
+
function generateEntryRsc(fallbackOrigin = "") {
|
|
1702
1843
|
// The 404 page, if the app has one, and the layouts it renders inside.
|
|
1703
1844
|
// Computed here rather than looked up at runtime: not-found is not a route,
|
|
1704
1845
|
// so the manifest has no entry to read its chain from.
|
|
1705
|
-
const notFoundComponent = [...components.keys()].find((name) => name.endsWith(
|
|
1846
|
+
const notFoundComponent = [...components.keys()].find((name) => name.endsWith("/not-found"));
|
|
1706
1847
|
const notFoundLayouts = notFoundComponent
|
|
1707
1848
|
? [...components.keys()]
|
|
1708
|
-
.filter((name) => name.endsWith(
|
|
1709
|
-
notFoundComponent.startsWith(name.slice(0, -
|
|
1849
|
+
.filter((name) => name.endsWith("/layout") &&
|
|
1850
|
+
notFoundComponent.startsWith(name.slice(0, -"layout".length)))
|
|
1710
1851
|
.sort((a, b) => a.length - b.length)
|
|
1711
1852
|
: [];
|
|
1712
1853
|
const imports = [];
|
|
@@ -1731,7 +1872,7 @@ function generateEntryRsc(fallbackOrigin = '') {
|
|
|
1731
1872
|
meta.static ? `static: ${c.alias}_meta.metadata` : null,
|
|
1732
1873
|
meta.generate ? `generate: ${c.alias}_meta.generateMetadata` : null,
|
|
1733
1874
|
].filter(Boolean);
|
|
1734
|
-
metaEntries.push(` ${JSON.stringify(c.name)}: { ${fields.join(
|
|
1875
|
+
metaEntries.push(` ${JSON.stringify(c.name)}: { ${fields.join(", ")} },`);
|
|
1735
1876
|
}
|
|
1736
1877
|
if (hasStaticParams(c.absPath)) {
|
|
1737
1878
|
// The namespace import may already be in place for metadata; a second
|
|
@@ -1744,9 +1885,11 @@ function generateEntryRsc(fallbackOrigin = '') {
|
|
|
1744
1885
|
imports.push(`import * as ${c.alias}_schema from ${JSON.stringify(c.absPath)}`);
|
|
1745
1886
|
const fields = [
|
|
1746
1887
|
urlSchemas.params ? `params: ${c.alias}_schema.params` : null,
|
|
1747
|
-
urlSchemas.searchParams
|
|
1888
|
+
urlSchemas.searchParams
|
|
1889
|
+
? `searchParams: ${c.alias}_schema.searchParams`
|
|
1890
|
+
: null,
|
|
1748
1891
|
].filter(Boolean);
|
|
1749
|
-
schemaEntries.push(` ${JSON.stringify(c.name)}: { ${fields.join(
|
|
1892
|
+
schemaEntries.push(` ${JSON.stringify(c.name)}: { ${fields.join(", ")} },`);
|
|
1750
1893
|
}
|
|
1751
1894
|
}
|
|
1752
1895
|
// The engine's own modules are named without an extension: this plugin runs
|
|
@@ -1765,21 +1908,21 @@ import { notFoundDigest, isNotFoundSignal } from ${JSON.stringify(join(packageDi
|
|
|
1765
1908
|
import { noteRequestRead } from ${JSON.stringify(join(packageDir, "request"))}
|
|
1766
1909
|
import { redirectDigest } from ${JSON.stringify(join(packageDir, "redirectDigest"))}
|
|
1767
1910
|
import { createRscHandler } from ${JSON.stringify(join(packageDir, "host"))}
|
|
1768
|
-
import { httpHostCalls } from ${JSON.stringify(join(packageDir,
|
|
1769
|
-
import { prerenderedBeside } from ${JSON.stringify(join(packageDir,
|
|
1911
|
+
import { httpHostCalls } from ${JSON.stringify(join(packageDir, "hostCalls"))}
|
|
1912
|
+
import { prerenderedBeside } from ${JSON.stringify(join(packageDir, "files"))}
|
|
1770
1913
|
import { renderToReadableStream, decodeReply, loadServerAction } from '@vitejs/plugin-rsc/rsc'
|
|
1771
|
-
import { isQuery, queryCacheControl, isQueryValidationError } from ${JSON.stringify(join(packageDir,
|
|
1772
|
-
import { isActionValidationError, isClientBuilt } from ${JSON.stringify(join(packageDir,
|
|
1914
|
+
import { isQuery, queryCacheControl, isQueryValidationError } from ${JSON.stringify(join(packageDir, "query"))}
|
|
1915
|
+
import { isActionValidationError, isClientBuilt } from ${JSON.stringify(join(packageDir, "action"))}
|
|
1773
1916
|
import { Suspense, createElement, Fragment } from 'react'
|
|
1774
1917
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
1775
|
-
${imports.join(
|
|
1918
|
+
${imports.join("\n")}
|
|
1776
1919
|
|
|
1777
1920
|
type HostFn = (name: string, ...args: unknown[]) => Promise<unknown>
|
|
1778
1921
|
type LayoutEntry = { component: string; props?: Record<string, unknown> }
|
|
1779
1922
|
type SlotOverride = { component: string; props?: Record<string, unknown> }
|
|
1780
1923
|
|
|
1781
1924
|
const components: Record<string, any> = {
|
|
1782
|
-
${mapEntries.join(
|
|
1925
|
+
${mapEntries.join("\n")}
|
|
1783
1926
|
}
|
|
1784
1927
|
|
|
1785
1928
|
/**
|
|
@@ -1799,16 +1942,21 @@ ${mapEntries.join('\n')}
|
|
|
1799
1942
|
const APP_HEAD: { tag: any; props: Record<string, string> }[] = ${JSON.stringify(headTags(foundAssets))}
|
|
1800
1943
|
|
|
1801
1944
|
const WEB_MANIFEST: { href: string; themeColor?: string } | null = ${JSON.stringify(webManifestOptions
|
|
1802
|
-
? {
|
|
1945
|
+
? {
|
|
1946
|
+
href: MANIFEST_PATH,
|
|
1947
|
+
...(webManifestOptions.themeColor
|
|
1948
|
+
? { themeColor: webManifestOptions.themeColor }
|
|
1949
|
+
: {}),
|
|
1950
|
+
}
|
|
1803
1951
|
: null)}
|
|
1804
1952
|
|
|
1805
1953
|
const urlSchemas: Record<string, { params?: any; searchParams?: any }> = {
|
|
1806
|
-
${schemaEntries.join(
|
|
1954
|
+
${schemaEntries.join("\n")}
|
|
1807
1955
|
}
|
|
1808
1956
|
|
|
1809
1957
|
/** route.ts modules, by the name the manifest matched. */
|
|
1810
1958
|
const apiRoutes: Record<string, any> = {
|
|
1811
|
-
${apiEntries.join(
|
|
1959
|
+
${apiEntries.join("\n")}
|
|
1812
1960
|
}
|
|
1813
1961
|
|
|
1814
1962
|
/**
|
|
@@ -1944,11 +2092,11 @@ export async function handleApiRoute(
|
|
|
1944
2092
|
}
|
|
1945
2093
|
|
|
1946
2094
|
const metadataMap: Record<string, { static?: any; generate?: (p: any) => any }> = {
|
|
1947
|
-
${metaEntries.join(
|
|
2095
|
+
${metaEntries.join("\n")}
|
|
1948
2096
|
}
|
|
1949
2097
|
|
|
1950
2098
|
const staticParamsMap: Record<string, () => any> = {
|
|
1951
|
-
${paramEntries.join(
|
|
2099
|
+
${paramEntries.join("\n")}
|
|
1952
2100
|
}
|
|
1953
2101
|
|
|
1954
2102
|
/**
|
|
@@ -3478,7 +3626,7 @@ export async function handleRscPprShell(
|
|
|
3478
3626
|
* server rendering live pages it had already frozen. The gate is the mode, not
|
|
3479
3627
|
* the file.
|
|
3480
3628
|
*/
|
|
3481
|
-
${fallbackOrigin ? FALLBACK_CONSTS.replace(
|
|
3629
|
+
${fallbackOrigin ? FALLBACK_CONSTS.replace("__ORIGIN__", JSON.stringify(fallbackOrigin)) : ""}${NITRO_HOST_CALLS}
|
|
3482
3630
|
let devHandler: ((request: Request) => Promise<Response | null>) | null = null
|
|
3483
3631
|
|
|
3484
3632
|
export default async function handler(request: Request): Promise<Response> {
|
|
@@ -3502,7 +3650,8 @@ export default async function handler(request: Request): Promise<Response> {
|
|
|
3502
3650
|
resolveMetadata,
|
|
3503
3651
|
runRouteMiddleware,
|
|
3504
3652
|
} as never,
|
|
3505
|
-
${NITRO_HANDLER_OPTIONS}${NITRO_PRERENDERED}
|
|
3653
|
+
${NITRO_HANDLER_OPTIONS}${NITRO_PRERENDERED} maxActionBody: ${maxActionBody === undefined ? "undefined" : String(maxActionBody)},
|
|
3654
|
+
})
|
|
3506
3655
|
|
|
3507
3656
|
${fallbackOrigin ? FALLBACK_BODY : " return (await devHandler(request)) ?? (await notFound())\n"}}
|
|
3508
3657
|
|
|
@@ -3516,7 +3665,8 @@ ${fallbackOrigin ? FALLBACK_BODY : " return (await devHandler(request)) ?? (awa
|
|
|
3516
3665
|
* Without a not-found.tsx this is the string it always was.
|
|
3517
3666
|
*/
|
|
3518
3667
|
async function notFound(): Promise<Response> {
|
|
3519
|
-
${notFoundComponent
|
|
3668
|
+
${notFoundComponent
|
|
3669
|
+
? `
|
|
3520
3670
|
try {
|
|
3521
3671
|
const { htmlStream } = await handleRscHtmlStream(
|
|
3522
3672
|
${JSON.stringify(notFoundComponent)},
|
|
@@ -3537,13 +3687,14 @@ async function notFound(): Promise<Response> {
|
|
|
3537
3687
|
// A 404 page that throws is still a 404. Falling back rather than
|
|
3538
3688
|
// answering 500 keeps the status honest about what happened.
|
|
3539
3689
|
}
|
|
3540
|
-
`
|
|
3690
|
+
`
|
|
3691
|
+
: ""}
|
|
3541
3692
|
return new Response('Not found', { status: 404 })
|
|
3542
3693
|
}
|
|
3543
3694
|
`;
|
|
3544
3695
|
}
|
|
3545
3696
|
function generateEntrySsr() {
|
|
3546
|
-
const devUrls = join(packageDir,
|
|
3697
|
+
const devUrls = join(packageDir, "devUrls");
|
|
3547
3698
|
return `// GENERATED by rscKit() — do not edit.
|
|
3548
3699
|
import { createFromReadableStream } from '@vitejs/plugin-rsc/ssr'
|
|
3549
3700
|
import { renderToReadableStream, resume } from 'react-dom/server.edge'
|
|
@@ -3670,7 +3821,7 @@ export default {
|
|
|
3670
3821
|
}
|
|
3671
3822
|
`;
|
|
3672
3823
|
function generateEntryBrowser() {
|
|
3673
|
-
const clientBootstrap = join(packageDir,
|
|
3824
|
+
const clientBootstrap = join(packageDir, "js/createViteRscApp");
|
|
3674
3825
|
// Only for an exported build, and only what the client needs to work out how
|
|
3675
3826
|
// much of a page to ask for: a file server sends no headers, so without this
|
|
3676
3827
|
// every navigation takes the whole document and replaces the root. Inlined
|
|
@@ -3678,9 +3829,12 @@ function generateEntryBrowser() {
|
|
|
3678
3829
|
// a server answers this, and shipping a route table to every browser for
|
|
3679
3830
|
// nothing is a page-weight cost with no benefit.
|
|
3680
3831
|
const routesForClient = staticPayloads
|
|
3681
|
-
? routeManifest().routes.map((route) => ({
|
|
3832
|
+
? routeManifest().routes.map((route) => ({
|
|
3833
|
+
segments: route.segments,
|
|
3834
|
+
layouts: route.layouts,
|
|
3835
|
+
}))
|
|
3682
3836
|
: null;
|
|
3683
|
-
const refreshModule = join(packageDir,
|
|
3837
|
+
const refreshModule = join(packageDir, "js/navigate");
|
|
3684
3838
|
return `// GENERATED by rscKit() — do not edit.
|
|
3685
3839
|
import { createViteRscApp } from ${JSON.stringify(clientBootstrap)}
|
|
3686
3840
|
import { refresh } from ${JSON.stringify(refreshModule)}
|
|
@@ -3726,7 +3880,7 @@ if ('serviceWorker' in navigator && import.meta.env.PROD) {
|
|
|
3726
3880
|
})
|
|
3727
3881
|
}
|
|
3728
3882
|
`
|
|
3729
|
-
:
|
|
3883
|
+
: ""}`;
|
|
3730
3884
|
}
|
|
3731
3885
|
/**
|
|
3732
3886
|
* Intercepted URL patterns, published by the host before the build.
|
|
@@ -3748,10 +3902,10 @@ if ('serviceWorker' in navigator && import.meta.env.PROD) {
|
|
|
3748
3902
|
function interceptManifest() {
|
|
3749
3903
|
return routeManifest().intercepts.map((entry) => ({
|
|
3750
3904
|
// The client writes [id] where a Laravel route writes {id}.
|
|
3751
|
-
urlPattern:
|
|
3905
|
+
urlPattern: "/" +
|
|
3752
3906
|
entry.segments
|
|
3753
|
-
.map((seg) =>
|
|
3754
|
-
.join(
|
|
3907
|
+
.map((seg) => seg.type === "static" ? seg.value : "[" + seg.value + "]")
|
|
3908
|
+
.join("/"),
|
|
3755
3909
|
slot: entry.slot,
|
|
3756
3910
|
}));
|
|
3757
3911
|
}
|
|
@@ -3772,9 +3926,9 @@ function defaultExportBody(source) {
|
|
|
3772
3926
|
const start = match.index + match[0].length - 1;
|
|
3773
3927
|
for (let i = start; i < source.length; i++) {
|
|
3774
3928
|
const ch = source[i];
|
|
3775
|
-
if (ch ===
|
|
3929
|
+
if (ch === "{")
|
|
3776
3930
|
depth++;
|
|
3777
|
-
else if (ch ===
|
|
3931
|
+
else if (ch === "}" && --depth === 0)
|
|
3778
3932
|
return source.slice(start, i + 1);
|
|
3779
3933
|
}
|
|
3780
3934
|
return null;
|
|
@@ -3797,7 +3951,7 @@ function pageBlocksOnHostCall(source) {
|
|
|
3797
3951
|
const body = defaultExportBody(source);
|
|
3798
3952
|
// Matches `await rpc(`, and the explicitly-qualified forms a typed codebase
|
|
3799
3953
|
// may use: `await globalThis.rpc(` / `await (globalThis as any).rpc(`.
|
|
3800
|
-
const qualifier =
|
|
3954
|
+
const qualifier = "(?:\\(\\s*globalThis[^)]*\\)\\s*\\.\\s*|globalThis\\s*\\.\\s*)?";
|
|
3801
3955
|
const awaited = new RegExp(`\\bawait\\s+${qualifier}${hostGlobal}\\s*[<(]`);
|
|
3802
3956
|
return body !== null && awaited.test(body);
|
|
3803
3957
|
}
|
|
@@ -3805,7 +3959,7 @@ function pageBlocksOnHostCall(source) {
|
|
|
3805
3959
|
function hasLoadingInChain(pageDir) {
|
|
3806
3960
|
let dir = pageDir;
|
|
3807
3961
|
while (dir.startsWith(appDir)) {
|
|
3808
|
-
if (findRouteFile(dir,
|
|
3962
|
+
if (findRouteFile(dir, "loading"))
|
|
3809
3963
|
return true;
|
|
3810
3964
|
if (dir === appDir)
|
|
3811
3965
|
break;
|
|
@@ -3832,9 +3986,9 @@ function hasLoadingInChain(pageDir) {
|
|
|
3832
3986
|
function validateErrorBoundaries() {
|
|
3833
3987
|
const wrong = [];
|
|
3834
3988
|
for (const c of components.values()) {
|
|
3835
|
-
if (!c.name.endsWith(
|
|
3989
|
+
if (!c.name.endsWith("/error"))
|
|
3836
3990
|
continue;
|
|
3837
|
-
const source = readFileSync(c.absPath,
|
|
3991
|
+
const source = readFileSync(c.absPath, "utf-8");
|
|
3838
3992
|
if (!/^\s*['"]use client['"]/m.test(source)) {
|
|
3839
3993
|
wrong.push(` ${relative(projectRoot, c.absPath)}`);
|
|
3840
3994
|
}
|
|
@@ -3844,17 +3998,20 @@ function validateErrorBoundaries() {
|
|
|
3844
3998
|
function validateLoadingBoundaries() {
|
|
3845
3999
|
const errors = [];
|
|
3846
4000
|
for (const c of components.values()) {
|
|
3847
|
-
if (!c.name.endsWith(
|
|
4001
|
+
if (!c.name.endsWith("/page") && c.name !== "app/page")
|
|
3848
4002
|
continue;
|
|
3849
4003
|
const pageDir = dirname(c.absPath);
|
|
3850
|
-
const source = readFileSync(c.absPath,
|
|
4004
|
+
const source = readFileSync(c.absPath, "utf-8");
|
|
3851
4005
|
let reason = null;
|
|
3852
4006
|
if (pageBlocksOnHostCall(source)) {
|
|
3853
4007
|
reason = `its default export awaits ${hostGlobal}()`;
|
|
3854
4008
|
}
|
|
3855
4009
|
else {
|
|
3856
4010
|
const configPath = routeConfig ? join(pageDir, routeConfig.file) : null;
|
|
3857
|
-
if (routeConfig &&
|
|
4011
|
+
if (routeConfig &&
|
|
4012
|
+
configPath &&
|
|
4013
|
+
existsSync(configPath) &&
|
|
4014
|
+
routeConfig.dynamicPattern.test(readFileSync(configPath, "utf-8"))) {
|
|
3858
4015
|
reason = `${routeConfig.file} resolves props dynamically`;
|
|
3859
4016
|
}
|
|
3860
4017
|
}
|
|
@@ -3867,10 +4024,42 @@ function validateLoadingBoundaries() {
|
|
|
3867
4024
|
// ── Plugin ───────────────────────────────────────────────────────────────────
|
|
3868
4025
|
/** Names of plugins that transform JSX and must run after rsc() has split it. */
|
|
3869
4026
|
const JSX_PLUGIN_PATTERN = /react|babel|oxc/i;
|
|
4027
|
+
/**
|
|
4028
|
+
* The server-side stand-in for a client export, made extendable.
|
|
4029
|
+
*
|
|
4030
|
+
* In the rsc environment plugin-rsc replaces every export of a "use client"
|
|
4031
|
+
* module with a stub that throws when called. It emits the stub as an arrow
|
|
4032
|
+
* function, and an arrow function has no prototype, so a server module that
|
|
4033
|
+
* does `class NullStore extends ReactStore` - base-ui does, with only the
|
|
4034
|
+
* base class marked "use client" - dies at the class declaration with
|
|
4035
|
+
* "The superclass is not a constructor", on a line nobody wrote, whenever a
|
|
4036
|
+
* server component imports the library directly. Next.js allows that import;
|
|
4037
|
+
* React's own client references are proxies over a real function.
|
|
4038
|
+
*
|
|
4039
|
+
* A `function` stub has a prototype, so the declaration succeeds, and the
|
|
4040
|
+
* stub still throws - now from `super()`, with its own message - if the
|
|
4041
|
+
* class is ever constructed on the server.
|
|
4042
|
+
*/
|
|
4043
|
+
const CLIENT_STUB = /registerClientReference\(\s*\(\) => \{ throw new Error\("Unexpectedly client reference export '"/g;
|
|
4044
|
+
function extendableClientReferences() {
|
|
4045
|
+
return {
|
|
4046
|
+
name: "rsc-kit:extendable-client-references",
|
|
4047
|
+
enforce: "post",
|
|
4048
|
+
applyToEnvironment: (environment) => environment.name === "rsc",
|
|
4049
|
+
transform(code) {
|
|
4050
|
+
if (!code.includes("Unexpectedly client reference export"))
|
|
4051
|
+
return;
|
|
4052
|
+
return {
|
|
4053
|
+
code: code.replace(CLIENT_STUB, 'registerClientReference(function () { throw new Error("Unexpectedly client reference export \'"'),
|
|
4054
|
+
map: null,
|
|
4055
|
+
};
|
|
4056
|
+
},
|
|
4057
|
+
};
|
|
4058
|
+
}
|
|
3870
4059
|
export function rscKit(options = {}) {
|
|
3871
4060
|
resolvePaths(options);
|
|
3872
4061
|
const routesPlugin = {
|
|
3873
|
-
name:
|
|
4062
|
+
name: "rsc-kit",
|
|
3874
4063
|
config(_config, env) {
|
|
3875
4064
|
if (!existsSync(appDir)) {
|
|
3876
4065
|
throw new Error(`[rsc-kit] No app directory at ${appDir} — nothing to build.`);
|
|
@@ -3889,24 +4078,24 @@ export function rscKit(options = {}) {
|
|
|
3889
4078
|
// editing is a state to pass through, not to be thrown out of.
|
|
3890
4079
|
if (components.size === 0) {
|
|
3891
4080
|
const message = `No routes under ${appDir}. A directory with a page.tsx in it is a route; ` +
|
|
3892
|
-
|
|
3893
|
-
if (env.command ===
|
|
4081
|
+
"without one there is nothing to serve.";
|
|
4082
|
+
if (env.command === "build")
|
|
3894
4083
|
throw new Error(`[rsc-kit] ${message}`);
|
|
3895
4084
|
log(message);
|
|
3896
4085
|
}
|
|
3897
4086
|
const notClient = validateErrorBoundaries();
|
|
3898
4087
|
if (notClient.length) {
|
|
3899
|
-
throw new Error(
|
|
3900
|
-
notClient.join(
|
|
4088
|
+
throw new Error("[rsc-kit] An error.tsx must be a client component.\n\n" +
|
|
4089
|
+
notClient.join("\n") +
|
|
3901
4090
|
"\n\nAdd 'use client' at the top. It is rendered inside an error boundary and is\n" +
|
|
3902
|
-
|
|
4091
|
+
"handed a reset() callback to call, neither of which a server component can do.");
|
|
3903
4092
|
}
|
|
3904
4093
|
const loadingErrors = validateLoadingBoundaries();
|
|
3905
4094
|
if (loadingErrors.length) {
|
|
3906
|
-
throw new Error(
|
|
3907
|
-
loadingErrors.join(
|
|
3908
|
-
|
|
3909
|
-
|
|
4095
|
+
throw new Error("[rsc-kit] A page that blocks before it can paint needs a loading.tsx boundary.\n\n" +
|
|
4096
|
+
loadingErrors.join("\n") +
|
|
4097
|
+
"\n\nAdd loading.tsx in the page directory (or a parent), or move the slow work\n" +
|
|
4098
|
+
"into a child component wrapped in its own <Suspense> so the page can paint.");
|
|
3910
4099
|
}
|
|
3911
4100
|
// Before the entries, because the app's own source imports these and the
|
|
3912
4101
|
// module graph is walked as soon as this hook returns.
|
|
@@ -3919,7 +4108,7 @@ export function rscKit(options = {}) {
|
|
|
3919
4108
|
// host calls resolve their endpoint, from the app's own .env, so the two
|
|
3920
4109
|
// cannot end up pointing at different backends. Development only: a
|
|
3921
4110
|
// build's server.ts decides this for itself.
|
|
3922
|
-
const backendEnv = loadEnv(env.mode, projectRoot,
|
|
4111
|
+
const backendEnv = loadEnv(env.mode, projectRoot, "");
|
|
3923
4112
|
// Only a backend this server could actually call. The shared secret is
|
|
3924
4113
|
// what makes one a backend rather than a url that happens to be in the
|
|
3925
4114
|
// environment — APP_URL is not a Laravel-only name, and a JavaScript
|
|
@@ -3931,16 +4120,19 @@ export function rscKit(options = {}) {
|
|
|
3931
4120
|
// the address down means it.
|
|
3932
4121
|
const backendSecret = hostCallOptions?.secret ?? backendEnv.RSC_HOST_CALL_SECRET;
|
|
3933
4122
|
const detected = backendSecret
|
|
3934
|
-
? (hostCallOptions?.endpoint ??
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
4123
|
+
? (hostCallOptions?.endpoint ??
|
|
4124
|
+
backendEnv.RSC_BACKEND ??
|
|
4125
|
+
backendEnv.APP_URL ??
|
|
4126
|
+
"")
|
|
4127
|
+
: "";
|
|
4128
|
+
const fallbackOrigin = options.devFallback === false ? "" : (options.devFallback ?? detected);
|
|
4129
|
+
writeFileSync(join(genDir, "entry.rsc.tsx"), generateEntryRsc(fallbackOrigin));
|
|
4130
|
+
writeFileSync(join(genDir, "entry.ssr.tsx"), generateEntrySsr());
|
|
4131
|
+
writeFileSync(join(genDir, "entry.browser.tsx"), generateEntryBrowser());
|
|
3940
4132
|
// Written beside the entries, for a host to read instead of walking the
|
|
3941
4133
|
// route tree itself. Laravel scans it a second time today; a JS host
|
|
3942
4134
|
// would otherwise have to write a third walk of the same directories.
|
|
3943
|
-
writeFileSync(join(outDir,
|
|
4135
|
+
writeFileSync(join(outDir, "routes.json"), JSON.stringify(manifest, null, 2));
|
|
3944
4136
|
return {
|
|
3945
4137
|
// Off, not merely unused: Vite warns when publicDir sits inside outDir,
|
|
3946
4138
|
// and assetsDir is normally a directory under the build output. An app
|
|
@@ -3961,7 +4153,7 @@ export function rscKit(options = {}) {
|
|
|
3961
4153
|
* answer travels with the bundle instead of with whoever starts it.
|
|
3962
4154
|
*/
|
|
3963
4155
|
define: {
|
|
3964
|
-
|
|
4156
|
+
"process.env.NODE_ENV": JSON.stringify(env.mode === "development" ? "development" : "production"),
|
|
3965
4157
|
// A constant, so the boundary and its import fall out of the bundle
|
|
3966
4158
|
// entirely when this is off rather than shipping a branch nobody takes.
|
|
3967
4159
|
__RSC_VIEW_TRANSITIONS__: JSON.stringify(viewTransitions),
|
|
@@ -3998,7 +4190,7 @@ export function rscKit(options = {}) {
|
|
|
3998
4190
|
// "The server is configured with a public base URL", which reads as a
|
|
3999
4191
|
// routing bug rather than as this line. In dev the pages are the root;
|
|
4000
4192
|
// the assets come from the same origin either way.
|
|
4001
|
-
base:
|
|
4193
|
+
base: "/",
|
|
4002
4194
|
// Where the generated entries live, so Vite resolves them as its own
|
|
4003
4195
|
// source. A build concern only.
|
|
4004
4196
|
//
|
|
@@ -4018,7 +4210,7 @@ export function rscKit(options = {}) {
|
|
|
4018
4210
|
// built bundles reference it zero times — deduping it was a no-op left
|
|
4019
4211
|
// over from the hand-rolled engine.
|
|
4020
4212
|
resolve: {
|
|
4021
|
-
dedupe: [
|
|
4213
|
+
dedupe: ["react", "react-dom", "@vitejs/plugin-rsc"],
|
|
4022
4214
|
// `import Link from '<packageAlias>/Link'` resolves to the client
|
|
4023
4215
|
// runtime shipped here, for hosts that vendor this package outside
|
|
4024
4216
|
// node_modules. Installed from npm the name resolves on its own.
|
|
@@ -4026,15 +4218,35 @@ export function rscKit(options = {}) {
|
|
|
4026
4218
|
},
|
|
4027
4219
|
build: { emptyOutDir: true },
|
|
4028
4220
|
environments: {
|
|
4029
|
-
// Server bundles — stay under the (non-public) out dir.
|
|
4030
|
-
|
|
4031
|
-
|
|
4221
|
+
// Server bundles — stay under the (non-public) out dir. `bun` and
|
|
4222
|
+
// `bun:*` are the runtime's own modules, like `node:*`: nothing to
|
|
4223
|
+
// bundle, and a build under Node has nothing to resolve them to.
|
|
4224
|
+
// Left as imports for the runtime that has them.
|
|
4225
|
+
rsc: {
|
|
4226
|
+
build: {
|
|
4227
|
+
rollupOptions: {
|
|
4228
|
+
input: { index: join(genDir, "entry.rsc.tsx") },
|
|
4229
|
+
external: RUNTIME_BUILTINS,
|
|
4230
|
+
},
|
|
4231
|
+
},
|
|
4232
|
+
},
|
|
4233
|
+
ssr: {
|
|
4234
|
+
build: {
|
|
4235
|
+
rollupOptions: {
|
|
4236
|
+
input: { index: join(genDir, "entry.ssr.tsx") },
|
|
4237
|
+
external: RUNTIME_BUILTINS,
|
|
4238
|
+
},
|
|
4239
|
+
},
|
|
4240
|
+
},
|
|
4032
4241
|
// Client bundle — emitted into public/ for the web server to serve.
|
|
4033
4242
|
client: {
|
|
4034
4243
|
build: {
|
|
4035
4244
|
outDir: publicAssetsDir,
|
|
4036
4245
|
emptyOutDir: true,
|
|
4037
|
-
rollupOptions: {
|
|
4246
|
+
rollupOptions: {
|
|
4247
|
+
input: { index: join(genDir, "entry.browser.tsx") },
|
|
4248
|
+
output: { chunkFileNames: clientChunkFileName },
|
|
4249
|
+
},
|
|
4038
4250
|
},
|
|
4039
4251
|
},
|
|
4040
4252
|
},
|
|
@@ -4061,22 +4273,24 @@ export function rscKit(options = {}) {
|
|
|
4061
4273
|
// no host installed every rpc() is refused — so a page whose data comes
|
|
4062
4274
|
// from the backend renders its shell and then blanks, which reads as a
|
|
4063
4275
|
// hydration bug rather than a missing wire.
|
|
4064
|
-
server.httpServer?.once(
|
|
4276
|
+
server.httpServer?.once("listening", async () => {
|
|
4065
4277
|
const env = server.environments?.rsc;
|
|
4066
4278
|
if (!env?.runner)
|
|
4067
4279
|
return;
|
|
4068
4280
|
// The app's own .env, unprefixed. A Laravel app already has both of
|
|
4069
4281
|
// these, which is what makes this need no configuring: APP_URL is the
|
|
4070
4282
|
// backend and RSC_HOST_CALL_SECRET is the secret it checks.
|
|
4071
|
-
const fromEnv = loadEnv(server.config.mode, projectRoot,
|
|
4283
|
+
const fromEnv = loadEnv(server.config.mode, projectRoot, "");
|
|
4072
4284
|
const secret = hostCallOptions?.secret ?? fromEnv.RSC_HOST_CALL_SECRET;
|
|
4073
4285
|
const origin = hostCallOptions?.endpoint ?? fromEnv.RSC_BACKEND ?? fromEnv.APP_URL;
|
|
4074
4286
|
if (!secret || !origin)
|
|
4075
4287
|
return;
|
|
4076
|
-
const path = hostCallOptions?.path ??
|
|
4077
|
-
|
|
4288
|
+
const path = hostCallOptions?.path ??
|
|
4289
|
+
fromEnv.RSC_HOST_CALL_PATH ??
|
|
4290
|
+
"/__rsc/host-call";
|
|
4291
|
+
const endpoint = origin.replace(/\/$/, "") + path;
|
|
4078
4292
|
try {
|
|
4079
|
-
const entry = await env.runner.import(join(genDir,
|
|
4293
|
+
const entry = await env.runner.import(join(genDir, "entry.rsc.tsx"));
|
|
4080
4294
|
const install = entry.installHostFn;
|
|
4081
4295
|
install?.(httpHostCalls({ endpoint, secret }));
|
|
4082
4296
|
}
|
|
@@ -4099,7 +4313,7 @@ export function rscKit(options = {}) {
|
|
|
4099
4313
|
}
|
|
4100
4314
|
catch { }
|
|
4101
4315
|
};
|
|
4102
|
-
server.httpServer?.once(
|
|
4316
|
+
server.httpServer?.once("listening", () => {
|
|
4103
4317
|
// The url Vite resolved, not one built from the port. A dev server
|
|
4104
4318
|
// whose port is already taken on IPv4 binds IPv6 only and keeps the
|
|
4105
4319
|
// number — so http://127.0.0.1:<port> is a reachable-looking address
|
|
@@ -4108,25 +4322,25 @@ export function rscKit(options = {}) {
|
|
|
4108
4322
|
const resolved = server.resolvedUrls?.local?.[0];
|
|
4109
4323
|
const address = server.httpServer?.address();
|
|
4110
4324
|
const url = resolved ??
|
|
4111
|
-
(typeof address ===
|
|
4112
|
-
? `http://${address.family ===
|
|
4325
|
+
(typeof address === "object" && address
|
|
4326
|
+
? `http://${address.family === "IPv6" ? `[${address.address}]` : address.address}:${address.port}`
|
|
4113
4327
|
: null);
|
|
4114
4328
|
if (!url)
|
|
4115
4329
|
return;
|
|
4116
4330
|
mkdirSync(dirname(hotFile), { recursive: true });
|
|
4117
|
-
writeFileSync(hotFile, url.replace(/\/$/,
|
|
4331
|
+
writeFileSync(hotFile, url.replace(/\/$/, ""));
|
|
4118
4332
|
});
|
|
4119
|
-
for (const signal of [
|
|
4333
|
+
for (const signal of ["SIGINT", "SIGTERM", "exit"]) {
|
|
4120
4334
|
process.once(signal, remove);
|
|
4121
4335
|
}
|
|
4122
|
-
server.httpServer?.once(
|
|
4336
|
+
server.httpServer?.once("close", remove);
|
|
4123
4337
|
}
|
|
4124
4338
|
const shapes = new Set(ROUTE_FILES.map((name) => name));
|
|
4125
4339
|
const affectsRouting = (file) => {
|
|
4126
4340
|
if (!file.startsWith(sourceDir))
|
|
4127
4341
|
return false;
|
|
4128
|
-
const base = file.split(
|
|
4129
|
-
const stem = base.replace(/\.(tsx|jsx|ts|js)$/,
|
|
4342
|
+
const base = file.split("/").pop() ?? "";
|
|
4343
|
+
const stem = base.replace(/\.(tsx|jsx|ts|js)$/, "");
|
|
4130
4344
|
// The host's route-config file, whatever it named it. Hardcoding one
|
|
4131
4345
|
// here would put a backend's convention back into a plugin that is
|
|
4132
4346
|
// published without any — generic-host.test.ts fails if it reappears.
|
|
@@ -4143,8 +4357,8 @@ export function rscKit(options = {}) {
|
|
|
4143
4357
|
// Watched explicitly: the Vite root is the *out* directory, so the app's
|
|
4144
4358
|
// source tree is outside it and nothing would report a file appearing.
|
|
4145
4359
|
server.watcher.add(sourceDir);
|
|
4146
|
-
server.watcher.on(
|
|
4147
|
-
server.watcher.on(
|
|
4360
|
+
server.watcher.on("add", restart);
|
|
4361
|
+
server.watcher.on("unlink", restart);
|
|
4148
4362
|
},
|
|
4149
4363
|
/**
|
|
4150
4364
|
* Freeze what can be frozen, once every bundle exists.
|
|
@@ -4180,7 +4394,8 @@ export function rscKit(options = {}) {
|
|
|
4180
4394
|
// differ — <outDir>/dist/rsc on its own, node_modules/.nitro/… under
|
|
4181
4395
|
// Nitro — and hard-coding the first is what silently skipped the second.
|
|
4182
4396
|
// Both environments emit `index.js`, so this is one path, not a branch.
|
|
4183
|
-
const rscOut = builder?.environments?.rsc?.config?.build?.outDir ??
|
|
4397
|
+
const rscOut = builder?.environments?.rsc?.config?.build?.outDir ??
|
|
4398
|
+
join(outDir, "dist/rsc");
|
|
4184
4399
|
const bundle = resolveRscBundle(rscOut);
|
|
4185
4400
|
// Where the frozen pages go, which is not the same question.
|
|
4186
4401
|
//
|
|
@@ -4192,9 +4407,19 @@ export function rscKit(options = {}) {
|
|
|
4192
4407
|
// they are in place by the time it does.
|
|
4193
4408
|
const clientOut = builder?.environments?.client?.config?.build?.outDir;
|
|
4194
4409
|
const staticDir = clientOut
|
|
4195
|
-
? join(dirname(clientOut),
|
|
4410
|
+
? join(dirname(clientOut), "server", NITRO_STATIC_DIR)
|
|
4196
4411
|
: join(outDir, NITRO_STATIC_DIR);
|
|
4197
4412
|
const { frozen, results } = await prerenderAfterBundles(bundle, staticDir, clientOut ?? publicAssetsDir, builder ? knownActionsOf(builder.config, projectRoot) : []);
|
|
4413
|
+
// The same pages once more, as a module. A Worker has no filesystem, and
|
|
4414
|
+
// until this existed every Cloudflare deploy rendered its "static" pages
|
|
4415
|
+
// live, silently - the reader found no directory and fell through. The
|
|
4416
|
+
// module is uploaded as a sibling of the bundle and imported at runtime
|
|
4417
|
+
// when the directory is not there. Only under Nitro, whose presets are
|
|
4418
|
+
// the ones without a disk; on its own the plugin serves from outDir.
|
|
4419
|
+
if (clientOut && existsSync(staticDir)) {
|
|
4420
|
+
const { inlineModuleName, inlineModuleSource } = await import("./files.js");
|
|
4421
|
+
writeFileSync(join(dirname(staticDir), inlineModuleName(NITRO_STATIC_DIR)), await inlineModuleSource(staticDir));
|
|
4422
|
+
}
|
|
4198
4423
|
// Manifest first. The service worker precaches whatever it finds in this
|
|
4199
4424
|
// directory, so writing it afterwards leaves it out of the list — and an
|
|
4200
4425
|
// installed app whose manifest is the one file that needs the network is
|
|
@@ -4208,11 +4433,12 @@ export function rscKit(options = {}) {
|
|
|
4208
4433
|
},
|
|
4209
4434
|
configResolved(config) {
|
|
4210
4435
|
isWatch = config.build?.watch != null;
|
|
4211
|
-
|
|
4436
|
+
resolvedConfig = config;
|
|
4437
|
+
const nitroMain = config.plugins.find((p) => p.name === "nitro:main");
|
|
4212
4438
|
if (nitroMain?.hotUpdate) {
|
|
4213
4439
|
const original = nitroMain.hotUpdate;
|
|
4214
4440
|
nitroMain.hotUpdate = function (ctx) {
|
|
4215
|
-
if (this.environment?.name ===
|
|
4441
|
+
if (this.environment?.name === "rsc")
|
|
4216
4442
|
return;
|
|
4217
4443
|
return original.call(this, ctx);
|
|
4218
4444
|
};
|
|
@@ -4221,12 +4447,12 @@ export function rscKit(options = {}) {
|
|
|
4221
4447
|
// placed ahead of it sees the wrong graph and fails in ways that are hard
|
|
4222
4448
|
// to trace back here. Cheaper to refuse than to let it through.
|
|
4223
4449
|
const names = config.plugins.map((p) => p.name);
|
|
4224
|
-
const rscAt = names.findIndex((n) => n ===
|
|
4450
|
+
const rscAt = names.findIndex((n) => n === "rsc" || n.startsWith("rsc:"));
|
|
4225
4451
|
const jsxAt = names.findIndex((n) => JSX_PLUGIN_PATTERN.test(n));
|
|
4226
4452
|
if (rscAt !== -1 && jsxAt !== -1 && jsxAt < rscAt) {
|
|
4227
4453
|
throw new Error(`[rsc-kit] Plugin "${names[jsxAt]}" is resolved ahead of rsc(), so it would ` +
|
|
4228
|
-
|
|
4229
|
-
|
|
4454
|
+
"transform JSX before the client/server split.\n" +
|
|
4455
|
+
"Put rscKit() first in your plugins array. If it already is, that plugin " +
|
|
4230
4456
|
"sets enforce: 'pre' and needs to be moved after rsc() explicitly.");
|
|
4231
4457
|
}
|
|
4232
4458
|
},
|
|
@@ -4255,6 +4481,7 @@ export function rscKit(options = {}) {
|
|
|
4255
4481
|
clientChunks: (meta) => meta.normalizedId,
|
|
4256
4482
|
...actionEncryptionKey(),
|
|
4257
4483
|
}),
|
|
4484
|
+
extendableClientReferences(),
|
|
4258
4485
|
routesPlugin,
|
|
4259
4486
|
];
|
|
4260
4487
|
}
|
|
@@ -4303,14 +4530,14 @@ function actionEncryptionKey() {
|
|
|
4303
4530
|
// the key is read, so what ships is the lookup rather than the secret. A
|
|
4304
4531
|
// literal here would put the key in the bundle, which is the thing being
|
|
4305
4532
|
// avoided.
|
|
4306
|
-
return { defineEncryptionKey:
|
|
4533
|
+
return { defineEncryptionKey: "process.env.RSC_ACTION_ENCRYPTION_KEY" };
|
|
4307
4534
|
}
|
|
4308
4535
|
async function appPluginRsc(options = {}) {
|
|
4309
4536
|
try {
|
|
4310
4537
|
// Resolved against a file *in* the root, since a directory specifier
|
|
4311
4538
|
// resolves relative to its parent.
|
|
4312
|
-
const fromApp = createRequire(join(projectRoot,
|
|
4313
|
-
const entry = fromApp.resolve(
|
|
4539
|
+
const fromApp = createRequire(join(projectRoot, "package.json"));
|
|
4540
|
+
const entry = fromApp.resolve("@vitejs/plugin-rsc");
|
|
4314
4541
|
const mod = (await import(pathToFileURL(entry).href));
|
|
4315
4542
|
return (mod.default ?? rsc)(options);
|
|
4316
4543
|
}
|