@rangojs/router 0.0.0-experimental.150 → 0.0.0-experimental.151
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/vite/index.js +146 -76
- package/package.json +2 -2
- package/skills/debug-manifest/SKILL.md +2 -10
- package/skills/middleware/SKILL.md +33 -0
- package/skills/ppr/SKILL.md +18 -3
- package/skills/prerender/SKILL.md +44 -1
- package/src/build/generate-manifest.ts +5 -36
- package/src/build/route-trie.ts +5 -50
- package/src/cache/cache-key-utils.ts +0 -1
- package/src/prerender/build-shell-capture.ts +215 -21
- package/src/router/handler-context.ts +10 -1
- package/src/router/middleware-types.ts +17 -0
- package/src/router/middleware.ts +4 -0
- package/src/router/prerender-match.ts +7 -0
- package/src/router/router-interfaces.ts +0 -6
- package/src/router/router-options.ts +0 -8
- package/src/router.ts +0 -4
- package/src/rsc/handler.ts +55 -51
- package/src/rsc/helpers.ts +461 -0
- package/src/rsc/progressive-enhancement.ts +66 -27
- package/src/rsc/rsc-rendering.ts +91 -57
- package/src/rsc/server-action.ts +49 -46
- package/src/rsc/shell-build-manifest.ts +50 -8
- package/src/rsc/shell-capture.ts +17 -6
- package/src/server/context.ts +34 -10
- package/src/server/request-context.ts +31 -0
- package/src/testing/internal/context.ts +9 -0
- package/src/testing/render-handler.ts +14 -0
- package/src/testing/run-middleware.ts +14 -0
- package/src/types/handler-context.ts +12 -1
- package/src/vite/discovery/discover-routers.ts +44 -52
- package/src/vite/discovery/virtual-module-codegen.ts +112 -2
- package/src/vite/rango.ts +31 -0
- package/src/vite/router-discovery.ts +131 -22
|
@@ -108,6 +108,27 @@ export interface RequestContext<
|
|
|
108
108
|
/** @internal Stub response for collecting headers/cookies. Use ctx.headers or ctx.header() instead. */
|
|
109
109
|
readonly res: Response;
|
|
110
110
|
|
|
111
|
+
/**
|
|
112
|
+
* True for build-time render/capture requests. Live requests use false.
|
|
113
|
+
* Build-time shell capture sets this while replaying middleware so apps can
|
|
114
|
+
* skip side-effectful runtime work.
|
|
115
|
+
*/
|
|
116
|
+
readonly build: boolean;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Opt this request out of PPR shell serving/capture.
|
|
120
|
+
* Runtime middleware can call this before the PPR commit point; handlers can
|
|
121
|
+
* call it on a MISS to prevent the follow-up capture.
|
|
122
|
+
*
|
|
123
|
+
* Scope: the PPR SHELL axis only. It does NOT disable prerender B-segment
|
|
124
|
+
* (Prerender/Static) serving, and it is inert in the prerender-collect /
|
|
125
|
+
* static-render contexts (no live shell decision to influence there).
|
|
126
|
+
*/
|
|
127
|
+
dynamic(): void;
|
|
128
|
+
|
|
129
|
+
/** @internal Request-local PPR opt-out marker set by ctx.dynamic(). */
|
|
130
|
+
_dynamic?: boolean;
|
|
131
|
+
|
|
111
132
|
/** @internal Get a cookie value (effective: request + response mutations). Use cookies().get() instead. */
|
|
112
133
|
cookie(name: string): string | undefined;
|
|
113
134
|
/** @internal Get all cookies (effective merged view). Use cookies().getAll() instead. */
|
|
@@ -628,6 +649,7 @@ export type PublicRequestContext<
|
|
|
628
649
|
| "_variables"
|
|
629
650
|
| "_classifiedRoute"
|
|
630
651
|
| "_cacheSignal"
|
|
652
|
+
| "_dynamic"
|
|
631
653
|
| "res"
|
|
632
654
|
>;
|
|
633
655
|
|
|
@@ -775,6 +797,8 @@ export interface CreateRequestContextOptions<TEnv> {
|
|
|
775
797
|
>;
|
|
776
798
|
/** Optional Cloudflare execution context for waitUntil support */
|
|
777
799
|
executionContext?: ExecutionContext;
|
|
800
|
+
/** Build-time render/capture request marker. Defaults to false. */
|
|
801
|
+
build?: boolean;
|
|
778
802
|
/** Optional theme configuration (enables ctx.theme and ctx.setTheme) */
|
|
779
803
|
themeConfig?: ResolvedThemeConfig | null;
|
|
780
804
|
/** Resolved rango state cookie name, for the server seat of invalidateClientCache(). */
|
|
@@ -804,6 +828,7 @@ export function createRequestContext<TEnv>(
|
|
|
804
828
|
explicitTaggedStores,
|
|
805
829
|
cacheProfiles,
|
|
806
830
|
executionContext,
|
|
831
|
+
build = false,
|
|
807
832
|
themeConfig,
|
|
808
833
|
stateCookieName,
|
|
809
834
|
version: stateVersion,
|
|
@@ -951,6 +976,11 @@ export function createRequestContext<TEnv>(
|
|
|
951
976
|
pathname: url.pathname,
|
|
952
977
|
searchParams: cleanUrl.searchParams,
|
|
953
978
|
_variables: variables,
|
|
979
|
+
build,
|
|
980
|
+
dynamic(): void {
|
|
981
|
+
ctx._dynamic = true;
|
|
982
|
+
},
|
|
983
|
+
_dynamic: false,
|
|
954
984
|
get: ((keyOrVar: any) => {
|
|
955
985
|
if (isNonCacheable(variables, keyOrVar) && isInsideCacheScope()) {
|
|
956
986
|
throw new Error(
|
|
@@ -1089,6 +1119,7 @@ export function createRequestContext<TEnv>(
|
|
|
1089
1119
|
_cacheProfiles: cacheProfiles,
|
|
1090
1120
|
|
|
1091
1121
|
waitUntil(fn: () => Promise<void>): void {
|
|
1122
|
+
if (ctx.build) return;
|
|
1092
1123
|
// Wrap in Promise.resolve().then(fn) so a SYNCHRONOUS throw in a
|
|
1093
1124
|
// non-async callback becomes a rejected promise handed to the host's
|
|
1094
1125
|
// waitUntil (logged as a background failure), instead of escaping into
|
|
@@ -55,6 +55,14 @@ export interface CreateTestContextOptions<TEnv> {
|
|
|
55
55
|
routeMap?: Record<string, string>;
|
|
56
56
|
routeName?: string;
|
|
57
57
|
params?: Record<string, string>;
|
|
58
|
+
/**
|
|
59
|
+
* Seed `ctx.build` (default false). Set `true` to unit-test middleware or a
|
|
60
|
+
* handler that branches on the build-time PPR shell-capture pass — e.g.
|
|
61
|
+
* `if (ctx.build) ctx.dynamic()`. Mirrors the synthetic build request the
|
|
62
|
+
* shell-capture producer creates; also makes `ctx.waitUntil()` inert, as at
|
|
63
|
+
* build time.
|
|
64
|
+
*/
|
|
65
|
+
build?: boolean;
|
|
58
66
|
/**
|
|
59
67
|
* Router basename for this request (what the RSC handler stores on the
|
|
60
68
|
* context). Drives redirect() prefixing. Normalized exactly like
|
|
@@ -150,6 +158,7 @@ export function createTestRequestContext<TEnv>(
|
|
|
150
158
|
request,
|
|
151
159
|
url,
|
|
152
160
|
variables,
|
|
161
|
+
build: opts.build,
|
|
153
162
|
themeConfig:
|
|
154
163
|
opts.theme === undefined ? undefined : resolveThemeConfig(opts.theme),
|
|
155
164
|
cacheStore: opts.cacheStore,
|
|
@@ -80,6 +80,12 @@ export interface RenderHandlerOptions<TEnv = any> {
|
|
|
80
80
|
routeName?: string;
|
|
81
81
|
/** Route name -> pattern map enabling `ctx.reverse()`. */
|
|
82
82
|
routeMap?: Record<string, string>;
|
|
83
|
+
/**
|
|
84
|
+
* Seed `ctx.build` (default false) so a handler that branches on the
|
|
85
|
+
* build-time pass — including calling `ctx.dynamic()` on a MISS — is
|
|
86
|
+
* unit-testable. Assert a `ctx.dynamic()` call via `result.dynamic`.
|
|
87
|
+
*/
|
|
88
|
+
build?: boolean;
|
|
83
89
|
/**
|
|
84
90
|
* Seed the data `ctx.use(SomeLoader)` returns — NO real loader runs (same model
|
|
85
91
|
* as `runLoader`'s `loaders`). Matched by loader reference, so a real
|
|
@@ -163,6 +169,12 @@ export interface RenderHandlerResult {
|
|
|
163
169
|
locationState: Record<string, unknown>;
|
|
164
170
|
/** What the handler pushed via `ctx.use(Handle)(...)` (e.g. Meta, Breadcrumbs), keyed by handle. */
|
|
165
171
|
handles: Map<Handle<any, any>, unknown[]>;
|
|
172
|
+
/**
|
|
173
|
+
* Whether the handler called `ctx.dynamic()` (the PPR shell opt-out). The
|
|
174
|
+
* public way to assert the opt-out without reading the `@internal`
|
|
175
|
+
* `ctx._dynamic`.
|
|
176
|
+
*/
|
|
177
|
+
dynamic: boolean;
|
|
166
178
|
}
|
|
167
179
|
|
|
168
180
|
/**
|
|
@@ -239,6 +251,7 @@ export async function renderHandler<TEnv = any>(
|
|
|
239
251
|
request,
|
|
240
252
|
url,
|
|
241
253
|
variables: seedVariables({}, opts.vars),
|
|
254
|
+
build: opts.build,
|
|
242
255
|
stateCookieName,
|
|
243
256
|
version: opts.stateCookie?.version,
|
|
244
257
|
cacheStore: opts.cacheStore,
|
|
@@ -353,5 +366,6 @@ export async function renderHandler<TEnv = any>(
|
|
|
353
366
|
stateCookieName,
|
|
354
367
|
locationState,
|
|
355
368
|
handles: handlePushes,
|
|
369
|
+
dynamic: (reqCtx as RequestContext<TEnv>)._dynamic === true,
|
|
356
370
|
};
|
|
357
371
|
}
|
|
@@ -49,6 +49,13 @@ export interface RunMiddlewareOptions<TEnv = any> {
|
|
|
49
49
|
env?: TEnv;
|
|
50
50
|
/** Route params surfaced as `ctx.params`. */
|
|
51
51
|
params?: Record<string, string>;
|
|
52
|
+
/**
|
|
53
|
+
* Seed `ctx.build` (default false) so a middleware that branches on the
|
|
54
|
+
* build-time PPR shell-capture pass (e.g. `if (ctx.build) ctx.dynamic()`) is
|
|
55
|
+
* unit-testable. With `build: true`, `ctx.waitUntil()` is inert, matching the
|
|
56
|
+
* build producer. Assert a `ctx.dynamic()` call via `result.dynamic`.
|
|
57
|
+
*/
|
|
58
|
+
build?: boolean;
|
|
52
59
|
/** Variables a prior middleware would have set (object or [key, value] list). */
|
|
53
60
|
vars?: VarsInit;
|
|
54
61
|
/** Route name -> pattern map enabling `ctx.reverse()`. */
|
|
@@ -102,6 +109,11 @@ export interface RunMiddlewareResult<TEnv = any> {
|
|
|
102
109
|
ctx: RequestContext<TEnv>;
|
|
103
110
|
/** Number of times the terminal handler ran (0 = short-circuited, 1 = passed through). */
|
|
104
111
|
nextCalled: number;
|
|
112
|
+
/**
|
|
113
|
+
* Whether the chain called `ctx.dynamic()` (the PPR shell opt-out). The public
|
|
114
|
+
* way to assert the opt-out without reading the `@internal` `ctx._dynamic`.
|
|
115
|
+
*/
|
|
116
|
+
dynamic: boolean;
|
|
105
117
|
/**
|
|
106
118
|
* The effective cookie view after the chain ran: request cookies merged with
|
|
107
119
|
* anything the chain set or deleted (last-write-wins), as `{ name: value }`.
|
|
@@ -146,6 +158,7 @@ export async function runMiddleware<TEnv = any>(
|
|
|
146
158
|
routeMap: opts.routeMap,
|
|
147
159
|
routeName: opts.routeName,
|
|
148
160
|
params: opts.params,
|
|
161
|
+
build: opts.build,
|
|
149
162
|
basename: opts.basename,
|
|
150
163
|
theme: opts.theme,
|
|
151
164
|
cacheStore: opts.cacheStore,
|
|
@@ -197,6 +210,7 @@ export async function runMiddleware<TEnv = any>(
|
|
|
197
210
|
response,
|
|
198
211
|
ctx,
|
|
199
212
|
nextCalled,
|
|
213
|
+
dynamic: ctx._dynamic === true,
|
|
200
214
|
cookies,
|
|
201
215
|
headers,
|
|
202
216
|
locationState,
|
|
@@ -202,7 +202,18 @@ export type HandlerContext<
|
|
|
202
202
|
* Build-time collection and dev on-demand prerender use `true`.
|
|
203
203
|
* Live request rendering, including passthrough fallback, uses `false`.
|
|
204
204
|
*/
|
|
205
|
-
build: boolean;
|
|
205
|
+
readonly build: boolean;
|
|
206
|
+
/**
|
|
207
|
+
* Opt this request out of PPR shell serving/capture.
|
|
208
|
+
* Middleware can call this before a shell HIT commits. Handlers run after
|
|
209
|
+
* that commit point, so they only prevent scheduling a new capture on MISS.
|
|
210
|
+
*
|
|
211
|
+
* Scope: this gates the PPR SHELL axis only. It does NOT disable prerender
|
|
212
|
+
* B-segment (Prerender/Static) serving — a Prerender() route's build-baked
|
|
213
|
+
* segments still replay at runtime — and it is inert in the prerender-collect
|
|
214
|
+
* and static-render contexts, which have no live shell decision to influence.
|
|
215
|
+
*/
|
|
216
|
+
dynamic(): void;
|
|
206
217
|
/**
|
|
207
218
|
* True when running in Vite dev mode, false during production build or
|
|
208
219
|
* live request rendering. Use this to branch on runtime mode without
|
|
@@ -150,7 +150,6 @@ export async function discoverRouters(
|
|
|
150
150
|
const newPerRouterManifestDataMap = new Map<string, any>();
|
|
151
151
|
const newPerRouterPrecomputedMap = new Map<string, PrecomputedEntry[]>();
|
|
152
152
|
const newPerRouterTrieMap = new Map<string, any>();
|
|
153
|
-
let mergedRouteAncestry: Record<string, string[]> = {};
|
|
154
153
|
let mergedRouteTrailingSlash: Record<string, string> = {};
|
|
155
154
|
|
|
156
155
|
let routerMountIndex = 0;
|
|
@@ -245,10 +244,6 @@ export async function discoverRouters(
|
|
|
245
244
|
factoryOnlyPrefixes,
|
|
246
245
|
});
|
|
247
246
|
|
|
248
|
-
// Merge ancestry (internal field, used only for trie building)
|
|
249
|
-
if (manifest._routeAncestry) {
|
|
250
|
-
Object.assign(mergedRouteAncestry, manifest._routeAncestry);
|
|
251
|
-
}
|
|
252
247
|
// Merge trailing slash config
|
|
253
248
|
if (manifest.routeTrailingSlash) {
|
|
254
249
|
Object.assign(mergedRouteTrailingSlash, manifest.routeTrailingSlash);
|
|
@@ -303,64 +298,61 @@ export async function discoverRouters(
|
|
|
303
298
|
(performance.now() - manifestGenStart).toFixed(1),
|
|
304
299
|
);
|
|
305
300
|
|
|
306
|
-
// Build route trie from merged manifest
|
|
301
|
+
// Build route trie from merged manifest
|
|
307
302
|
let newMergedRouteTrie: any = null;
|
|
308
303
|
const trieStart = debug ? performance.now() : 0;
|
|
309
304
|
if (Object.keys(newMergedRouteManifest).length > 0) {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
routeToStaticPrefix[name] = "";
|
|
318
|
-
}
|
|
305
|
+
// Build routeToStaticPrefix from saved manifests
|
|
306
|
+
const routeToStaticPrefix: Record<string, string> = {};
|
|
307
|
+
for (const { manifest } of allManifests) {
|
|
308
|
+
// Root-level routes have empty static prefix
|
|
309
|
+
for (const name of Object.keys(manifest.routeManifest)) {
|
|
310
|
+
if (!(name in routeToStaticPrefix)) {
|
|
311
|
+
routeToStaticPrefix[name] = "";
|
|
319
312
|
}
|
|
320
|
-
buildRouteToStaticPrefix(manifest.prefixTree, routeToStaticPrefix);
|
|
321
313
|
}
|
|
314
|
+
buildRouteToStaticPrefix(manifest.prefixTree, routeToStaticPrefix);
|
|
315
|
+
}
|
|
322
316
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
}
|
|
317
|
+
// Collect prerender route names and response type routes from all manifests
|
|
318
|
+
const prerenderRouteNames = new Set<string>();
|
|
319
|
+
const passthroughRouteNames = new Set<string>();
|
|
320
|
+
const mergedResponseTypeRoutes: Record<string, string> = {};
|
|
321
|
+
for (const { manifest } of allManifests) {
|
|
322
|
+
if (manifest.prerenderRoutes) {
|
|
323
|
+
for (const name of manifest.prerenderRoutes) {
|
|
324
|
+
prerenderRouteNames.add(name);
|
|
332
325
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
if (manifest.responseTypeRoutes) {
|
|
339
|
-
Object.assign(mergedResponseTypeRoutes, manifest.responseTypeRoutes);
|
|
326
|
+
}
|
|
327
|
+
if (manifest.passthroughRoutes) {
|
|
328
|
+
for (const name of manifest.passthroughRoutes) {
|
|
329
|
+
passthroughRouteNames.add(name);
|
|
340
330
|
}
|
|
341
331
|
}
|
|
332
|
+
if (manifest.responseTypeRoutes) {
|
|
333
|
+
Object.assign(mergedResponseTypeRoutes, manifest.responseTypeRoutes);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
342
336
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
);
|
|
337
|
+
// buildRouteTrie reads these via ?.has / ?.[] — empty is observationally
|
|
338
|
+
// identical to undefined, so no empty->undefined coercion is needed.
|
|
339
|
+
newMergedRouteTrie = buildRouteTrie(
|
|
340
|
+
newMergedRouteManifest,
|
|
341
|
+
routeToStaticPrefix,
|
|
342
|
+
mergedRouteTrailingSlash,
|
|
343
|
+
prerenderRouteNames,
|
|
344
|
+
passthroughRouteNames,
|
|
345
|
+
mergedResponseTypeRoutes,
|
|
346
|
+
);
|
|
354
347
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
348
|
+
// Build per-router tries for multi-router isolation. Uses the single
|
|
349
|
+
// shared buildPerRouterTrie so the production serialized trie is built by
|
|
350
|
+
// exactly the same code as the dev/HMR runtime rebuild (manifest-init.ts).
|
|
351
|
+
// Returns null for route-less manifests (route-trie.ts).
|
|
352
|
+
for (const { id, manifest } of allManifests) {
|
|
353
|
+
const perRouterTrie = buildPerRouterTrie(manifest);
|
|
354
|
+
if (perRouterTrie) {
|
|
355
|
+
newPerRouterTrieMap.set(id, perRouterTrie);
|
|
364
356
|
}
|
|
365
357
|
}
|
|
366
358
|
}
|
|
@@ -5,11 +5,25 @@
|
|
|
5
5
|
* per-router virtual modules used by the load() hook.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
8
9
|
import { dirname, basename, join } from "node:path";
|
|
9
10
|
import { jsonParseExpression } from "../utils/manifest-utils.js";
|
|
10
11
|
import { VIRTUAL_ROUTES_MANIFEST_ID } from "./state.js";
|
|
11
12
|
import type { DiscoveryState } from "./state.js";
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Serialized-payload size (bytes) at or above which the trie/precomputedEntries
|
|
16
|
+
* are shipped through a runtime channel (Text module / `?raw` import) instead of
|
|
17
|
+
* an inline `JSON.parse` literal (issue #665). 512KB is ~2500 routes — the
|
|
18
|
+
* crossover where the channel's benefit (removing the JS compile on cloudflare,
|
|
19
|
+
* reclaiming ~19% escaping bloat everywhere) clears the cost of an extra
|
|
20
|
+
* artifact. Below it the inline literal wins (sub-millisecond compile, ~0 gain),
|
|
21
|
+
* so the whole long tail of normal apps stays byte-identical. The compile/parse
|
|
22
|
+
* cost tracks bytes, not route count, so this stays a byte threshold.
|
|
23
|
+
* RANGO_MANIFEST_TEXT=1 bypasses it.
|
|
24
|
+
*/
|
|
25
|
+
const MANIFEST_EXTERNALIZE_THRESHOLD = 512 * 1024;
|
|
26
|
+
|
|
13
27
|
/**
|
|
14
28
|
* Generate the code for the main virtual:rsc-router/routes-manifest module.
|
|
15
29
|
*/
|
|
@@ -137,6 +151,22 @@ export function generateRoutesManifestModule(state: DiscoveryState): string {
|
|
|
137
151
|
return `// Route manifest will be populated at runtime`;
|
|
138
152
|
}
|
|
139
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Deterministic short hash (FNV-1a, base36) used to disambiguate staged
|
|
156
|
+
* manifest filenames. The sanitized `safeId` alone collapses distinct router
|
|
157
|
+
* ids — e.g. "a/b" and "a.b" both sanitize to "a_b" — which in a multi-router
|
|
158
|
+
* build would overwrite one router's staged manifest with another's. Appending
|
|
159
|
+
* a hash of the ORIGINAL id keeps them distinct.
|
|
160
|
+
*/
|
|
161
|
+
function shortHash(input: string): string {
|
|
162
|
+
let h = 0x811c9dc5;
|
|
163
|
+
for (let i = 0; i < input.length; i++) {
|
|
164
|
+
h ^= input.charCodeAt(i);
|
|
165
|
+
h = Math.imul(h, 0x01000193);
|
|
166
|
+
}
|
|
167
|
+
return (h >>> 0).toString(36);
|
|
168
|
+
}
|
|
169
|
+
|
|
140
170
|
/**
|
|
141
171
|
* Generate the code for a per-router virtual module.
|
|
142
172
|
*/
|
|
@@ -171,13 +201,93 @@ export function generatePerRouterModule(
|
|
|
171
201
|
lines.push(`export const manifest = ${jsonParseExpression(manifest)};`);
|
|
172
202
|
}
|
|
173
203
|
}
|
|
174
|
-
|
|
204
|
+
const hasTrie = !!trie;
|
|
205
|
+
const hasEntries = !!entries && entries.length > 0;
|
|
206
|
+
|
|
207
|
+
// The trie + precomputedEntries are the largest generated data. Inlined as a
|
|
208
|
+
// `JSON.parse('<literal>')` in the JS chunk, the isolate/process must LEX and
|
|
209
|
+
// compile that multi-MB source literal on first request before JSON.parse even
|
|
210
|
+
// runs — pure overhead, since it is data, not logic (issue #665). It also
|
|
211
|
+
// ships bloated: the build minifier re-quotes the literal to double quotes,
|
|
212
|
+
// escaping every JSON `"` as `\"` (measured 5.71MB vs 4.64MB un-escaped at 26k
|
|
213
|
+
// routes).
|
|
214
|
+
//
|
|
215
|
+
// The ONLY way to fix both is to keep the JSON out of JavaScript source — as a
|
|
216
|
+
// raw asset file that never passes through the JS minifier/compiler. On
|
|
217
|
+
// cloudflare that is a workerd Text module: import the staged `.txt` and the
|
|
218
|
+
// isolate gets the raw un-escaped bytes as a string with no compile pass
|
|
219
|
+
// (measured ~17% / ~88ms cold first-hit on a real edge deploy at 26k, and it
|
|
220
|
+
// shrinks the worker upload). The CF vite plugin types `.txt` as Text;
|
|
221
|
+
// wrangler's default rules type **/*.txt as Text for deploy and dev.
|
|
222
|
+
//
|
|
223
|
+
// Scoped to cloudflare deliberately. A Text module is the only no-JS byte
|
|
224
|
+
// channel that needs no filesystem, and cloudflare is where it pays: edge cold
|
|
225
|
+
// isolates spin up per-request under load and on every deploy, and Workers
|
|
226
|
+
// have a hard upload-size limit. node/vercel fall through to the inline
|
|
227
|
+
// literal below — an efficient runtime channel there needs `fs` (a raw asset
|
|
228
|
+
// read), whose fragility (asset co-location, runtime ENOENT) is not worth a
|
|
229
|
+
// large-app-only win that Fluid Compute amortizes and that essentially no real
|
|
230
|
+
// app reaches. `?raw` is NOT an option: in this build it inlines the JSON as a
|
|
231
|
+
// (double-quoted, compiled) JS string literal — same bloat, same compile as
|
|
232
|
+
// the inline form, plus a staged file for nothing.
|
|
233
|
+
//
|
|
234
|
+
// Build mode only (dev rebuilds the manifest per HMR; parse cost is
|
|
235
|
+
// irrelevant). Below MANIFEST_EXTERNALIZE_THRESHOLD the inline literal wins.
|
|
236
|
+
// RANGO_MANIFEST_TEXT overrides: "0" forces inline, "1" forces the Text module
|
|
237
|
+
// and bypasses the threshold (used by the cloudflare-basic dogfood e2e, whose
|
|
238
|
+
// real manifest is below the threshold).
|
|
239
|
+
const preset = state.opts?.preset ?? "node";
|
|
240
|
+
const override = process.env.RANGO_MANIFEST_TEXT;
|
|
241
|
+
const payload = manifestPayload(trie, hasTrie, entries, hasEntries);
|
|
242
|
+
const payloadJson = hasTrie || hasEntries ? JSON.stringify(payload) : "";
|
|
243
|
+
const useTextModule =
|
|
244
|
+
state.isBuildMode &&
|
|
245
|
+
preset === "cloudflare" &&
|
|
246
|
+
override !== "0" &&
|
|
247
|
+
(hasTrie || hasEntries) &&
|
|
248
|
+
(override === "1" || payloadJson.length >= MANIFEST_EXTERNALIZE_THRESHOLD);
|
|
249
|
+
|
|
250
|
+
if (useTextModule) {
|
|
251
|
+
const dir = join(state.projectRoot, "node_modules", ".rango");
|
|
252
|
+
mkdirSync(dir, { recursive: true });
|
|
253
|
+
const safeId = `${routerId.replace(/[^a-zA-Z0-9_-]/g, "_")}-${shortHash(routerId)}`;
|
|
254
|
+
const filePath = join(dir, `manifest-${safeId}.txt`).replaceAll("\\", "/");
|
|
255
|
+
writeFileSync(filePath, payloadJson);
|
|
256
|
+
lines.push(`import __manifestJson from ${JSON.stringify(filePath)};`);
|
|
257
|
+
lines.push(`const __manifestData = JSON.parse(__manifestJson);`);
|
|
258
|
+
emitManifestExports(lines, hasTrie, hasEntries);
|
|
259
|
+
return lines.join("\n");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (hasTrie) {
|
|
175
263
|
lines.push(`export const trie = ${jsonParseExpression(trie)};`);
|
|
176
264
|
}
|
|
177
|
-
if (
|
|
265
|
+
if (hasEntries) {
|
|
178
266
|
lines.push(
|
|
179
267
|
`export const precomputedEntries = ${jsonParseExpression(entries)};`,
|
|
180
268
|
);
|
|
181
269
|
}
|
|
182
270
|
return lines.join("\n") || "";
|
|
183
271
|
}
|
|
272
|
+
|
|
273
|
+
function manifestPayload(
|
|
274
|
+
trie: unknown,
|
|
275
|
+
hasTrie: boolean,
|
|
276
|
+
entries: unknown,
|
|
277
|
+
hasEntries: boolean,
|
|
278
|
+
): Record<string, unknown> {
|
|
279
|
+
const payload: Record<string, unknown> = {};
|
|
280
|
+
if (hasTrie) payload.t = trie;
|
|
281
|
+
if (hasEntries) payload.p = entries;
|
|
282
|
+
return payload;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function emitManifestExports(
|
|
286
|
+
lines: string[],
|
|
287
|
+
hasTrie: boolean,
|
|
288
|
+
hasEntries: boolean,
|
|
289
|
+
): void {
|
|
290
|
+
if (hasTrie) lines.push(`export const trie = __manifestData.t;`);
|
|
291
|
+
if (hasEntries)
|
|
292
|
+
lines.push(`export const precomputedEntries = __manifestData.p;`);
|
|
293
|
+
}
|
package/src/vite/rango.ts
CHANGED
|
@@ -46,6 +46,31 @@ import { createRangoDebugger, NS } from "./debug.js";
|
|
|
46
46
|
|
|
47
47
|
const debugConfig = createRangoDebugger(NS.config);
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Syntax target for the node/vercel server (`ssr` + `rsc`) build environments.
|
|
51
|
+
*
|
|
52
|
+
* Vite 8's build-target default is `baseline-widely-available`, which resolves
|
|
53
|
+
* to a BROWSER baseline (`chrome111`/`edge111`/…, ≈ES2022) and is applied
|
|
54
|
+
* per-environment with NO server carve-out: `resolveRolldownOptions` pipes it
|
|
55
|
+
* straight into rolldown/oxc's `transform.target` for `consumer:"server"` chunks
|
|
56
|
+
* too. So without an explicit server target our ssr/rsc bundles — which only run
|
|
57
|
+
* on Node or workerd — get compiled for browsers, needlessly rewriting modern
|
|
58
|
+
* syntax (e.g. Explicit Resource Management `using`/`await using`) into
|
|
59
|
+
* `_usingCtx()` runtime helpers. `esnext` emits server code at authored
|
|
60
|
+
* modernity: no helpers, marginally smaller/faster bundles, semantics that match
|
|
61
|
+
* reality. The `client` env is intentionally left at the browser baseline.
|
|
62
|
+
*
|
|
63
|
+
* The cloudflare preset does NOT use this: `@cloudflare/vite-plugin` hardcodes
|
|
64
|
+
* `es2024` on its server envs and its `config()` merges after ours, so anything
|
|
65
|
+
* we set there is a dead no-op (workerd runs es2024 fine).
|
|
66
|
+
*
|
|
67
|
+
* Caveat: `esnext` stops downleveling ERM `using`, which parses natively only on
|
|
68
|
+
* Node 24+ (workerd runs it). Ordinary ≤ES2022 server code still runs across the
|
|
69
|
+
* whole supported Node range; only an app that authors `using` AND deploys to
|
|
70
|
+
* Node < 24 would need to downlevel it itself. See issue #729.
|
|
71
|
+
*/
|
|
72
|
+
const SERVER_BUILD_TARGET = "esnext";
|
|
73
|
+
|
|
49
74
|
// The leading-directive 'use client' sniff is shared with version-plugin's
|
|
50
75
|
// getClientModuleSignature so the two cannot drift. Imported for local use by the
|
|
51
76
|
// HMR transform below and re-exported because the E8 sniff test imports it from
|
|
@@ -409,6 +434,9 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
409
434
|
},
|
|
410
435
|
ssr: {
|
|
411
436
|
...(vercelServerEnv ?? {}),
|
|
437
|
+
build: {
|
|
438
|
+
target: SERVER_BUILD_TARGET,
|
|
439
|
+
},
|
|
412
440
|
optimizeDeps: {
|
|
413
441
|
entries: [VIRTUAL_IDS.ssr],
|
|
414
442
|
include: [
|
|
@@ -428,6 +456,9 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
428
456
|
},
|
|
429
457
|
rsc: {
|
|
430
458
|
...(vercelServerEnv ?? {}),
|
|
459
|
+
build: {
|
|
460
|
+
target: SERVER_BUILD_TARGET,
|
|
461
|
+
},
|
|
431
462
|
optimizeDeps: {
|
|
432
463
|
entries: [VIRTUAL_IDS.rsc],
|
|
433
464
|
include: [
|