@rangojs/router 0.19.1 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/rango.js +26 -14
- package/dist/types/browser/event-controller.d.ts +32 -0
- package/dist/types/browser/history-state.d.ts +16 -4
- package/dist/types/browser/navigation-transaction.d.ts +7 -0
- package/dist/types/browser/rango-state.d.ts +3 -1
- package/dist/types/browser/react/context.d.ts +11 -0
- package/dist/types/browser/react/location-state-shared.d.ts +84 -10
- package/dist/types/browser/react/location-state.d.ts +7 -2
- package/dist/types/browser/react/subscribe-to-nothing.d.ts +7 -0
- package/dist/types/cache/cache-scope.d.ts +4 -5
- package/dist/types/cache/cf/cf-cache-store.d.ts +27 -6
- package/dist/types/cache/cf/cf-cache-types.d.ts +12 -4
- package/dist/types/cache/handle-snapshot.d.ts +36 -50
- package/dist/types/cache/index.d.ts +2 -0
- package/dist/types/cache/shell-snapshot.d.ts +19 -3
- package/dist/types/cache/types.d.ts +21 -10
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +28 -16
- package/dist/types/client-urls/navigation.d.ts +7 -1
- package/dist/types/client-urls/optimistic-location.d.ts +10 -4
- package/dist/types/handles/deferred-resolution.d.ts +11 -1
- package/dist/types/prerender/build-shell-capture.d.ts +8 -6
- package/dist/types/router/match-middleware/cache-lookup.d.ts +1 -1
- package/dist/types/router/navigation-snapshot.d.ts +8 -1
- package/dist/types/router/router-interfaces.d.ts +2 -1
- package/dist/types/router/router-options.d.ts +17 -7
- package/dist/types/router/segment-resolution/loader-cache.d.ts +21 -9
- package/dist/types/router-versions.d.ts +39 -0
- package/dist/types/rsc/handler-context.d.ts +5 -0
- package/dist/types/rsc/shell-build-manifest.d.ts +3 -2
- package/dist/types/rsc/types.d.ts +4 -6
- package/dist/types/server/build-version-table.d.ts +44 -0
- package/dist/types/server/handle-store.d.ts +87 -46
- package/dist/types/server/request-context.d.ts +22 -8
- package/dist/types/ssr/entry-preload-priority.d.ts +69 -0
- package/dist/types/ssr/index.d.ts +1 -0
- package/dist/types/ssr/preinit-client-references.d.ts +17 -5
- package/dist/types/testing/build-versions.d.ts +32 -0
- package/dist/types/testing/dom.entry.d.ts +1 -1
- package/dist/types/testing/flight.entry.d.ts +1 -1
- package/dist/types/testing/index.d.ts +2 -0
- package/dist/types/testing/internal/seed-vars.d.ts +7 -2
- package/dist/types/testing/location-state-key.d.ts +5 -1
- package/dist/types/testing/render-route.d.ts +77 -6
- package/dist/types/testing/serve-shell-request.d.ts +21 -0
- package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
- package/dist/types/types/handler-context.d.ts +8 -5
- package/dist/types/vite/discovery/build-versions.d.ts +393 -0
- package/dist/types/vite/discovery/installed-packages.d.ts +26 -0
- package/dist/types/vite/discovery/router-versions-phase.d.ts +52 -0
- package/dist/types/vite/discovery/shell-prerender-phase.d.ts +7 -17
- package/dist/types/vite/discovery/state.d.ts +14 -0
- package/dist/types/vite/encryption-key.d.ts +18 -0
- package/dist/types/vite/plugin-types.d.ts +47 -6
- package/dist/types/vite/plugins/{version-injector.d.ts → entry-bootstrap-injector.d.ts} +4 -6
- package/dist/types/vite/plugins/expose-ids/export-analysis.d.ts +1 -0
- package/dist/types/vite/plugins/expose-ids/router-transform.d.ts +14 -0
- package/dist/types/vite/plugins/stable-rsc-output.d.ts +51 -0
- package/dist/types/vite/plugins/version-plugin.d.ts +9 -11
- package/dist/types/vite/plugins/virtual-entries.d.ts +21 -2
- package/dist/types/vite/router-discovery.d.ts +6 -0
- package/dist/types/vite/utils/client-chunks.d.ts +24 -2
- package/dist/types/vite/utils/compare-strings.d.ts +7 -0
- package/dist/types/vite/utils/package-resolution.d.ts +8 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +2 -0
- package/dist/vite/index.js +1544 -420
- package/package.json +1 -2
- package/skills/bundle-analysis/SKILL.md +1 -1
- package/skills/cache-guide/SKILL.md +84 -20
- package/skills/caching/SKILL.md +16 -5
- package/skills/client-urls/SKILL.md +16 -0
- package/skills/cloudflare/SKILL.md +2 -1
- package/skills/comparison/references/framework-comparison.md +16 -12
- package/skills/hooks/SKILL.md +3 -0
- package/skills/hooks/handle-and-actions.md +6 -0
- package/skills/hooks/navigation.md +28 -0
- package/skills/hooks/state.md +209 -8
- package/skills/hooks/urls.md +40 -1
- package/skills/host-router/SKILL.md +13 -0
- package/skills/intercept/SKILL.md +6 -0
- package/skills/loader/SKILL.md +19 -1
- package/skills/middleware/SKILL.md +2 -0
- package/skills/ppr/SKILL.md +119 -40
- package/skills/prerender/SKILL.md +7 -3
- package/skills/rango/SKILL.md +8 -7
- package/skills/router-setup/SKILL.md +23 -3
- package/skills/testing/SKILL.md +30 -26
- package/skills/testing/client-components.md +193 -8
- package/skills/testing/handles.md +2 -0
- package/skills/testing/loader.md +1 -1
- package/skills/testing/middleware.md +1 -1
- package/skills/testing/render-handler.md +18 -18
- package/skills/testing/serve-shell-request.md +63 -14
- package/skills/testing/server-actions.md +17 -17
- package/skills/testing/server-tree.md +1 -1
- package/skills/typesafety/generated-files-and-cli.md +1 -1
- package/skills/use-cache/SKILL.md +16 -5
- package/skills/vercel/SKILL.md +3 -3
- package/src/bin/rango.ts +1 -1
- package/src/browser/event-controller.ts +75 -2
- package/src/browser/history-state.ts +39 -17
- package/src/browser/navigation-bridge.ts +41 -18
- package/src/browser/navigation-transaction.ts +15 -10
- package/src/browser/partial-update.ts +10 -2
- package/src/browser/rango-state.ts +3 -1
- package/src/browser/react/Link.tsx +2 -3
- package/src/browser/react/NavigationProvider.tsx +25 -1
- package/src/browser/react/context.ts +13 -0
- package/src/browser/react/location-state-shared.ts +200 -34
- package/src/browser/react/location-state.ts +40 -75
- package/src/browser/react/subscribe-to-nothing.ts +7 -0
- package/src/browser/react/use-handle.ts +20 -1
- package/src/browser/rsc-router.tsx +33 -11
- package/src/browser/server-action-bridge.ts +17 -1
- package/src/cache/cache-scope.ts +18 -8
- package/src/cache/cf/cf-cache-store.ts +75 -55
- package/src/cache/cf/cf-cache-types.ts +12 -4
- package/src/cache/handle-snapshot.ts +69 -87
- package/src/cache/index.ts +4 -0
- package/src/cache/shell-snapshot.ts +35 -5
- package/src/cache/types.ts +21 -10
- package/src/cache/vercel/vercel-cache-store.ts +48 -20
- package/src/client-urls/client-root.tsx +1 -0
- package/src/client-urls/navigation.ts +10 -0
- package/src/client-urls/optimistic-location.ts +10 -4
- package/src/handles/deferred-resolution.ts +31 -0
- package/src/prerender/build-shell-capture.ts +11 -9
- package/src/router/manifest.ts +2 -0
- package/src/router/match-middleware/cache-lookup.ts +11 -18
- package/src/router/navigation-snapshot.ts +25 -6
- package/src/router/request-classification.ts +15 -12
- package/src/router/router-interfaces.ts +2 -1
- package/src/router/router-options.ts +17 -7
- package/src/router/segment-resolution/loader-cache.ts +95 -60
- package/src/router-versions.ts +60 -0
- package/src/rsc/full-payload.ts +6 -2
- package/src/rsc/handler-context.ts +5 -0
- package/src/rsc/handler.ts +14 -4
- package/src/rsc/rsc-rendering.ts +12 -1
- package/src/rsc/shell-build-manifest.ts +10 -7
- package/src/rsc/shell-capture.ts +36 -26
- package/src/rsc/types.ts +4 -6
- package/src/server/build-version-table.ts +115 -0
- package/src/server/handle-store.ts +344 -127
- package/src/server/request-context.ts +62 -11
- package/src/ssr/entry-preload-priority.ts +308 -0
- package/src/ssr/index.tsx +46 -9
- package/src/ssr/preinit-client-references.ts +17 -5
- package/src/ssr/ssr-root.tsx +16 -4
- package/src/testing/build-versions.ts +64 -0
- package/src/testing/dispatch.ts +6 -0
- package/src/testing/dom.entry.ts +1 -0
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +3 -0
- package/src/testing/internal/context.ts +2 -1
- package/src/testing/internal/seed-vars.ts +14 -2
- package/src/testing/location-state-key.ts +18 -8
- package/src/testing/render-handler.ts +2 -1
- package/src/testing/render-route.tsx +277 -43
- package/src/testing/serve-shell-request.ts +80 -11
- package/src/testing/vitest-stubs/version.ts +4 -1
- package/src/types/handler-context.ts +8 -5
- package/src/vite/discovery/build-versions.ts +1281 -0
- package/src/vite/discovery/installed-packages.ts +159 -0
- package/src/vite/discovery/prerender-collection.ts +38 -1
- package/src/vite/discovery/router-versions-phase.ts +336 -0
- package/src/vite/discovery/shell-prerender-phase.ts +28 -38
- package/src/vite/discovery/state.ts +19 -0
- package/src/vite/discovery/virtual-module-codegen.ts +7 -2
- package/src/vite/encryption-key.ts +101 -9
- package/src/vite/plugin-types.ts +48 -6
- package/src/vite/plugins/{version-injector.ts → entry-bootstrap-injector.ts} +7 -22
- package/src/vite/plugins/expose-ids/export-analysis.ts +1 -1
- package/src/vite/plugins/expose-ids/router-transform.ts +36 -15
- package/src/vite/plugins/expose-internal-ids.ts +12 -3
- package/src/vite/plugins/stable-rsc-output.ts +147 -0
- package/src/vite/plugins/version-plugin.ts +19 -24
- package/src/vite/plugins/version.d.ts +17 -2
- package/src/vite/plugins/virtual-entries.ts +40 -7
- package/src/vite/rango.ts +30 -3
- package/src/vite/router-discovery.ts +82 -13
- package/src/vite/utils/client-chunks.ts +84 -14
- package/src/vite/utils/compare-strings.ts +9 -0
- package/src/vite/utils/package-resolution.ts +9 -0
- package/src/vite/utils/prerender-utils.ts +4 -1
- package/src/vite/utils/shared-utils.ts +1 -1
|
@@ -10,6 +10,7 @@ import type { ScanFilter } from "../../build/generate-route-types.js";
|
|
|
10
10
|
import type { ClientUrlProjection } from "../../client-urls/server-projection.js";
|
|
11
11
|
|
|
12
12
|
export const VIRTUAL_ROUTES_MANIFEST_ID = "virtual:rsc-router/routes-manifest";
|
|
13
|
+
export const VIRTUAL_LOADER_MANIFEST_ID = "virtual:rsc-router/loader-manifest";
|
|
13
14
|
|
|
14
15
|
export interface PluginOptions {
|
|
15
16
|
enableBuildPrerender?: boolean;
|
|
@@ -147,6 +148,19 @@ export interface DiscoveryState {
|
|
|
147
148
|
* buildEnd (aborted build).
|
|
148
149
|
*/
|
|
149
150
|
shellPhaseTempServer: import("vite").ViteDevServer | null;
|
|
151
|
+
/**
|
|
152
|
+
* Inputs of the per-router cache versions (build-versions.ts), recorded by
|
|
153
|
+
* generateBundle. plugin-rsc builds the RSC and SSR environments twice (a
|
|
154
|
+
* scan pass, then the real pass); each pass overwrites its slot, so the
|
|
155
|
+
* buildApp post hook reads the real pass.
|
|
156
|
+
*/
|
|
157
|
+
serverBuildGraph: import("./build-versions.js").ServerBuildGraph | null;
|
|
158
|
+
clientBuildGraph: import("./build-versions.js").ClientBuildGraph | null;
|
|
159
|
+
ssrBundle: import("./build-versions.js").BundleFiles | null;
|
|
160
|
+
/** Per environment, the chunk files holding the version module. */
|
|
161
|
+
versionModuleFiles: Map<string, string[]>;
|
|
162
|
+
/** Prerender and Static payloads the build rendered, by content digest. */
|
|
163
|
+
buildData: import("./build-versions.js").BuildDataRecord[];
|
|
150
164
|
handlerChunkInfoMap: Map<string, ChunkInfo>;
|
|
151
165
|
staticHandlerChunkInfoMap: Map<string, ChunkInfo>;
|
|
152
166
|
rscEntryFileName: string | null;
|
|
@@ -204,6 +218,11 @@ export function createDiscoveryState(
|
|
|
204
218
|
shellCandidates: null,
|
|
205
219
|
prerenderPayloadValues: null,
|
|
206
220
|
shellPhaseTempServer: null,
|
|
221
|
+
serverBuildGraph: null,
|
|
222
|
+
clientBuildGraph: null,
|
|
223
|
+
ssrBundle: null,
|
|
224
|
+
versionModuleFiles: new Map(),
|
|
225
|
+
buildData: [],
|
|
207
226
|
handlerChunkInfoMap: new Map(),
|
|
208
227
|
staticHandlerChunkInfoMap: new Map(),
|
|
209
228
|
rscEntryFileName: null,
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { dirname, basename, join } from "node:path";
|
|
10
10
|
import { jsonParseExpression } from "../utils/manifest-utils.js";
|
|
11
|
+
import { compareStrings } from "../utils/compare-strings.js";
|
|
11
12
|
import { VIRTUAL_ROUTES_MANIFEST_ID } from "./state.js";
|
|
12
13
|
import type { DiscoveryState } from "./state.js";
|
|
13
14
|
|
|
@@ -111,8 +112,12 @@ export function generateRoutesManifestModule(state: DiscoveryState): string {
|
|
|
111
112
|
|
|
112
113
|
if (hasClientUrlModules) {
|
|
113
114
|
lines.push(`clearClientUrlProjections();`);
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
// Sorted: the map fills in transform order, which varies between runs,
|
|
116
|
+
// and this module's bytes feed cache versions (build-versions.ts).
|
|
117
|
+
const projections = [...(state.clientUrlProjectionMap ?? [])].sort(
|
|
118
|
+
([a], [b]) => compareStrings(a, b),
|
|
119
|
+
);
|
|
120
|
+
for (const [referenceId, projection] of projections) {
|
|
116
121
|
lines.push(
|
|
117
122
|
`setClientUrlProjection(${JSON.stringify(referenceId)}, ${jsonParseExpression(projection)});`,
|
|
118
123
|
);
|
|
@@ -9,17 +9,104 @@ import { randomBytes } from "node:crypto";
|
|
|
9
9
|
// via plugin-rsc's `defineEncryptionKey` so build-time-encrypted bound args
|
|
10
10
|
// decrypt at runtime.
|
|
11
11
|
//
|
|
12
|
-
//
|
|
13
|
-
// buildStart) and the main build share one key.
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
12
|
+
// Resolved once per process so the temp server (created during the main
|
|
13
|
+
// build's buildStart) and the main build share one key. Source order:
|
|
14
|
+
// rango({ encryptionKey }), then RANGO_ENCRYPTION_KEY, then a key generated for
|
|
15
|
+
// this build. Only the first two are stable across builds.
|
|
16
|
+
//
|
|
17
|
+
// The key is part of the cache version of every router whose server code
|
|
18
|
+
// encrypts with it (discovery/build-versions.ts adds the emitted key file to
|
|
19
|
+
// that router's hash): a payload cached under the old key carries bound args
|
|
20
|
+
// the new build cannot decrypt. So a generated key gives those routers a new
|
|
21
|
+
// version on every build, which is correct and also means their cache is
|
|
22
|
+
// cleared on every deploy.
|
|
23
|
+
|
|
24
|
+
/** AES key sizes Web Crypto imports for AES-GCM, in bytes. */
|
|
25
|
+
const AES_KEY_BYTES: ReadonlySet<number> = new Set([16, 24, 32]);
|
|
26
|
+
|
|
27
|
+
const EXPECTED =
|
|
28
|
+
"a base64-encoded AES key of 16, 24 or 32 bytes (openssl rand -base64 32)";
|
|
29
|
+
|
|
30
|
+
interface ResolvedEncryptionKey {
|
|
31
|
+
key: string;
|
|
32
|
+
/** True when the consumer supplied the key (option or env variable). */
|
|
33
|
+
stable: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let resolvedKey: ResolvedEncryptionKey | undefined;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Throws unless `value` is the base64 form of an AES key (16, 24 or 32 bytes).
|
|
40
|
+
* `source` names where the value came from, for the message.
|
|
41
|
+
*/
|
|
42
|
+
export function assertValidEncryptionKey(value: unknown, source: string): void {
|
|
43
|
+
if (typeof value !== "string") {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`[rango] ${source} must be a string, received ${typeof value}. Expected ${EXPECTED}.`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
if (value === "") {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`[rango] ${source} is an empty string. Unset it to generate a key per build, ` +
|
|
51
|
+
`or set it to ${EXPECTED}.`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
const bytes = /^[A-Za-z0-9+/]+={0,2}$/.test(value)
|
|
55
|
+
? Buffer.from(value, "base64")
|
|
56
|
+
: undefined;
|
|
57
|
+
// Buffer.from tolerates malformed base64 by dropping what it cannot decode;
|
|
58
|
+
// the round-trip rejects those inputs instead of using a shorter key.
|
|
59
|
+
if (
|
|
60
|
+
!bytes ||
|
|
61
|
+
bytes.toString("base64").replace(/=+$/, "") !== value.replace(/=+$/, "")
|
|
62
|
+
) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`[rango] ${source} is not valid base64. Expected ${EXPECTED}.`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
if (!AES_KEY_BYTES.has(bytes.length)) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`[rango] ${source} decodes to ${bytes.length} bytes. Expected ${EXPECTED}.`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolve the build's encryption key from rango({ encryptionKey }) or the
|
|
76
|
+
* RANGO_ENCRYPTION_KEY fallback, validating whichever is set. Called by
|
|
77
|
+
* rango() at config time, before any plugin-rsc instance reads the key, so an
|
|
78
|
+
* invalid key fails the config load instead of the first encrypted action.
|
|
79
|
+
* A key generated earlier in the process is kept when neither source is set:
|
|
80
|
+
* the discovery temp server must encrypt with the main build's key.
|
|
81
|
+
*/
|
|
82
|
+
export function configureEncryptionKey(option: string | undefined): void {
|
|
83
|
+
if (option !== undefined) {
|
|
84
|
+
assertValidEncryptionKey(option, "rango({ encryptionKey })");
|
|
85
|
+
resolvedKey = { key: option, stable: true };
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const fromEnv = process.env.RANGO_ENCRYPTION_KEY;
|
|
89
|
+
if (fromEnv !== undefined) {
|
|
90
|
+
assertValidEncryptionKey(fromEnv, "RANGO_ENCRYPTION_KEY");
|
|
91
|
+
resolvedKey = { key: fromEnv, stable: true };
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (resolvedKey && !resolvedKey.stable) return;
|
|
95
|
+
resolvedKey = { key: randomBytes(32).toString("base64"), stable: false };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function resolved(): ResolvedEncryptionKey {
|
|
99
|
+
if (!resolvedKey) configureEncryptionKey(undefined);
|
|
100
|
+
return resolvedKey!;
|
|
101
|
+
}
|
|
18
102
|
|
|
19
103
|
export function buildEncryptionKey(): string {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
104
|
+
return resolved().key;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** False when this build generated its own key (it differs on every build). */
|
|
108
|
+
export function isEncryptionKeyStable(): boolean {
|
|
109
|
+
return resolved().stable;
|
|
23
110
|
}
|
|
24
111
|
|
|
25
112
|
// The value plugin-rsc inlines as `export default () => (<expr>)`. A JSON string
|
|
@@ -27,3 +114,8 @@ export function buildEncryptionKey(): string {
|
|
|
27
114
|
export function defineEncryptionKeyExpr(): string {
|
|
28
115
|
return JSON.stringify(buildEncryptionKey());
|
|
29
116
|
}
|
|
117
|
+
|
|
118
|
+
/** @internal Test seam: forget the process-wide key. */
|
|
119
|
+
export function resetEncryptionKeyForTests(): void {
|
|
120
|
+
resolvedKey = undefined;
|
|
121
|
+
}
|
package/src/vite/plugin-types.ts
CHANGED
|
@@ -83,6 +83,12 @@ export interface ClientChunkMeta {
|
|
|
83
83
|
* strategy**. It splits app `"use client"` modules by **route id** — the segment
|
|
84
84
|
* after a route-root directory (`routes`, `app`, `pages`, `features`, `handlers`,
|
|
85
85
|
* …) — so `routes/dashboard/**` becomes `app-dashboard` at any nesting depth.
|
|
86
|
+
* The first marker in the path wins, except that an `app/` directly followed
|
|
87
|
+
* by another marker with a directory after it is treated as a source root:
|
|
88
|
+
* `app/routes/product/**` becomes `app-product`, while `app/dashboard/**`
|
|
89
|
+
* (Next-style), `app/components/**` and files directly in `app/routes/` stay
|
|
90
|
+
* `app-dashboard` / `app-components` / `app-routes`. The same route id under
|
|
91
|
+
* different markers is one group.
|
|
86
92
|
* Where it finds NO route structure (a flat `src/components/`, or host sub-apps
|
|
87
93
|
* already split by a dynamic `import()`), it inherits the default grouping
|
|
88
94
|
* unchanged — so the shared `src/components` chunk stays shared and host apps do
|
|
@@ -94,10 +100,14 @@ export interface ClientChunkMeta {
|
|
|
94
100
|
* back to the default grouping for that one module. Forwarded directly to
|
|
95
101
|
* `@vitejs/plugin-rsc`'s `clientChunks`.
|
|
96
102
|
*
|
|
97
|
-
* Every module maps to exactly one group, so there is no byte duplication
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
103
|
+
* Every module maps to exactly one group, so there is no byte duplication. A
|
|
104
|
+
* group is also the loading unit: rendering any one member downloads the whole
|
|
105
|
+
* group's chunk, every other member included. A component used by two routes
|
|
106
|
+
* lives in one group, and rendering it on either route fetches that entire
|
|
107
|
+
* group. Put genuinely shared client components OUTSIDE route directories so
|
|
108
|
+
* they land in the shared group rather than one route's chunk, and keep an
|
|
109
|
+
* always-rendered component (a layout header) out of a large group.
|
|
110
|
+
* `DEBUG=rango:chunks vite build` logs each group's module count and size.
|
|
101
111
|
*
|
|
102
112
|
* @default true
|
|
103
113
|
*/
|
|
@@ -137,14 +147,18 @@ interface RangoBaseOptions {
|
|
|
137
147
|
* - `"preinit"` (**default**): client-reference chunks render as EXECUTING
|
|
138
148
|
* `<script type="module" async>` tags hoisted into `<head>` (upgrading
|
|
139
149
|
* plugin-rsc's modulepreload hints in place), and the browser entry ships
|
|
140
|
-
* as Fizz `bootstrapModules` — a
|
|
141
|
-
*
|
|
150
|
+
* as Fizz `bootstrapModules` — a `modulepreload` hint after the head
|
|
151
|
+
* chunk scripts (default priority; Fizz's `fetchpriority=low` is dropped,
|
|
152
|
+
* #1025)
|
|
153
|
+
* plus the executing end-of-shell `id="_R_"` module script. Chunk
|
|
142
154
|
* execution overlaps body streaming instead of waiting for the hydration
|
|
143
155
|
* import walk; under PPR everything lands in the stored shell prelude.
|
|
144
156
|
* - `"preload"`: the previous behavior — `<link rel="modulepreload">` hints
|
|
145
157
|
* only, entry as an inline `import()` script at end of shell. Chunks
|
|
146
158
|
* fetch+compile early but execute only when hydration imports them.
|
|
147
159
|
*
|
|
160
|
+
* Any other value throws when the generated SSR entry is built.
|
|
161
|
+
*
|
|
148
162
|
* Build-only for the chunk half: plugin-rsc resolves no JS deps per client
|
|
149
163
|
* reference in dev, so dev documents carry no head chunk scripts in either
|
|
150
164
|
* mode (the bootstrap conversion does apply in dev). Trades and upstream
|
|
@@ -225,6 +239,34 @@ interface RangoBaseOptions {
|
|
|
225
239
|
prerender?: {
|
|
226
240
|
onError?: "fail" | "warn";
|
|
227
241
|
};
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* The key inline server actions encrypt their bound arguments with:
|
|
245
|
+
* base64-encoded 32 bytes (`openssl rand -base64 32`). Pass it from the
|
|
246
|
+
* environment, never as a literal in the config file:
|
|
247
|
+
*
|
|
248
|
+
* ```ts
|
|
249
|
+
* rango({ encryptionKey: process.env.RANGO_ENCRYPTION_KEY })
|
|
250
|
+
* ```
|
|
251
|
+
*
|
|
252
|
+
* Why set it: the key is part of the cache version of every router whose
|
|
253
|
+
* server code encrypts with it, because a payload cached under one key
|
|
254
|
+
* carries arguments another key cannot decrypt. Without a stable key rango
|
|
255
|
+
* generates one per build, so those routers get a new version, and a cleared
|
|
256
|
+
* cache, on every deploy. With a stable key their cache survives a deploy
|
|
257
|
+
* that does not change their server code. It also lets a build decrypt
|
|
258
|
+
* arguments an earlier build encrypted (a cached or prerendered payload, an
|
|
259
|
+
* open tab).
|
|
260
|
+
*
|
|
261
|
+
* Validated when `rango()` is called: a value that is not base64 or does not
|
|
262
|
+
* decode to an AES key size (16, 24 or 32 bytes) throws. `undefined` (the
|
|
263
|
+
* variable is unset) falls back to the `RANGO_ENCRYPTION_KEY` environment
|
|
264
|
+
* variable, then to a generated key. Rotating the key clears the cache of
|
|
265
|
+
* the routers that use it.
|
|
266
|
+
*
|
|
267
|
+
* @default process.env.RANGO_ENCRYPTION_KEY, else a key generated per build
|
|
268
|
+
*/
|
|
269
|
+
encryptionKey?: string;
|
|
228
270
|
}
|
|
229
271
|
|
|
230
272
|
/**
|
|
@@ -5,11 +5,9 @@ import { resolveRscEntryFromConfig } from "../utils/shared-utils.js";
|
|
|
5
5
|
import { RSC_ENTRY_BOOTSTRAP_IMPORTS } from "./virtual-entries.js";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Plugin that auto-injects
|
|
9
|
-
* custom entry.rsc
|
|
10
|
-
*
|
|
11
|
-
* automatically. It also ensures the routes-manifest and loader-manifest
|
|
12
|
-
* virtual modules are always imported.
|
|
8
|
+
* Plugin that auto-injects the routes-manifest and loader-manifest imports
|
|
9
|
+
* into a custom entry.rsc file (the Cloudflare preset, where the consumer owns
|
|
10
|
+
* the worker entry).
|
|
13
11
|
*
|
|
14
12
|
* The loader-manifest import is what makes fetchable loaders resolvable on a
|
|
15
13
|
* custom worker entry. The virtual RSC entry (getVirtualEntryRSC) imports the
|
|
@@ -23,13 +21,13 @@ import { RSC_ENTRY_BOOTSTRAP_IMPORTS } from "./virtual-entries.js";
|
|
|
23
21
|
* manifest, hence the production-only failure this fixes.
|
|
24
22
|
* @internal
|
|
25
23
|
*/
|
|
26
|
-
export function
|
|
24
|
+
export function createEntryBootstrapInjectorPlugin(
|
|
27
25
|
rscEntryPath: string | undefined,
|
|
28
26
|
): Plugin {
|
|
29
27
|
let resolvedEntryPath = "";
|
|
30
28
|
|
|
31
29
|
return {
|
|
32
|
-
name: "@rangojs/router:
|
|
30
|
+
name: "@rangojs/router:entry-bootstrap-injector",
|
|
33
31
|
enforce: "pre",
|
|
34
32
|
|
|
35
33
|
configResolved(config) {
|
|
@@ -54,21 +52,8 @@ export function createVersionInjectorPlugin(
|
|
|
54
52
|
const prepend: string[] = RSC_ENTRY_BOOTSTRAP_IMPORTS.map(
|
|
55
53
|
(id) => `import "${id}";`,
|
|
56
54
|
);
|
|
57
|
-
let newCode = code;
|
|
58
|
-
const needsVersion =
|
|
59
|
-
code.includes("createRSCHandler") &&
|
|
60
|
-
!code.includes("@rangojs/router:version") &&
|
|
61
|
-
/createRSCHandler\s*\(\s*\{/.test(code);
|
|
62
55
|
|
|
63
|
-
|
|
64
|
-
prepend.push(`import { VERSION } from "@rangojs/router:version";`);
|
|
65
|
-
newCode = newCode.replace(
|
|
66
|
-
/createRSCHandler\s*\(\s*\{/,
|
|
67
|
-
"createRSCHandler({\n version: VERSION,",
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const lines = newCode.split("\n");
|
|
56
|
+
const lines = code.split("\n");
|
|
72
57
|
let insertAt = 0;
|
|
73
58
|
while (insertAt < lines.length) {
|
|
74
59
|
const trimmed = lines[insertAt]!.trim();
|
|
@@ -78,7 +63,7 @@ export function createVersionInjectorPlugin(
|
|
|
78
63
|
break;
|
|
79
64
|
}
|
|
80
65
|
}
|
|
81
|
-
newCode = [
|
|
66
|
+
const newCode = [
|
|
82
67
|
...lines.slice(0, insertAt),
|
|
83
68
|
...prepend,
|
|
84
69
|
...lines.slice(insertAt),
|
|
@@ -55,7 +55,7 @@ export function isExportOnlyFile(
|
|
|
55
55
|
// (which may be nested, e.g. createLoader<A<B>>(...)) and the call paren are
|
|
56
56
|
// resolved per match via findCallParenAfterGenerics, so a nested `>` no longer
|
|
57
57
|
// defeats the scan the way `<[^>]*>` did (it stopped at the first `>`).
|
|
58
|
-
function createCallPattern(fnNames: string[]): RegExp {
|
|
58
|
+
export function createCallPattern(fnNames: string[]): RegExp {
|
|
59
59
|
return new RegExp(`\\b(?:${fnNames.map(escapeRegExp).join("|")})\\b`, "g");
|
|
60
60
|
}
|
|
61
61
|
|
|
@@ -8,10 +8,11 @@ import {
|
|
|
8
8
|
findCallParenAfterGenerics,
|
|
9
9
|
skipStringOrComment,
|
|
10
10
|
} from "../expose-id-utils.js";
|
|
11
|
-
import { escapeRegExp } from "../../../regex-escape.js";
|
|
12
11
|
import {
|
|
13
12
|
getImportedFnNames,
|
|
14
13
|
buildUnsupportedShapeWarning,
|
|
14
|
+
createCallPattern,
|
|
15
|
+
offsetToLineColumn,
|
|
15
16
|
} from "./export-analysis.js";
|
|
16
17
|
import { codeMatchIndices } from "../../../build/route-types/source-scan.js";
|
|
17
18
|
import { createRangoDebugger, createCounter, NS } from "../../debug.js";
|
|
@@ -42,6 +43,26 @@ function skipLeadingTrivia(code: string, start: number, end: number): number {
|
|
|
42
43
|
return i;
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
/**
|
|
47
|
+
* A router's `$$id`: a hash of its root-relative file and of its position
|
|
48
|
+
* among the file's createRouter() calls.
|
|
49
|
+
*
|
|
50
|
+
* Not of the call's line. `code` is TS-transformed, and a dev server keeps
|
|
51
|
+
* comments there that a build drops, so a line of `code` gave build-time
|
|
52
|
+
* discovery and the bundle different ids for any router with a comment above
|
|
53
|
+
* it (e2e/test-app, tests/cloudflare-basic): the build registered the lazy
|
|
54
|
+
* route manifest under an id no running router had. And a line of the file as
|
|
55
|
+
* written changes with every blank line or comment added above the call,
|
|
56
|
+
* which renamed the state cookie, the route manifest chunk and the router's
|
|
57
|
+
* cache versions for an edit that changed nothing.
|
|
58
|
+
*/
|
|
59
|
+
export function routerId(filePath: string, callIndex: number): string {
|
|
60
|
+
return createHash("sha256")
|
|
61
|
+
.update(`${filePath}#${callIndex}`)
|
|
62
|
+
.digest("hex")
|
|
63
|
+
.slice(0, 8);
|
|
64
|
+
}
|
|
65
|
+
|
|
45
66
|
export function transformRouter(
|
|
46
67
|
code: string,
|
|
47
68
|
filePath: string,
|
|
@@ -53,14 +74,12 @@ export function transformRouter(
|
|
|
53
74
|
// e.g. createRouter<Config<Env>>(...)) and the opening paren are located
|
|
54
75
|
// separately via findCallParenAfterGenerics so a nested `>` does not defeat
|
|
55
76
|
// the scan (a `<[^>]*>` regex stopped at the first `>`).
|
|
56
|
-
const pat =
|
|
57
|
-
`\\b(?:${routerFnNames.map(escapeRegExp).join("|")})\\b`,
|
|
58
|
-
"g",
|
|
59
|
-
);
|
|
77
|
+
const pat = createCallPattern(routerFnNames);
|
|
60
78
|
let match: RegExpExecArray | null;
|
|
61
79
|
const s = new MagicString(code);
|
|
62
80
|
let changed = false;
|
|
63
81
|
const unsupportedSites: Array<{ line: number; column: number }> = [];
|
|
82
|
+
let callIndex = -1;
|
|
64
83
|
|
|
65
84
|
// Compute the import path for the generated route names file.
|
|
66
85
|
// filePath is relative to project root (e.g., "src/router.tsx")
|
|
@@ -86,6 +105,7 @@ export function transformRouter(
|
|
|
86
105
|
// type position) yields -1 and is skipped.
|
|
87
106
|
const parenPos = findCallParenAfterGenerics(code, calleeEnd);
|
|
88
107
|
if (parenPos === -1) continue;
|
|
108
|
+
callIndex++;
|
|
89
109
|
|
|
90
110
|
const closeParen = findMatchingParen(code, parenPos + 1);
|
|
91
111
|
const callArgs = code.slice(parenPos + 1, closeParen);
|
|
@@ -98,12 +118,7 @@ export function transformRouter(
|
|
|
98
118
|
if (callArgs.includes(`$$routeNames: ${routeNamesVar}`)) continue;
|
|
99
119
|
|
|
100
120
|
const sourceFilePath = absolutePath ?? filePath;
|
|
101
|
-
const
|
|
102
|
-
const hash = createHash("sha256")
|
|
103
|
-
.update(`${filePath}:${lineNumber}`)
|
|
104
|
-
.digest("hex")
|
|
105
|
-
.slice(0, 8);
|
|
106
|
-
const injected = ` $$id: "${hash}", $$sourceFile: "${sourceFilePath}", $$routeNames: ${routeNamesVar},`;
|
|
121
|
+
const injected = ` $$id: "${routerId(filePath, callIndex)}", $$sourceFile: "${sourceFilePath}", $$routeNames: ${routeNamesVar},`;
|
|
107
122
|
|
|
108
123
|
// Skip a leading comment/whitespace run so `createRouter(/* c *\/ {...})`
|
|
109
124
|
// and a newline-then-comment prefix still resolve to the object literal.
|
|
@@ -124,9 +139,7 @@ export function transformRouter(
|
|
|
124
139
|
// stable $$id can be injected here. Record it so the plugin can warn —
|
|
125
140
|
// and crucially do NOT mark changed, so a dead named-routes.gen import is
|
|
126
141
|
// not prepended for a call we never touched.
|
|
127
|
-
|
|
128
|
-
const column = callStart - (lastNl + 1) + 1;
|
|
129
|
-
unsupportedSites.push({ line: lineNumber, column });
|
|
142
|
+
unsupportedSites.push(offsetToLineColumn(code, callStart));
|
|
130
143
|
}
|
|
131
144
|
}
|
|
132
145
|
|
|
@@ -156,11 +169,19 @@ export function transformRouter(
|
|
|
156
169
|
*/
|
|
157
170
|
export function exposeRouterId(): Plugin {
|
|
158
171
|
let projectRoot = "";
|
|
172
|
+
// $$sourceFile is read by route discovery and the CLI, which run on a dev
|
|
173
|
+
// server (the build's discovery temp server included) and need the absolute
|
|
174
|
+
// path. Nothing reads it when serving, so a production build ships the
|
|
175
|
+
// root-relative path: an absolute one would put the build machine's checkout
|
|
176
|
+
// directory in every router's chunk and give the same source a different
|
|
177
|
+
// cache version per directory (discovery/build-versions.ts hashes the chunk).
|
|
178
|
+
let rootRelativeSourceFile = false;
|
|
159
179
|
const counter = createCounter(debug, "expose-router-id");
|
|
160
180
|
return {
|
|
161
181
|
name: "@rangojs/router:expose-router-id",
|
|
162
182
|
configResolved(config) {
|
|
163
183
|
projectRoot = config.root;
|
|
184
|
+
rootRelativeSourceFile = config.command === "build";
|
|
164
185
|
},
|
|
165
186
|
buildEnd() {
|
|
166
187
|
counter?.flush();
|
|
@@ -188,7 +209,7 @@ export function exposeRouterId(): Plugin {
|
|
|
188
209
|
code,
|
|
189
210
|
filePath,
|
|
190
211
|
routerFnNames,
|
|
191
|
-
normalizePath(id),
|
|
212
|
+
rootRelativeSourceFile ? undefined : normalizePath(id),
|
|
192
213
|
warn,
|
|
193
214
|
);
|
|
194
215
|
} finally {
|
|
@@ -45,6 +45,8 @@ import {
|
|
|
45
45
|
transformHandlerIds,
|
|
46
46
|
} from "./expose-ids/handler-transform.js";
|
|
47
47
|
import { createRangoDebugger, createCounter, NS } from "../debug.js";
|
|
48
|
+
import { compareStrings } from "../utils/compare-strings.js";
|
|
49
|
+
import { VIRTUAL_LOADER_MANIFEST_ID } from "../discovery/state.js";
|
|
48
50
|
|
|
49
51
|
const debug = createRangoDebugger(NS.transform);
|
|
50
52
|
|
|
@@ -56,7 +58,7 @@ export type { ExposeInternalIdsApi } from "./expose-ids/types.js";
|
|
|
56
58
|
// Virtual module for loader manifest
|
|
57
59
|
// ---------------------------------------------------------------------------
|
|
58
60
|
|
|
59
|
-
const VIRTUAL_LOADER_MANIFEST =
|
|
61
|
+
const VIRTUAL_LOADER_MANIFEST = VIRTUAL_LOADER_MANIFEST_ID;
|
|
60
62
|
const RESOLVED_VIRTUAL_LOADER_MANIFEST = "\0" + VIRTUAL_LOADER_MANIFEST;
|
|
61
63
|
|
|
62
64
|
// ---------------------------------------------------------------------------
|
|
@@ -245,10 +247,17 @@ setLoaderImports({});
|
|
|
245
247
|
`;
|
|
246
248
|
}
|
|
247
249
|
|
|
248
|
-
// Build mode: generate lazy import map
|
|
250
|
+
// Build mode: generate lazy import map. Sorted by id: the registry fills
|
|
251
|
+
// in directory-scan order (the pre-scan below) and transform order, both
|
|
252
|
+
// of which vary between machines and runs, and this module's bytes feed
|
|
253
|
+
// the cache version of every router bundled with it
|
|
254
|
+
// (discovery/build-versions.ts).
|
|
249
255
|
const lazyImports: string[] = [];
|
|
256
|
+
const sortedLoaders = [...loaderRegistry].sort(([a], [b]) =>
|
|
257
|
+
compareStrings(a, b),
|
|
258
|
+
);
|
|
250
259
|
|
|
251
|
-
for (const [hashedId, { filePath, exportName }] of
|
|
260
|
+
for (const [hashedId, { filePath, exportName }] of sortedLoaders) {
|
|
252
261
|
lazyImports.push(
|
|
253
262
|
` "${hashedId}": () => import("/${filePath}").then(m => m.${exportName})`,
|
|
254
263
|
);
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { parseAst, type Plugin } from "vite";
|
|
2
|
+
import { escapeRegExp } from "../../regex-escape.js";
|
|
3
|
+
import { compareStrings } from "../utils/compare-strings.js";
|
|
4
|
+
import { SERVER_REFERENCES_MODULE_ID } from "../discovery/build-versions.js";
|
|
5
|
+
|
|
6
|
+
/** One property of an object literal, by its position in the source. */
|
|
7
|
+
export interface LiteralProperty {
|
|
8
|
+
readonly key: string;
|
|
9
|
+
/** Range of the whole property, key included. */
|
|
10
|
+
readonly start: number;
|
|
11
|
+
readonly end: number;
|
|
12
|
+
/** The value's AST node. */
|
|
13
|
+
readonly value: any;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The properties of an object-literal AST node, or undefined when it is not an
|
|
18
|
+
* object literal of plain keys (a spread, a computed key).
|
|
19
|
+
*/
|
|
20
|
+
export function literalProperties(object: any): LiteralProperty[] | undefined {
|
|
21
|
+
if (object?.type !== "ObjectExpression") return undefined;
|
|
22
|
+
const properties: LiteralProperty[] = [];
|
|
23
|
+
for (const property of object.properties ?? []) {
|
|
24
|
+
const key =
|
|
25
|
+
property?.type === "Property" && !property.computed
|
|
26
|
+
? (property.key?.value ?? property.key?.name)
|
|
27
|
+
: undefined;
|
|
28
|
+
if (typeof key !== "string") return undefined;
|
|
29
|
+
properties.push({
|
|
30
|
+
key,
|
|
31
|
+
start: property.start,
|
|
32
|
+
end: property.end,
|
|
33
|
+
value: property.value,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return properties;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The properties of a module's `export default { ... }`, or undefined when
|
|
41
|
+
* the module does not parse or is not that shape. plugin-rsc generates its
|
|
42
|
+
* registries (the server-reference map, the assets manifest) in this form.
|
|
43
|
+
*/
|
|
44
|
+
export function defaultExportProperties(
|
|
45
|
+
code: string,
|
|
46
|
+
): LiteralProperty[] | undefined {
|
|
47
|
+
let program: any;
|
|
48
|
+
try {
|
|
49
|
+
program = parseAst(code);
|
|
50
|
+
} catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
return literalProperties(
|
|
54
|
+
(program.body ?? []).find(
|
|
55
|
+
(node: any) => node?.type === "ExportDefaultDeclaration",
|
|
56
|
+
)?.declaration,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Return `code` with the properties of its default-exported object literal
|
|
62
|
+
* sorted by key, or undefined when there is nothing to reorder (or the module
|
|
63
|
+
* is not that shape).
|
|
64
|
+
*/
|
|
65
|
+
export function sortDefaultExportProperties(code: string): string | undefined {
|
|
66
|
+
const properties = defaultExportProperties(code);
|
|
67
|
+
if (!properties || properties.length < 2) return undefined;
|
|
68
|
+
const sorted = [...properties].sort((a, b) => compareStrings(a.key, b.key));
|
|
69
|
+
if (sorted.every((property, index) => property === properties[index])) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
return (
|
|
73
|
+
code.slice(0, properties[0]!.start) +
|
|
74
|
+
sorted
|
|
75
|
+
.map((property) => code.slice(property.start, property.end))
|
|
76
|
+
.join(",\n") +
|
|
77
|
+
code.slice(properties[properties.length - 1]!.end)
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const ASSETS_MANIFEST_ID = "virtual:vite-rsc/assets-manifest";
|
|
82
|
+
const ASSETS_MANIFEST_IMPORT =
|
|
83
|
+
/import\s+([A-Za-z_$][\w$]*)\s+from\s*["']virtual:vite-rsc\/assets-manifest["']/;
|
|
84
|
+
/** The local name plugin-rsc's own runtime modules import the manifest under. */
|
|
85
|
+
const ASSETS_MANIFEST_BINDING = "assetsManifest";
|
|
86
|
+
/**
|
|
87
|
+
* The names plugin-rsc's GENERATED code imports it under. Only these are
|
|
88
|
+
* renamed: the rename is textual, and a name an app author picked
|
|
89
|
+
* (`import manifest from ...`) can also be a word in that module's strings.
|
|
90
|
+
*/
|
|
91
|
+
const GENERATED_BINDINGS: ReadonlySet<string> = new Set([
|
|
92
|
+
"__vite_rsc_assets_manifest__",
|
|
93
|
+
"__vite_rsc_assets_manifest",
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
function identifier(name: string): RegExp {
|
|
97
|
+
return new RegExp(`(?<![\\w$.])${escapeRegExp(name)}(?![\\w$])`, "g");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Return `code` with its default import of plugin-rsc's assets manifest
|
|
102
|
+
* renamed to {@link ASSETS_MANIFEST_BINDING}, or undefined when there is
|
|
103
|
+
* nothing to rename (no such import under a generated name, or the module
|
|
104
|
+
* uses the canonical name for something else).
|
|
105
|
+
*/
|
|
106
|
+
export function canonicalAssetsManifestBinding(
|
|
107
|
+
code: string,
|
|
108
|
+
): string | undefined {
|
|
109
|
+
const local = code.match(ASSETS_MANIFEST_IMPORT)?.[1];
|
|
110
|
+
if (local === undefined || !GENERATED_BINDINGS.has(local)) return undefined;
|
|
111
|
+
if (identifier(ASSETS_MANIFEST_BINDING).test(code)) return undefined;
|
|
112
|
+
return code.replace(identifier(local), ASSETS_MANIFEST_BINDING);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Removes two run-to-run differences from plugin-rsc 0.5.35's build output.
|
|
117
|
+
* The cache versions hash these bytes (discovery/build-versions.ts).
|
|
118
|
+
*
|
|
119
|
+
* 1. Property order of the server-reference map. plugin-rsc writes it in the
|
|
120
|
+
* order its transforms first saw each `"use server"` module
|
|
121
|
+
* (`ServerReferencesManager.claimMap`), which varies with the concurrent
|
|
122
|
+
* scan builds (e2e/test-app: 2 of 2 consecutive builds differed). Sorted
|
|
123
|
+
* here so a minified server build, where stripServerReferenceMap finds no
|
|
124
|
+
* region comments, still hashes the same bytes.
|
|
125
|
+
* 2. Local name of the assets-manifest import. plugin-rsc imports the manifest
|
|
126
|
+
* under three names; the bundler merges them into one import per chunk and
|
|
127
|
+
* keeps a different one from build to build (the cache-versions fixture:
|
|
128
|
+
* 1 of 10 concurrent builds). One name in, one name out.
|
|
129
|
+
* @internal
|
|
130
|
+
*/
|
|
131
|
+
export function stableRscOutput(): Plugin {
|
|
132
|
+
return {
|
|
133
|
+
name: "@rangojs/router:stable-rsc-output",
|
|
134
|
+
apply: "build",
|
|
135
|
+
// After plugin-rsc's transforms, which add the manifest import.
|
|
136
|
+
enforce: "post",
|
|
137
|
+
transform(code, id) {
|
|
138
|
+
const stable =
|
|
139
|
+
id === SERVER_REFERENCES_MODULE_ID
|
|
140
|
+
? sortDefaultExportProperties(code)
|
|
141
|
+
: code.includes(ASSETS_MANIFEST_ID)
|
|
142
|
+
? canonicalAssetsManifestBinding(code)
|
|
143
|
+
: undefined;
|
|
144
|
+
return stable === undefined ? null : { code: stable, map: null };
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|