@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.
Files changed (65) hide show
  1. package/README.md +5 -1
  2. package/dist/types/browser/event-controller.d.ts +6 -0
  3. package/dist/types/browser/prefetch/cache.d.ts +31 -2
  4. package/dist/types/cache/cf/cf-cache-constants.d.ts +8 -1
  5. package/dist/types/cache/cf/cf-cache-store.d.ts +15 -1
  6. package/dist/types/cache/cf/cf-cache-types.d.ts +1 -1
  7. package/dist/types/cache/cf/cf-kv-utils.d.ts +21 -0
  8. package/dist/types/handles/is-thenable.d.ts +2 -4
  9. package/dist/types/route-definition/helpers-types.d.ts +5 -4
  10. package/dist/types/rsc/helpers.d.ts +3 -0
  11. package/dist/types/rsc/render-pipeline.d.ts +9 -0
  12. package/dist/types/rsc/routine-plan.d.ts +124 -0
  13. package/dist/types/rsc/shell-capture-constants.d.ts +17 -0
  14. package/dist/types/server/request-context.d.ts +4 -1
  15. package/dist/types/static-handler.d.ts +15 -1
  16. package/dist/types/urls/path-helper-types.d.ts +6 -7
  17. package/dist/types/vite/encryption-key.d.ts +2 -0
  18. package/dist/types/vite/plugins/expose-internal-ids.d.ts +10 -0
  19. package/dist/types/vite/plugins/server-ref-hashing.d.ts +24 -0
  20. package/dist/types/vite/plugins/server-reference-pattern.d.ts +1 -0
  21. package/dist/types/vite/utils/shared-utils.d.ts +12 -0
  22. package/dist/vite/index.js +154 -57
  23. package/package.json +3 -3
  24. package/skills/caching/SKILL.md +1 -1
  25. package/skills/mime-routes/SKILL.md +3 -1
  26. package/skills/parallel/SKILL.md +1 -1
  27. package/skills/response-routes/SKILL.md +4 -2
  28. package/skills/typesafety/generated-files-and-cli.md +16 -9
  29. package/skills/typesafety/route-types.md +5 -1
  30. package/skills/use-cache/SKILL.md +47 -0
  31. package/src/browser/event-controller.ts +40 -15
  32. package/src/browser/partial-update.ts +26 -11
  33. package/src/browser/prefetch/cache.ts +53 -9
  34. package/src/browser/prefetch/fetch.ts +83 -2
  35. package/src/browser/react/NavigationProvider.tsx +7 -0
  36. package/src/cache/cache-runtime.ts +89 -15
  37. package/src/cache/cf/cf-cache-constants.ts +8 -1
  38. package/src/cache/cf/cf-cache-store.ts +41 -64
  39. package/src/cache/cf/cf-cache-types.ts +1 -1
  40. package/src/cache/cf/cf-kv-utils.ts +38 -0
  41. package/src/cache/segment-codec.ts +21 -5
  42. package/src/handles/is-thenable.ts +2 -4
  43. package/src/route-definition/helpers-types.ts +5 -3
  44. package/src/router/match-middleware/cache-lookup.ts +24 -15
  45. package/src/rsc/handler.ts +26 -5
  46. package/src/rsc/helpers.ts +13 -0
  47. package/src/rsc/progressive-enhancement.ts +247 -70
  48. package/src/rsc/render-pipeline.ts +68 -24
  49. package/src/rsc/routine-plan.ts +359 -0
  50. package/src/rsc/rsc-rendering.ts +555 -302
  51. package/src/rsc/server-action.ts +180 -71
  52. package/src/rsc/shell-capture-constants.ts +18 -0
  53. package/src/rsc/shell-capture.ts +92 -21
  54. package/src/server/request-context.ts +6 -0
  55. package/src/ssr/ssr-root.tsx +1 -0
  56. package/src/static-handler.ts +18 -2
  57. package/src/urls/path-helper-types.ts +9 -10
  58. package/src/vite/encryption-key.ts +29 -0
  59. package/src/vite/plugins/expose-action-id.ts +2 -2
  60. package/src/vite/plugins/expose-internal-ids.ts +46 -0
  61. package/src/vite/plugins/server-ref-hashing.ts +74 -0
  62. package/src/vite/plugins/server-reference-pattern.ts +10 -0
  63. package/src/vite/rango.ts +9 -0
  64. package/src/vite/router-discovery.ts +21 -2
  65. 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 need hashed refs
229
- ...(options.forceBuild ? [hashClientRefs(state.projectRoot)] : []),
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/") ||