@rangojs/router 0.0.0-experimental.7 → 0.0.0-experimental.70

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 (307) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +942 -4
  3. package/dist/bin/rango.js +1689 -0
  4. package/dist/vite/index.js +4951 -930
  5. package/package.json +70 -60
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +294 -0
  8. package/skills/caching/SKILL.md +93 -23
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +12 -8
  11. package/skills/document-cache/SKILL.md +18 -16
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/hooks/SKILL.md +334 -72
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +131 -8
  16. package/skills/layout/SKILL.md +100 -3
  17. package/skills/links/SKILL.md +92 -31
  18. package/skills/loader/SKILL.md +404 -44
  19. package/skills/middleware/SKILL.md +173 -34
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +204 -1
  22. package/skills/prerender/SKILL.md +685 -0
  23. package/skills/rango/SKILL.md +85 -16
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +257 -14
  26. package/skills/router-setup/SKILL.md +210 -32
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +9 -8
  29. package/skills/typesafety/SKILL.md +328 -89
  30. package/skills/use-cache/SKILL.md +324 -0
  31. package/src/__internal.ts +102 -4
  32. package/src/bin/rango.ts +321 -0
  33. package/src/browser/action-coordinator.ts +97 -0
  34. package/src/browser/action-response-classifier.ts +99 -0
  35. package/src/browser/app-version.ts +14 -0
  36. package/src/browser/event-controller.ts +92 -64
  37. package/src/browser/history-state.ts +80 -0
  38. package/src/browser/intercept-utils.ts +52 -0
  39. package/src/browser/link-interceptor.ts +24 -4
  40. package/src/browser/logging.ts +55 -0
  41. package/src/browser/merge-segment-loaders.ts +20 -12
  42. package/src/browser/navigation-bridge.ts +296 -558
  43. package/src/browser/navigation-client.ts +179 -69
  44. package/src/browser/navigation-store.ts +73 -55
  45. package/src/browser/navigation-transaction.ts +297 -0
  46. package/src/browser/network-error-handler.ts +61 -0
  47. package/src/browser/partial-update.ts +328 -313
  48. package/src/browser/prefetch/cache.ts +206 -0
  49. package/src/browser/prefetch/fetch.ts +150 -0
  50. package/src/browser/prefetch/observer.ts +65 -0
  51. package/src/browser/prefetch/policy.ts +48 -0
  52. package/src/browser/prefetch/queue.ts +160 -0
  53. package/src/browser/prefetch/resource-ready.ts +77 -0
  54. package/src/browser/rango-state.ts +112 -0
  55. package/src/browser/react/Link.tsx +230 -74
  56. package/src/browser/react/NavigationProvider.tsx +87 -11
  57. package/src/browser/react/context.ts +11 -0
  58. package/src/browser/react/filter-segment-order.ts +11 -0
  59. package/src/browser/react/index.ts +12 -12
  60. package/src/browser/react/location-state-shared.ts +95 -53
  61. package/src/browser/react/location-state.ts +60 -15
  62. package/src/browser/react/mount-context.ts +6 -1
  63. package/src/browser/react/nonce-context.ts +23 -0
  64. package/src/browser/react/shallow-equal.ts +27 -0
  65. package/src/browser/react/use-action.ts +29 -51
  66. package/src/browser/react/use-client-cache.ts +5 -3
  67. package/src/browser/react/use-handle.ts +30 -126
  68. package/src/browser/react/use-href.tsx +2 -2
  69. package/src/browser/react/use-link-status.ts +6 -5
  70. package/src/browser/react/use-navigation.ts +22 -63
  71. package/src/browser/react/use-params.ts +65 -0
  72. package/src/browser/react/use-pathname.ts +47 -0
  73. package/src/browser/react/use-router.ts +76 -0
  74. package/src/browser/react/use-search-params.ts +56 -0
  75. package/src/browser/react/use-segments.ts +80 -97
  76. package/src/browser/response-adapter.ts +73 -0
  77. package/src/browser/rsc-router.tsx +214 -58
  78. package/src/browser/scroll-restoration.ts +127 -52
  79. package/src/browser/segment-reconciler.ts +221 -0
  80. package/src/browser/segment-structure-assert.ts +16 -0
  81. package/src/browser/server-action-bridge.ts +510 -603
  82. package/src/browser/shallow.ts +6 -1
  83. package/src/browser/types.ts +141 -48
  84. package/src/browser/validate-redirect-origin.ts +29 -0
  85. package/src/build/generate-manifest.ts +235 -24
  86. package/src/build/generate-route-types.ts +39 -0
  87. package/src/build/index.ts +13 -0
  88. package/src/build/route-trie.ts +265 -0
  89. package/src/build/route-types/ast-helpers.ts +25 -0
  90. package/src/build/route-types/ast-route-extraction.ts +98 -0
  91. package/src/build/route-types/codegen.ts +102 -0
  92. package/src/build/route-types/include-resolution.ts +418 -0
  93. package/src/build/route-types/param-extraction.ts +48 -0
  94. package/src/build/route-types/per-module-writer.ts +128 -0
  95. package/src/build/route-types/router-processing.ts +618 -0
  96. package/src/build/route-types/scan-filter.ts +85 -0
  97. package/src/build/runtime-discovery.ts +231 -0
  98. package/src/cache/background-task.ts +34 -0
  99. package/src/cache/cache-key-utils.ts +44 -0
  100. package/src/cache/cache-policy.ts +125 -0
  101. package/src/cache/cache-runtime.ts +342 -0
  102. package/src/cache/cache-scope.ts +167 -309
  103. package/src/cache/cf/cf-cache-store.ts +571 -17
  104. package/src/cache/cf/index.ts +13 -3
  105. package/src/cache/document-cache.ts +116 -77
  106. package/src/cache/handle-capture.ts +81 -0
  107. package/src/cache/handle-snapshot.ts +41 -0
  108. package/src/cache/index.ts +1 -15
  109. package/src/cache/memory-segment-store.ts +191 -13
  110. package/src/cache/profile-registry.ts +73 -0
  111. package/src/cache/read-through-swr.ts +134 -0
  112. package/src/cache/segment-codec.ts +256 -0
  113. package/src/cache/taint.ts +153 -0
  114. package/src/cache/types.ts +72 -122
  115. package/src/client.rsc.tsx +3 -1
  116. package/src/client.tsx +105 -179
  117. package/src/component-utils.ts +4 -4
  118. package/src/components/DefaultDocument.tsx +5 -1
  119. package/src/context-var.ts +156 -0
  120. package/src/debug.ts +19 -9
  121. package/src/errors.ts +108 -2
  122. package/src/handle.ts +55 -29
  123. package/src/handles/MetaTags.tsx +73 -20
  124. package/src/handles/breadcrumbs.ts +66 -0
  125. package/src/handles/index.ts +1 -0
  126. package/src/handles/meta.ts +30 -13
  127. package/src/host/cookie-handler.ts +21 -15
  128. package/src/host/errors.ts +8 -8
  129. package/src/host/index.ts +4 -7
  130. package/src/host/pattern-matcher.ts +27 -27
  131. package/src/host/router.ts +61 -39
  132. package/src/host/testing.ts +8 -8
  133. package/src/host/types.ts +15 -7
  134. package/src/host/utils.ts +1 -1
  135. package/src/href-client.ts +119 -29
  136. package/src/index.rsc.ts +155 -19
  137. package/src/index.ts +223 -30
  138. package/src/internal-debug.ts +11 -0
  139. package/src/loader.rsc.ts +26 -157
  140. package/src/loader.ts +27 -10
  141. package/src/network-error-thrower.tsx +3 -1
  142. package/src/outlet-provider.tsx +45 -0
  143. package/src/prerender/param-hash.ts +37 -0
  144. package/src/prerender/store.ts +186 -0
  145. package/src/prerender.ts +524 -0
  146. package/src/reverse.ts +351 -0
  147. package/src/root-error-boundary.tsx +41 -29
  148. package/src/route-content-wrapper.tsx +7 -4
  149. package/src/route-definition/dsl-helpers.ts +982 -0
  150. package/src/route-definition/helper-factories.ts +200 -0
  151. package/src/route-definition/helpers-types.ts +434 -0
  152. package/src/route-definition/index.ts +55 -0
  153. package/src/route-definition/redirect.ts +101 -0
  154. package/src/route-definition/resolve-handler-use.ts +149 -0
  155. package/src/route-definition.ts +1 -1428
  156. package/src/route-map-builder.ts +217 -123
  157. package/src/route-name.ts +53 -0
  158. package/src/route-types.ts +70 -8
  159. package/src/router/content-negotiation.ts +215 -0
  160. package/src/router/debug-manifest.ts +72 -0
  161. package/src/router/error-handling.ts +9 -9
  162. package/src/router/find-match.ts +160 -0
  163. package/src/router/handler-context.ts +435 -86
  164. package/src/router/intercept-resolution.ts +402 -0
  165. package/src/router/lazy-includes.ts +237 -0
  166. package/src/router/loader-resolution.ts +356 -128
  167. package/src/router/logging.ts +251 -0
  168. package/src/router/manifest.ts +154 -35
  169. package/src/router/match-api.ts +555 -0
  170. package/src/router/match-context.ts +5 -3
  171. package/src/router/match-handlers.ts +440 -0
  172. package/src/router/match-middleware/background-revalidation.ts +108 -93
  173. package/src/router/match-middleware/cache-lookup.ts +459 -10
  174. package/src/router/match-middleware/cache-store.ts +98 -26
  175. package/src/router/match-middleware/intercept-resolution.ts +57 -17
  176. package/src/router/match-middleware/segment-resolution.ts +80 -6
  177. package/src/router/match-pipelines.ts +10 -45
  178. package/src/router/match-result.ts +55 -33
  179. package/src/router/metrics.ts +240 -15
  180. package/src/router/middleware-cookies.ts +55 -0
  181. package/src/router/middleware-types.ts +220 -0
  182. package/src/router/middleware.ts +324 -369
  183. package/src/router/navigation-snapshot.ts +182 -0
  184. package/src/router/pattern-matching.ts +211 -43
  185. package/src/router/prerender-match.ts +502 -0
  186. package/src/router/preview-match.ts +98 -0
  187. package/src/router/request-classification.ts +310 -0
  188. package/src/router/revalidation.ts +137 -38
  189. package/src/router/route-snapshot.ts +245 -0
  190. package/src/router/router-context.ts +41 -21
  191. package/src/router/router-interfaces.ts +484 -0
  192. package/src/router/router-options.ts +618 -0
  193. package/src/router/router-registry.ts +24 -0
  194. package/src/router/segment-resolution/fresh.ts +743 -0
  195. package/src/router/segment-resolution/helpers.ts +268 -0
  196. package/src/router/segment-resolution/loader-cache.ts +199 -0
  197. package/src/router/segment-resolution/revalidation.ts +1373 -0
  198. package/src/router/segment-resolution/static-store.ts +67 -0
  199. package/src/router/segment-resolution.ts +21 -0
  200. package/src/router/segment-wrappers.ts +291 -0
  201. package/src/router/telemetry-otel.ts +299 -0
  202. package/src/router/telemetry.ts +300 -0
  203. package/src/router/timeout.ts +148 -0
  204. package/src/router/trie-matching.ts +239 -0
  205. package/src/router/types.ts +78 -3
  206. package/src/router.ts +740 -4252
  207. package/src/rsc/handler-context.ts +45 -0
  208. package/src/rsc/handler.ts +907 -797
  209. package/src/rsc/helpers.ts +140 -6
  210. package/src/rsc/index.ts +0 -20
  211. package/src/rsc/loader-fetch.ts +229 -0
  212. package/src/rsc/manifest-init.ts +90 -0
  213. package/src/rsc/nonce.ts +14 -0
  214. package/src/rsc/origin-guard.ts +141 -0
  215. package/src/rsc/progressive-enhancement.ts +391 -0
  216. package/src/rsc/response-error.ts +37 -0
  217. package/src/rsc/response-route-handler.ts +347 -0
  218. package/src/rsc/rsc-rendering.ts +246 -0
  219. package/src/rsc/runtime-warnings.ts +42 -0
  220. package/src/rsc/server-action.ts +356 -0
  221. package/src/rsc/ssr-setup.ts +128 -0
  222. package/src/rsc/types.ts +46 -11
  223. package/src/search-params.ts +230 -0
  224. package/src/segment-system.tsx +165 -17
  225. package/src/server/context.ts +315 -58
  226. package/src/server/cookie-store.ts +190 -0
  227. package/src/server/fetchable-loader-store.ts +37 -0
  228. package/src/server/handle-store.ts +113 -15
  229. package/src/server/loader-registry.ts +24 -64
  230. package/src/server/request-context.ts +607 -81
  231. package/src/server.ts +35 -130
  232. package/src/ssr/index.tsx +103 -30
  233. package/src/static-handler.ts +126 -0
  234. package/src/theme/ThemeProvider.tsx +21 -15
  235. package/src/theme/ThemeScript.tsx +5 -5
  236. package/src/theme/constants.ts +5 -2
  237. package/src/theme/index.ts +4 -14
  238. package/src/theme/theme-context.ts +4 -30
  239. package/src/theme/theme-script.ts +21 -18
  240. package/src/types/boundaries.ts +158 -0
  241. package/src/types/cache-types.ts +198 -0
  242. package/src/types/error-types.ts +192 -0
  243. package/src/types/global-namespace.ts +100 -0
  244. package/src/types/handler-context.ts +791 -0
  245. package/src/types/index.ts +88 -0
  246. package/src/types/loader-types.ts +210 -0
  247. package/src/types/route-config.ts +170 -0
  248. package/src/types/route-entry.ts +109 -0
  249. package/src/types/segments.ts +150 -0
  250. package/src/types.ts +1 -1623
  251. package/src/urls/include-helper.ts +197 -0
  252. package/src/urls/index.ts +53 -0
  253. package/src/urls/path-helper-types.ts +346 -0
  254. package/src/urls/path-helper.ts +364 -0
  255. package/src/urls/pattern-types.ts +107 -0
  256. package/src/urls/response-types.ts +116 -0
  257. package/src/urls/type-extraction.ts +372 -0
  258. package/src/urls/urls-function.ts +98 -0
  259. package/src/urls.ts +1 -802
  260. package/src/use-loader.tsx +161 -81
  261. package/src/vite/discovery/bundle-postprocess.ts +181 -0
  262. package/src/vite/discovery/discover-routers.ts +348 -0
  263. package/src/vite/discovery/prerender-collection.ts +439 -0
  264. package/src/vite/discovery/route-types-writer.ts +258 -0
  265. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  266. package/src/vite/discovery/state.ts +117 -0
  267. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  268. package/src/vite/index.ts +15 -1129
  269. package/src/vite/plugin-types.ts +103 -0
  270. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  271. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  272. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  273. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
  274. package/src/vite/plugins/expose-id-utils.ts +299 -0
  275. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  276. package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
  277. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  278. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  279. package/src/vite/plugins/expose-ids/types.ts +45 -0
  280. package/src/vite/plugins/expose-internal-ids.ts +786 -0
  281. package/src/vite/plugins/performance-tracks.ts +88 -0
  282. package/src/vite/plugins/refresh-cmd.ts +127 -0
  283. package/src/vite/plugins/use-cache-transform.ts +323 -0
  284. package/src/vite/plugins/version-injector.ts +83 -0
  285. package/src/vite/plugins/version-plugin.ts +266 -0
  286. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  287. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  288. package/src/vite/rango.ts +462 -0
  289. package/src/vite/router-discovery.ts +918 -0
  290. package/src/vite/utils/ast-handler-extract.ts +517 -0
  291. package/src/vite/utils/banner.ts +36 -0
  292. package/src/vite/utils/bundle-analysis.ts +137 -0
  293. package/src/vite/utils/manifest-utils.ts +70 -0
  294. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  295. package/src/vite/utils/prerender-utils.ts +207 -0
  296. package/src/vite/utils/shared-utils.ts +170 -0
  297. package/CLAUDE.md +0 -43
  298. package/src/browser/lru-cache.ts +0 -69
  299. package/src/browser/request-controller.ts +0 -164
  300. package/src/cache/memory-store.ts +0 -253
  301. package/src/href-context.ts +0 -33
  302. package/src/href.ts +0 -255
  303. package/src/server/route-manifest-cache.ts +0 -173
  304. package/src/vite/expose-handle-id.ts +0 -209
  305. package/src/vite/expose-loader-id.ts +0 -426
  306. package/src/vite/expose-location-state-id.ts +0 -177
  307. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -0,0 +1,88 @@
1
+ /**
2
+ * React Performance Tracks — RSDW client patch
3
+ *
4
+ * Patches the RSDW client so _debugInfo recovery works for plain-object
5
+ * payloads (our RscPayload shape). Without this, the Server Components
6
+ * track in Chrome DevTools stays empty.
7
+ *
8
+ * React's flushComponentPerformance uses splice(0) to empty _debugInfo
9
+ * after resolution, then recovers it from the resolved value — but only
10
+ * for arrays, async iterables, React elements, and lazy types. Since our
11
+ * RscPayload is a plain object, _debugInfo is lost. This patch relaxes
12
+ * the check so _debugInfo is recovered from any object.
13
+ */
14
+
15
+ import type { Plugin } from "vite";
16
+ import { readFile } from "node:fs/promises";
17
+
18
+ const RSDW_PATCH_RE =
19
+ /((?:var|let|const)\s+\w+\s*=\s*root\._children\s*,\s*(\w+)\s*=\s*root\._debugInfo\s*[;,])/;
20
+
21
+ function buildPatchReplacement(match: string, debugInfoVar: string): string {
22
+ return `${match}
23
+ if (${debugInfoVar} && 0 === ${debugInfoVar}.length && "fulfilled" === root.status) {
24
+ var _resolved = "function" === typeof resolveLazy ? resolveLazy(root.value) : root.value;
25
+ if ("object" === typeof _resolved && null !== _resolved && isArrayImpl(_resolved._debugInfo)) {
26
+ ${debugInfoVar} = _resolved._debugInfo;
27
+ }
28
+ }`;
29
+ }
30
+
31
+ export function patchRsdwClientDebugInfoRecovery(code: string): {
32
+ code: string;
33
+ debugInfoVar: string | null;
34
+ } {
35
+ const match = code.match(RSDW_PATCH_RE);
36
+ if (!match) {
37
+ return { code, debugInfoVar: null };
38
+ }
39
+
40
+ return {
41
+ code: code.replace(match[1]!, buildPatchReplacement(match[1]!, match[2]!)),
42
+ debugInfoVar: match[2]!,
43
+ };
44
+ }
45
+
46
+ export function performanceTracksOptimizeDepsPlugin(): {
47
+ name: string;
48
+ setup(build: any): void;
49
+ } {
50
+ return {
51
+ name: "@rangojs/router:performance-tracks-optimize-deps",
52
+ setup(build: any): void {
53
+ build.onLoad(
54
+ {
55
+ filter:
56
+ /react-server-dom-webpack-client\.browser\.(development|production)\.js$/,
57
+ },
58
+ async (args: { path: string }) => {
59
+ const code = await readFile(args.path, "utf8");
60
+ const patched = patchRsdwClientDebugInfoRecovery(code);
61
+ return {
62
+ contents: patched.code,
63
+ loader: "js",
64
+ };
65
+ },
66
+ );
67
+ },
68
+ };
69
+ }
70
+
71
+ export function performanceTracksPlugin(): Plugin {
72
+ return {
73
+ name: "@rangojs/router:performance-tracks",
74
+
75
+ transform(code, id) {
76
+ if (!id.includes("react-server-dom") || !id.includes("client")) return;
77
+ const patched = patchRsdwClientDebugInfoRecovery(code);
78
+ if (!patched.debugInfoVar) return;
79
+ if (process.env.INTERNAL_RANGO_DEBUG)
80
+ console.log(
81
+ "[perf-tracks] patched RSDW client (var:",
82
+ patched.debugInfoVar,
83
+ ")",
84
+ );
85
+ return patched.code;
86
+ },
87
+ };
88
+ }
@@ -0,0 +1,127 @@
1
+ import type { Plugin } from "vite";
2
+
3
+ /**
4
+ * Vite plugin that triggers a full browser reload from terminal input.
5
+ *
6
+ * This plugin is intentionally passive:
7
+ * - it never enables raw mode on stdin
8
+ * - it never restores terminal state
9
+ * - it reacts to Ctrl+R when that raw byte reaches the process
10
+ * - it also supports safe line-based fallbacks like "e" + Enter
11
+ *
12
+ * Usage:
13
+ * ```ts
14
+ * import { poke } from "@rangojs/router/vite";
15
+ *
16
+ * export default defineConfig({
17
+ * plugins: [rango(), poke()],
18
+ * });
19
+ * ```
20
+ */
21
+ export function poke(): Plugin {
22
+ return {
23
+ name: "vite-plugin-poke",
24
+ apply: "serve",
25
+
26
+ configureServer(server) {
27
+ const stdin = process.stdin;
28
+ const debug = process.env.RANGO_POKE_DEBUG === "1";
29
+
30
+ const triggerReload = (source: string) => {
31
+ server.hot.send({ type: "full-reload", path: "*" });
32
+ server.config.logger.info(` browser reload (${source})`, {
33
+ timestamp: true,
34
+ });
35
+ };
36
+
37
+ const toBuffer = (chunk: string | Buffer): Buffer => {
38
+ return typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
39
+ };
40
+
41
+ const formatChunk = (chunk: string | Buffer): string => {
42
+ const data = toBuffer(chunk);
43
+ const hex = Array.from(data)
44
+ .map((byte) => `0x${byte.toString(16).padStart(2, "0")}`)
45
+ .join(" ");
46
+ const ascii = Array.from(data)
47
+ .map((byte) => {
48
+ if (byte >= 0x20 && byte <= 0x7e) return String.fromCharCode(byte);
49
+ if (byte === 0x0a) return "\\n";
50
+ if (byte === 0x0d) return "\\r";
51
+ if (byte === 0x09) return "\\t";
52
+ return ".";
53
+ })
54
+ .join("");
55
+ return `len=${data.length} hex=[${hex}] ascii="${ascii}"`;
56
+ };
57
+
58
+ const readCtrlR = (chunk: string | Buffer): boolean => {
59
+ const data =
60
+ typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
61
+ return data.length === 1 && data[0] === 0x12;
62
+ };
63
+
64
+ const readSubmittedCommands = (chunk: string | Buffer): string[] => {
65
+ const text = toBuffer(chunk)
66
+ .toString("utf8")
67
+ .replace(/\r\n/g, "\n")
68
+ .replace(/\r/g, "\n");
69
+
70
+ if (!text.includes("\n")) return [];
71
+
72
+ const lines = text.split("\n");
73
+ lines.pop();
74
+ return lines;
75
+ };
76
+
77
+ if (debug) {
78
+ server.config.logger.info(
79
+ ` poke debug enabled (isTTY=${stdin.isTTY ? "yes" : "no"}, isRaw=${stdin.isTTY ? (stdin.isRaw ? "yes" : "no") : "n/a"})`,
80
+ { timestamp: true },
81
+ );
82
+ }
83
+
84
+ if (stdin.isTTY) {
85
+ server.config.logger.info(
86
+ " poke ready: press e + enter to reload browser (ctrl+r also works when available)",
87
+ { timestamp: true },
88
+ );
89
+ }
90
+
91
+ const onData = (data: string | Buffer) => {
92
+ if (debug) {
93
+ server.config.logger.info(` poke stdin ${formatChunk(data)}`, {
94
+ timestamp: true,
95
+ });
96
+ }
97
+
98
+ // Only react to the exact Ctrl+R byte when some host terminal or
99
+ // wrapper already delivers it to this process. We intentionally do
100
+ // not enable raw mode here because that can steal Vite shortcuts
101
+ // like "r" / "q" and interfere with terminal-level controls.
102
+ if (readCtrlR(data)) {
103
+ triggerReload("ctrl+r");
104
+ return;
105
+ }
106
+
107
+ for (const command of readSubmittedCommands(data)) {
108
+ if (command === "e") {
109
+ triggerReload("e+enter");
110
+ return;
111
+ }
112
+
113
+ if (command === "\u001br") {
114
+ triggerReload("option+r+enter");
115
+ return;
116
+ }
117
+ }
118
+ };
119
+
120
+ stdin.on("data", onData);
121
+
122
+ server.httpServer?.on("close", () => {
123
+ stdin.off("data", onData);
124
+ });
125
+ },
126
+ };
127
+ }
@@ -0,0 +1,323 @@
1
+ /**
2
+ * "use cache" Vite Transform Plugin
3
+ *
4
+ * Detects "use cache" directives at file-level and function-level,
5
+ * then wraps exports with registerCachedFunction() from the cache runtime.
6
+ *
7
+ * File-level: "use cache" at top of file wraps all exports (except
8
+ * layout/template default exports which receive children).
9
+ *
10
+ * Function-level: "use cache: profileName" inside a function body
11
+ * hoists the function and wraps it.
12
+ *
13
+ * Uses transform helpers from @vitejs/plugin-rsc/transforms:
14
+ * - hasDirective() for file-level detection
15
+ * - transformWrapExport() for file-level wrapping
16
+ * - transformHoistInlineDirective() for function-level hoisting
17
+ */
18
+
19
+ import type { Plugin } from "vite";
20
+ import path from "node:path";
21
+ import MagicString from "magic-string";
22
+ import { normalizePath, hashId } from "./expose-id-utils.js";
23
+
24
+ const CACHE_RUNTIME_IMPORT = "@rangojs/router/cache-runtime";
25
+
26
+ // Files whose default export receives {children} from the framework
27
+ // and should not be wrapped (children can't be cache-keyed).
28
+ const LAYOUT_TEMPLATE_PATTERN = /\/(layout|template)\.(tsx?|jsx?)$/;
29
+
30
+ export function useCacheTransform(): Plugin {
31
+ let projectRoot = "";
32
+ let isBuild = false;
33
+ let rscTransforms: typeof import("@vitejs/plugin-rsc/transforms") | null =
34
+ null;
35
+
36
+ return {
37
+ name: "@rangojs/router:use-cache",
38
+ enforce: "post",
39
+
40
+ configResolved(config) {
41
+ projectRoot = config.root;
42
+ isBuild = config.command === "build";
43
+ },
44
+
45
+ async transform(code, id) {
46
+ // Only process in RSC environment
47
+ if (this.environment?.name !== "rsc") return;
48
+
49
+ // Quick bail: no "use cache" in source
50
+ if (!code.includes("use cache")) return;
51
+
52
+ // Skip node_modules and virtual modules
53
+ if (id.includes("/node_modules/") || id.startsWith("\0")) return;
54
+
55
+ // Only JS/TS files
56
+ if (!/\.(tsx?|jsx?|mjs)$/.test(id)) return;
57
+
58
+ // Lazy-load transform helpers
59
+ if (!rscTransforms) {
60
+ try {
61
+ rscTransforms = await import("@vitejs/plugin-rsc/transforms");
62
+ } catch {
63
+ return;
64
+ }
65
+ }
66
+
67
+ const {
68
+ hasDirective,
69
+ transformWrapExport,
70
+ transformHoistInlineDirective,
71
+ } = rscTransforms;
72
+
73
+ // Parse AST
74
+ let ast: any;
75
+ try {
76
+ const { parseAst } = await import("vite");
77
+ ast = parseAst(code);
78
+ } catch {
79
+ return;
80
+ }
81
+
82
+ const filePath = normalizePath(path.relative(projectRoot, id));
83
+ const isLayoutOrTemplate = LAYOUT_TEMPLATE_PATTERN.test(id);
84
+
85
+ // Check for file-level "use cache"
86
+ if (hasDirective(ast.body, "use cache")) {
87
+ return transformFileLevelUseCache(
88
+ code,
89
+ ast,
90
+ filePath,
91
+ id,
92
+ isBuild,
93
+ isLayoutOrTemplate,
94
+ transformWrapExport,
95
+ );
96
+ }
97
+
98
+ // Check for function-level "use cache" / "use cache: profileName"
99
+ // (only if there's no file-level directive but code still contains the string)
100
+ const functionResult = transformFunctionLevelUseCache(
101
+ code,
102
+ ast,
103
+ filePath,
104
+ id,
105
+ isBuild,
106
+ transformHoistInlineDirective,
107
+ );
108
+
109
+ // Always check for near-miss directives, even when valid directives
110
+ // exist. A file may contain both valid and invalid "use cache" directives
111
+ // in different functions — the invalid ones should still warn.
112
+ warnOnNearMissDirectives(ast, id, this.warn.bind(this));
113
+
114
+ if (functionResult) return functionResult;
115
+ },
116
+ };
117
+ }
118
+
119
+ function transformFileLevelUseCache(
120
+ code: string,
121
+ ast: any,
122
+ filePath: string,
123
+ sourceId: string,
124
+ isBuild: boolean,
125
+ isLayoutOrTemplate: boolean,
126
+ transformWrapExport: (typeof import("@vitejs/plugin-rsc/transforms"))["transformWrapExport"],
127
+ ) {
128
+ // Collect non-function exports to report after wrapping
129
+ const nonFunctionExports: string[] = [];
130
+
131
+ const { exportNames, output } = transformWrapExport(code, ast, {
132
+ runtime: (value: string, name: string) => {
133
+ const funcId = isBuild ? hashId(filePath, name) : `${filePath}#${name}`;
134
+ return `__rango_registerCachedFunction(${value}, ${JSON.stringify(funcId)}, "default")`;
135
+ },
136
+ rejectNonAsyncFunction: false,
137
+ filter: (name: string, meta: { isFunction?: boolean }) => {
138
+ // Skip default export of layout/template files (they receive children)
139
+ if (name === "default" && isLayoutOrTemplate) return false;
140
+ // Non-function exports cannot be wrapped with registerCachedFunction
141
+ if (meta.isFunction === false) {
142
+ nonFunctionExports.push(name);
143
+ return false;
144
+ }
145
+ return true;
146
+ },
147
+ });
148
+
149
+ if (nonFunctionExports.length > 0) {
150
+ throw new Error(
151
+ `[rango:use-cache] File-level "use cache" in ${sourceId} cannot wrap ` +
152
+ `non-function export${nonFunctionExports.length > 1 ? "s" : ""}: ` +
153
+ `${nonFunctionExports.map((n) => `"${n}"`).join(", ")}. ` +
154
+ `Only function exports can be cached. Either remove "use cache" from ` +
155
+ `the file level and add it inside individual functions, or move the ` +
156
+ `non-function exports to a separate module.`,
157
+ );
158
+ }
159
+
160
+ if (exportNames.length === 0) {
161
+ // Even if no exports were wrapped, strip the directive
162
+ const s = new MagicString(code);
163
+ const directive = findFileLevelDirective(ast);
164
+ if (directive) {
165
+ s.overwrite(
166
+ directive.start,
167
+ directive.end,
168
+ `/* "use cache" -- wrapped by rango */`,
169
+ );
170
+ return {
171
+ code: s.toString(),
172
+ map: s.generateMap({ source: sourceId, hires: "boundary" }),
173
+ };
174
+ }
175
+ return;
176
+ }
177
+
178
+ // Prepend the import
179
+ output.prepend(
180
+ `import { registerCachedFunction as __rango_registerCachedFunction } from ${JSON.stringify(CACHE_RUNTIME_IMPORT)};\n`,
181
+ );
182
+
183
+ // Replace the directive with a comment
184
+ const directive = findFileLevelDirective(ast);
185
+ if (directive) {
186
+ output.overwrite(
187
+ directive.start,
188
+ directive.end,
189
+ `/* "use cache" -- wrapped by rango */`,
190
+ );
191
+ }
192
+
193
+ return {
194
+ code: output.toString(),
195
+ map: output.generateMap({ source: sourceId, hires: "boundary" }),
196
+ };
197
+ }
198
+
199
+ function transformFunctionLevelUseCache(
200
+ code: string,
201
+ ast: any,
202
+ filePath: string,
203
+ sourceId: string,
204
+ isBuild: boolean,
205
+ transformHoistInlineDirective: (typeof import("@vitejs/plugin-rsc/transforms"))["transformHoistInlineDirective"],
206
+ ) {
207
+ try {
208
+ const { output, names } = transformHoistInlineDirective(code, ast, {
209
+ directive: /^use cache(:\s*[\w-]+)?$/,
210
+ runtime: (
211
+ value: string,
212
+ name: string,
213
+ meta: { directiveMatch: RegExpMatchArray },
214
+ ) => {
215
+ const funcId = isBuild ? hashId(filePath, name) : `${filePath}#${name}`;
216
+ const profileMatch = meta.directiveMatch[1];
217
+ const profileName = profileMatch
218
+ ? profileMatch.replace(/^:\s*/, "").trim()
219
+ : "default";
220
+ return `__rango_registerCachedFunction(${value}, ${JSON.stringify(funcId)}, ${JSON.stringify(profileName)})`;
221
+ },
222
+ rejectNonAsyncFunction: false,
223
+ });
224
+
225
+ if (names.length === 0) return;
226
+
227
+ // Use a top-level import instead of await import() — the hoisted wrapper
228
+ // may be placed in a non-async context (e.g., inside a synchronous
229
+ // urls() callback) where await is not allowed.
230
+ output.prepend(
231
+ `import { registerCachedFunction as __rango_registerCachedFunction } from ${JSON.stringify(CACHE_RUNTIME_IMPORT)};\n`,
232
+ );
233
+
234
+ return {
235
+ code: output.toString(),
236
+ map: output.generateMap({ source: sourceId, hires: "boundary" }),
237
+ };
238
+ } catch {
239
+ // Transform failed (e.g., syntax not supported), skip
240
+ return;
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Find the file-level "use cache" directive AST node for removal.
246
+ */
247
+ function findFileLevelDirective(
248
+ ast: any,
249
+ ): { start: number; end: number } | null {
250
+ for (const node of ast.body ?? []) {
251
+ if (
252
+ node.type === "ExpressionStatement" &&
253
+ node.expression?.type === "Literal" &&
254
+ typeof node.expression.value === "string" &&
255
+ node.expression.value.startsWith("use cache")
256
+ ) {
257
+ return { start: node.start, end: node.end };
258
+ }
259
+ }
260
+ return null;
261
+ }
262
+
263
+ /**
264
+ * The valid directive regex (must stay in sync with transformFunctionLevelUseCache).
265
+ */
266
+ const VALID_DIRECTIVE_RE = /^use cache(:\s*[\w-]+)?$/;
267
+
268
+ /**
269
+ * Regex for near-miss: starts with "use cache:" but has invalid tokens.
270
+ */
271
+ const NEAR_MISS_RE = /^use cache:\s*.+$/;
272
+
273
+ /**
274
+ * Walk the AST looking for string literals that look like malformed
275
+ * "use cache" directives and emit a Vite warning for each.
276
+ *
277
+ * This catches cases like `"use cache: bad.name"` or `"use cache: "`
278
+ * that the transform regex silently ignores.
279
+ */
280
+ function warnOnNearMissDirectives(
281
+ ast: any,
282
+ fileId: string,
283
+ warn: (message: string) => void,
284
+ ): void {
285
+ const visit = (node: any) => {
286
+ if (!node || typeof node !== "object") return;
287
+
288
+ if (
289
+ node.type === "ExpressionStatement" &&
290
+ node.expression?.type === "Literal" &&
291
+ typeof node.expression.value === "string"
292
+ ) {
293
+ const value = node.expression.value;
294
+ if (
295
+ value.startsWith("use cache") &&
296
+ NEAR_MISS_RE.test(value) &&
297
+ !VALID_DIRECTIVE_RE.test(value)
298
+ ) {
299
+ const profilePart = value.slice("use cache:".length).trim();
300
+ warn(
301
+ `[rango:use-cache] "${value}" in ${fileId} has an invalid profile name "${profilePart}". ` +
302
+ `Profile names must match [a-zA-Z0-9_-]+. This directive will be ignored.`,
303
+ );
304
+ }
305
+ }
306
+
307
+ // Walk into function bodies where directives appear
308
+ for (const key of Object.keys(node)) {
309
+ const child = node[key];
310
+ if (Array.isArray(child)) {
311
+ for (const item of child) {
312
+ visit(item);
313
+ }
314
+ } else if (child && typeof child === "object" && child.type) {
315
+ visit(child);
316
+ }
317
+ }
318
+ };
319
+
320
+ for (const node of ast.body ?? []) {
321
+ visit(node);
322
+ }
323
+ }
@@ -0,0 +1,83 @@
1
+ import type { Plugin } from "vite";
2
+ import { resolve } from "node:path";
3
+ import * as Vite from "vite";
4
+
5
+ /**
6
+ * Plugin that auto-injects VERSION and routes-manifest into custom entry.rsc files.
7
+ * If a custom entry.rsc file uses createRSCHandler but doesn't pass version,
8
+ * this transform adds the import and property automatically.
9
+ * Also ensures the routes-manifest virtual module is always imported.
10
+ * @internal
11
+ */
12
+ export function createVersionInjectorPlugin(
13
+ rscEntryPath: string | undefined,
14
+ ): Plugin {
15
+ let resolvedEntryPath = "";
16
+
17
+ return {
18
+ name: "@rangojs/router:version-injector",
19
+ enforce: "pre",
20
+
21
+ configResolved(config) {
22
+ let entryPath = rscEntryPath;
23
+ // Cloudflare preset: read entry from resolved environment config.
24
+ // The @cloudflare/vite-plugin reads wrangler config (toml/json/jsonc)
25
+ // and sets optimizeDeps.entries on the RSC environment.
26
+ if (!entryPath) {
27
+ const rscEnvConfig = (config.environments as any)?.["rsc"];
28
+ const entries = rscEnvConfig?.optimizeDeps?.entries;
29
+ if (typeof entries === "string") {
30
+ entryPath = entries;
31
+ } else if (Array.isArray(entries) && entries.length > 0) {
32
+ entryPath = entries[0];
33
+ }
34
+ }
35
+ if (entryPath) {
36
+ resolvedEntryPath = resolve(config.root, entryPath);
37
+ }
38
+ },
39
+
40
+ transform(code, id) {
41
+ if (!resolvedEntryPath) return null;
42
+ // Only transform the RSC entry file
43
+ const normalizedId = Vite.normalizePath(id);
44
+ const normalizedEntry = Vite.normalizePath(resolvedEntryPath);
45
+
46
+ if (normalizedId !== normalizedEntry) {
47
+ return null;
48
+ }
49
+
50
+ // Prepend imports at the top of the file. ES imports are hoisted
51
+ // by the module system, so source position is irrelevant.
52
+ const prepend: string[] = [];
53
+ let newCode = code;
54
+
55
+ if (!code.includes("virtual:rsc-router/routes-manifest")) {
56
+ prepend.push(`import "virtual:rsc-router/routes-manifest";`);
57
+ }
58
+
59
+ // Auto-inject VERSION if file uses createRSCHandler without version
60
+ const needsVersion =
61
+ code.includes("createRSCHandler") &&
62
+ !code.includes("@rangojs/router:version") &&
63
+ /createRSCHandler\s*\(\s*\{/.test(code);
64
+
65
+ if (needsVersion) {
66
+ prepend.push(`import { VERSION } from "@rangojs/router:version";`);
67
+ newCode = newCode.replace(
68
+ /createRSCHandler\s*\(\s*\{/,
69
+ "createRSCHandler({\n version: VERSION,",
70
+ );
71
+ }
72
+
73
+ if (prepend.length === 0 && newCode === code) return null;
74
+
75
+ newCode = prepend.join("\n") + (prepend.length > 0 ? "\n" : "") + newCode;
76
+
77
+ return {
78
+ code: newCode,
79
+ map: null,
80
+ };
81
+ },
82
+ };
83
+ }