@rangojs/router 0.5.0 → 0.5.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 +5 -1
- package/dist/types/browser/event-controller.d.ts +6 -0
- package/dist/types/browser/prefetch/cache.d.ts +31 -2
- package/dist/types/cache/cf/cf-cache-constants.d.ts +8 -1
- package/dist/types/cache/cf/cf-cache-store.d.ts +15 -1
- package/dist/types/cache/cf/cf-cache-types.d.ts +1 -1
- package/dist/types/cache/cf/cf-kv-utils.d.ts +21 -0
- package/dist/types/handles/is-thenable.d.ts +2 -4
- package/dist/types/route-definition/helpers-types.d.ts +5 -4
- package/dist/types/rsc/helpers.d.ts +3 -0
- package/dist/types/rsc/render-pipeline.d.ts +9 -0
- package/dist/types/rsc/routine-plan.d.ts +124 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +17 -0
- package/dist/types/server/request-context.d.ts +4 -1
- package/dist/types/static-handler.d.ts +15 -1
- package/dist/types/urls/path-helper-types.d.ts +6 -7
- package/dist/types/vite/encryption-key.d.ts +2 -0
- package/dist/types/vite/plugins/expose-internal-ids.d.ts +10 -0
- package/dist/types/vite/plugins/server-ref-hashing.d.ts +24 -0
- package/dist/types/vite/plugins/server-reference-pattern.d.ts +1 -0
- package/dist/types/vite/utils/shared-utils.d.ts +12 -0
- package/dist/vite/index.js +154 -57
- package/package.json +3 -3
- package/skills/caching/SKILL.md +1 -1
- package/skills/mime-routes/SKILL.md +3 -1
- package/skills/parallel/SKILL.md +1 -1
- package/skills/response-routes/SKILL.md +4 -2
- package/skills/typesafety/generated-files-and-cli.md +16 -9
- package/skills/typesafety/route-types.md +5 -1
- package/skills/use-cache/SKILL.md +47 -0
- package/src/browser/event-controller.ts +40 -15
- package/src/browser/partial-update.ts +26 -11
- package/src/browser/prefetch/cache.ts +53 -9
- package/src/browser/prefetch/fetch.ts +83 -2
- package/src/browser/react/NavigationProvider.tsx +7 -0
- package/src/cache/cache-runtime.ts +89 -15
- package/src/cache/cf/cf-cache-constants.ts +8 -1
- package/src/cache/cf/cf-cache-store.ts +41 -64
- package/src/cache/cf/cf-cache-types.ts +1 -1
- package/src/cache/cf/cf-kv-utils.ts +38 -0
- package/src/cache/segment-codec.ts +21 -5
- package/src/handles/is-thenable.ts +2 -4
- package/src/route-definition/helpers-types.ts +5 -3
- package/src/router/match-middleware/cache-lookup.ts +24 -15
- package/src/rsc/handler.ts +26 -5
- package/src/rsc/helpers.ts +13 -0
- package/src/rsc/progressive-enhancement.ts +247 -70
- package/src/rsc/render-pipeline.ts +68 -24
- package/src/rsc/routine-plan.ts +359 -0
- package/src/rsc/rsc-rendering.ts +555 -302
- package/src/rsc/server-action.ts +180 -71
- package/src/rsc/shell-capture-constants.ts +18 -0
- package/src/rsc/shell-capture.ts +92 -21
- package/src/server/request-context.ts +6 -0
- package/src/ssr/ssr-root.tsx +1 -0
- package/src/static-handler.ts +18 -2
- package/src/urls/path-helper-types.ts +9 -10
- package/src/vite/encryption-key.ts +29 -0
- package/src/vite/plugins/expose-action-id.ts +2 -2
- package/src/vite/plugins/expose-internal-ids.ts +46 -0
- package/src/vite/plugins/server-ref-hashing.ts +74 -0
- package/src/vite/plugins/server-reference-pattern.ts +10 -0
- package/src/vite/rango.ts +9 -0
- package/src/vite/router-discovery.ts +21 -2
- package/src/vite/utils/shared-utils.ts +12 -7
|
@@ -68,6 +68,52 @@ const VIRTUAL_HANDLER_PREFIX = "virtual:handler-extract:";
|
|
|
68
68
|
// Consolidated plugin
|
|
69
69
|
// ---------------------------------------------------------------------------
|
|
70
70
|
|
|
71
|
+
interface RscScanPluginApi {
|
|
72
|
+
manager?: { isScanBuild?: boolean };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Stub export-only loader modules before plugin-rsc reduces scan modules to
|
|
77
|
+
* imports. The regular post transform remains authoritative outside scan builds.
|
|
78
|
+
*
|
|
79
|
+
* Handles and location-state definitions are intentionally excluded: handle
|
|
80
|
+
* collect functions run in the browser, and location-state definitions are
|
|
81
|
+
* callable browser objects. Their dependencies must remain visible to non-RSC
|
|
82
|
+
* validation rather than being hidden behind an opaque scan stub.
|
|
83
|
+
*/
|
|
84
|
+
export function createLoaderScanStubPlugin(): Plugin {
|
|
85
|
+
let projectRoot = "";
|
|
86
|
+
let rscApi: RscScanPluginApi | undefined;
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
name: "@rangojs/router:loader-scan-stubs",
|
|
90
|
+
apply: "build",
|
|
91
|
+
configResolved(config) {
|
|
92
|
+
projectRoot = config.root;
|
|
93
|
+
rscApi = config.plugins.find((plugin) => plugin.name === "rsc:minimal")
|
|
94
|
+
?.api as RscScanPluginApi | undefined;
|
|
95
|
+
},
|
|
96
|
+
transform(code, id) {
|
|
97
|
+
if (
|
|
98
|
+
!rscApi?.manager?.isScanBuild ||
|
|
99
|
+
this.environment?.name === "rsc" ||
|
|
100
|
+
id.includes("/node_modules/") ||
|
|
101
|
+
!code.includes("createLoader") ||
|
|
102
|
+
!hasCreateLoaderImport(code)
|
|
103
|
+
) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const fnNames = getImportedFnNames(code, "createLoader");
|
|
108
|
+
const bindings = collectCreateExportBindings(code, fnNames);
|
|
109
|
+
const filePath = normalizePath(path.relative(projectRoot, id));
|
|
110
|
+
return (
|
|
111
|
+
generateClientLoaderStubs(bindings, code, filePath, true) ?? undefined
|
|
112
|
+
);
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
71
117
|
export function exposeInternalIds(options?: { forceBuild?: boolean }): Plugin {
|
|
72
118
|
let config: ResolvedConfig;
|
|
73
119
|
let isBuild = false;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
import { createRangoDebugger, createCounter, NS } from "../debug.js";
|
|
3
|
+
import { computeProductionHash } from "./client-ref-hashing.js";
|
|
4
|
+
import { registerServerReferenceRegex } from "./server-reference-pattern.js";
|
|
5
|
+
|
|
6
|
+
const debug = createRangoDebugger(NS.transform);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Replace dev-style server-reference ids with their production hashes. The temp
|
|
10
|
+
* server renders Static/Prerender in a dev/serve Vite server, so plugin-rsc mints
|
|
11
|
+
* a dev-style id (e.g. "/src/urls/prerender.tsx"); rewriting only the id (group 2)
|
|
12
|
+
* leaves the hoisted function and its trailing encrypted .bind(...) untouched.
|
|
13
|
+
* Exported for testing; used by hashServerRefs. Returns null if nothing changed.
|
|
14
|
+
*/
|
|
15
|
+
export function transformServerRefs(
|
|
16
|
+
code: string,
|
|
17
|
+
projectRoot: string,
|
|
18
|
+
): string | null {
|
|
19
|
+
if (!code.includes("registerServerReference")) return null;
|
|
20
|
+
|
|
21
|
+
let hasReplacement = false;
|
|
22
|
+
const result = code.replace(
|
|
23
|
+
registerServerReferenceRegex(),
|
|
24
|
+
(match, _value: string, refKey: string) => {
|
|
25
|
+
// The temp server is not a long-lived HMR server, so ids are bare; strip a
|
|
26
|
+
// query suffix defensively so the hash matches plugin-rsc's manifest key.
|
|
27
|
+
const cleanKey = refKey.split("?")[0]!;
|
|
28
|
+
const hash = computeProductionHash(projectRoot, cleanKey);
|
|
29
|
+
if (hash === cleanKey) return match;
|
|
30
|
+
hasReplacement = true;
|
|
31
|
+
return match.replace(`"${refKey}"`, `"${hash}"`);
|
|
32
|
+
},
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
return hasReplacement ? result : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Server-side analog of hashClientRefs. Runs in the build-discovery temp server
|
|
40
|
+
* (a dev/serve Vite server, where plugin-rsc mints dev-style ids), rewriting
|
|
41
|
+
* registerServerReference ids to production hashes AFTER plugin-rsc's
|
|
42
|
+
* rsc:use-server transform (enforce:"post"), so the Flight serializer bakes the
|
|
43
|
+
* production hash into the stored prerender/static payloads.
|
|
44
|
+
*
|
|
45
|
+
* Without this, a server-created action embedded in a prerendered/static Flight
|
|
46
|
+
* is stored with a dev-style id that the production hash-keyed server-references
|
|
47
|
+
* manifest cannot resolve -> "server reference not found" on a build-time-cache
|
|
48
|
+
* hit. Mirrors hashClientRefs, which already does exactly this for the client
|
|
49
|
+
* half of the same payload (which is why client refs in stored Flight are
|
|
50
|
+
* already production hashes while server refs were not).
|
|
51
|
+
*/
|
|
52
|
+
export function hashServerRefs(projectRoot: string): Plugin {
|
|
53
|
+
const counter = createCounter(debug, "hash-server-refs");
|
|
54
|
+
return {
|
|
55
|
+
name: "@rangojs/router:hash-server-refs",
|
|
56
|
+
enforce: "post",
|
|
57
|
+
applyToEnvironment(env) {
|
|
58
|
+
return env.name === "rsc";
|
|
59
|
+
},
|
|
60
|
+
buildEnd() {
|
|
61
|
+
counter?.flush();
|
|
62
|
+
},
|
|
63
|
+
transform(code, id) {
|
|
64
|
+
const start = counter ? performance.now() : 0;
|
|
65
|
+
try {
|
|
66
|
+
const result = transformServerRefs(code, projectRoot);
|
|
67
|
+
if (result === null) return;
|
|
68
|
+
return { code: result, map: null };
|
|
69
|
+
} finally {
|
|
70
|
+
counter?.record(id, performance.now() - start);
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// plugin-rsc's rsc:use-server transform emits inline/exported server references
|
|
2
|
+
// as `registerServerReference(<value>, "<id>", "<name>")`, where <value> is a
|
|
3
|
+
// hoisted identifier (no top-level comma) and a trailing `.bind(null,
|
|
4
|
+
// encryptActionBoundArgs(...))` lies OUTSIDE the call. Two plugins parse this
|
|
5
|
+
// same shape -- expose-action-id ($id wrapping) and server-ref-hashing (id ->
|
|
6
|
+
// production hash) -- so the pattern has a single owner here. Returns a fresh
|
|
7
|
+
// /g RegExp per call to avoid sharing lastIndex state across callers.
|
|
8
|
+
export function registerServerReferenceRegex(): RegExp {
|
|
9
|
+
return /registerServerReference\(([^,]+),\s*"([^"]+)",\s*"([^"]+)"\)/g;
|
|
10
|
+
}
|
package/src/vite/rango.ts
CHANGED
|
@@ -2,7 +2,9 @@ import { type PluginOption } from "vite";
|
|
|
2
2
|
import { readFileSync, existsSync } from "node:fs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { exposeActionId } from "./plugins/expose-action-id.js";
|
|
5
|
+
import { defineEncryptionKeyExpr } from "./encryption-key.js";
|
|
5
6
|
import {
|
|
7
|
+
createLoaderScanStubPlugin,
|
|
6
8
|
exposeInternalIds,
|
|
7
9
|
exposeRouterId,
|
|
8
10
|
} from "./plugins/expose-internal-ids.js";
|
|
@@ -275,6 +277,9 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
275
277
|
entries: finalEntries,
|
|
276
278
|
serverHandler: false,
|
|
277
279
|
clientChunks,
|
|
280
|
+
// Share one encryption key with the build-discovery temp server so
|
|
281
|
+
// build-time prerender/static inline-action bound args decrypt at runtime.
|
|
282
|
+
defineEncryptionKey: defineEncryptionKeyExpr(),
|
|
278
283
|
}) as PluginOption,
|
|
279
284
|
);
|
|
280
285
|
plugins.push(clientRefDedup());
|
|
@@ -521,6 +526,9 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
521
526
|
rsc({
|
|
522
527
|
entries: finalEntries,
|
|
523
528
|
clientChunks,
|
|
529
|
+
// Share one encryption key with the build-discovery temp server so
|
|
530
|
+
// build-time prerender/static inline-action bound args decrypt at runtime.
|
|
531
|
+
defineEncryptionKey: defineEncryptionKeyExpr(),
|
|
524
532
|
}) as PluginOption,
|
|
525
533
|
);
|
|
526
534
|
plugins.push(clientRefDedup());
|
|
@@ -551,6 +559,7 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
|
|
|
551
559
|
},
|
|
552
560
|
});
|
|
553
561
|
|
|
562
|
+
plugins.push(createLoaderScanStubPlugin());
|
|
554
563
|
plugins.push(exposeActionId());
|
|
555
564
|
plugins.push(useCacheTransform());
|
|
556
565
|
plugins.push(exposeInternalIds());
|
|
@@ -45,6 +45,8 @@ import {
|
|
|
45
45
|
exposeRouterId,
|
|
46
46
|
} from "./plugins/expose-internal-ids.js";
|
|
47
47
|
import { hashClientRefs } from "./plugins/client-ref-hashing.js";
|
|
48
|
+
import { hashServerRefs } from "./plugins/server-ref-hashing.js";
|
|
49
|
+
import { defineEncryptionKeyExpr } from "./encryption-key.js";
|
|
48
50
|
import { extractHandlerExportsFromChunk } from "./utils/bundle-analysis.js";
|
|
49
51
|
import {
|
|
50
52
|
createDiscoveryState,
|
|
@@ -224,9 +226,26 @@ async function createTempRscServer(
|
|
|
224
226
|
ssr: "virtual:entry-ssr",
|
|
225
227
|
rsc: state.resolvedEntryPath!,
|
|
226
228
|
},
|
|
229
|
+
// The temp server renders Static/Prerender output and encrypts inline-
|
|
230
|
+
// action bound args. Give it the SAME key as the main build/dev runtime
|
|
231
|
+
// (defineEncryptionKeyExpr is process-cached) so those args decrypt at
|
|
232
|
+
// invocation. Unconditional, NOT build-only: under Cloudflare/workerd dev
|
|
233
|
+
// the RSC env has no module runner, so prerender is rendered by THIS temp
|
|
234
|
+
// server (via /__rsc_prerender), and the action decrypts in the main
|
|
235
|
+
// runtime with the rango.ts key -- gating on forceBuild left dev encrypting
|
|
236
|
+
// with the temp server's own random key, failing decryptActionBoundArgs.
|
|
237
|
+
// (hashServerRefs stays build-only: dev keeps dev-style ids the dev runtime
|
|
238
|
+
// resolves directly.)
|
|
239
|
+
defineEncryptionKey: defineEncryptionKeyExpr(),
|
|
227
240
|
}),
|
|
228
|
-
// hashClientRefs only in build mode — production bundles
|
|
229
|
-
|
|
241
|
+
// hashClientRefs/hashServerRefs only in build mode — production bundles
|
|
242
|
+
// need hashed refs. hashServerRefs is the server-side analog: it rewrites
|
|
243
|
+
// registerServerReference dev-style ids to production hashes so a
|
|
244
|
+
// server-created action embedded in prerendered/static Flight resolves
|
|
245
|
+
// against the production manifest on a build-time-cache hit.
|
|
246
|
+
...(options.forceBuild
|
|
247
|
+
? [hashClientRefs(state.projectRoot), hashServerRefs(state.projectRoot)]
|
|
248
|
+
: []),
|
|
230
249
|
createVersionPlugin(),
|
|
231
250
|
// Before the stub plugin, so "virtual:entry-ssr" resolves to the real
|
|
232
251
|
// SSR entry when the shell endpoint needs it (see the option doc).
|
|
@@ -249,16 +249,21 @@ export function onwarn(
|
|
|
249
249
|
defaultHandler(warning);
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
/**
|
|
253
|
+
* All of `browser/prefetch/` belongs to the eager "router" chunk — including
|
|
254
|
+
* the modules reachable only via loader.ts's dynamic import. #766 split those
|
|
255
|
+
* into a separate lazy chunk to trim the eager runtime (~2.3 KB gzip), but
|
|
256
|
+
* production's `defaultPrefetch: "viewport"` loads them on almost every page,
|
|
257
|
+
* so the split became a document -> router -> runtime request waterfall
|
|
258
|
+
* (measured 502 ms critical-path latency in a deployed worker). Same-chunk
|
|
259
|
+
* placement resolves the dynamic import without a network fetch
|
|
260
|
+
* (`Promise.resolve().then(...)` in the emitted chunk), and the merged chunk
|
|
261
|
+
* gzips smaller than the two chunks did apart. Do not re-split without new
|
|
262
|
+
* information (AGENTS.md, Bundle hygiene, rejected optimizations).
|
|
263
|
+
*/
|
|
252
264
|
export function getManualChunks(id: string): string | undefined {
|
|
253
265
|
const normalized = Vite.normalizePath(id);
|
|
254
266
|
|
|
255
|
-
if (
|
|
256
|
-
/\/browser\/prefetch\/(?:runtime|fetch|queue|observer|policy|resource-ready)\.[cm]?[jt]sx?(?:[?#].*)?$/.test(
|
|
257
|
-
normalized,
|
|
258
|
-
)
|
|
259
|
-
) {
|
|
260
|
-
return undefined;
|
|
261
|
-
}
|
|
262
267
|
if (
|
|
263
268
|
normalized.includes("node_modules/react/") ||
|
|
264
269
|
normalized.includes("node_modules/react-dom/") ||
|