@rangojs/router 0.0.0-experimental.3 → 0.0.0-experimental.30

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 (297) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +883 -4
  3. package/dist/bin/rango.js +1601 -0
  4. package/dist/vite/index.js +4655 -747
  5. package/package.json +78 -50
  6. package/skills/cache-guide/SKILL.md +262 -0
  7. package/skills/caching/SKILL.md +54 -25
  8. package/skills/composability/SKILL.md +172 -0
  9. package/skills/debug-manifest/SKILL.md +12 -8
  10. package/skills/document-cache/SKILL.md +23 -21
  11. package/skills/fonts/SKILL.md +167 -0
  12. package/skills/hooks/SKILL.md +390 -63
  13. package/skills/host-router/SKILL.md +218 -0
  14. package/skills/intercept/SKILL.md +133 -10
  15. package/skills/layout/SKILL.md +102 -5
  16. package/skills/links/SKILL.md +239 -0
  17. package/skills/loader/SKILL.md +366 -29
  18. package/skills/middleware/SKILL.md +173 -36
  19. package/skills/mime-routes/SKILL.md +128 -0
  20. package/skills/parallel/SKILL.md +80 -3
  21. package/skills/prerender/SKILL.md +643 -0
  22. package/skills/rango/SKILL.md +86 -16
  23. package/skills/response-routes/SKILL.md +411 -0
  24. package/skills/route/SKILL.md +227 -14
  25. package/skills/router-setup/SKILL.md +225 -32
  26. package/skills/tailwind/SKILL.md +129 -0
  27. package/skills/theme/SKILL.md +12 -11
  28. package/skills/typesafety/SKILL.md +401 -75
  29. package/skills/use-cache/SKILL.md +324 -0
  30. package/src/__internal.ts +10 -4
  31. package/src/bin/rango.ts +321 -0
  32. package/src/browser/action-coordinator.ts +97 -0
  33. package/src/browser/action-response-classifier.ts +99 -0
  34. package/src/browser/event-controller.ts +87 -64
  35. package/src/browser/history-state.ts +80 -0
  36. package/src/browser/intercept-utils.ts +52 -0
  37. package/src/browser/link-interceptor.ts +20 -4
  38. package/src/browser/logging.ts +55 -0
  39. package/src/browser/merge-segment-loaders.ts +20 -12
  40. package/src/browser/navigation-bridge.ts +201 -553
  41. package/src/browser/navigation-client.ts +124 -71
  42. package/src/browser/navigation-store.ts +33 -50
  43. package/src/browser/navigation-transaction.ts +295 -0
  44. package/src/browser/network-error-handler.ts +61 -0
  45. package/src/browser/partial-update.ts +267 -317
  46. package/src/browser/prefetch/cache.ts +146 -0
  47. package/src/browser/prefetch/fetch.ts +135 -0
  48. package/src/browser/prefetch/observer.ts +65 -0
  49. package/src/browser/prefetch/policy.ts +42 -0
  50. package/src/browser/prefetch/queue.ts +88 -0
  51. package/src/browser/rango-state.ts +112 -0
  52. package/src/browser/react/Link.tsx +173 -73
  53. package/src/browser/react/NavigationProvider.tsx +138 -27
  54. package/src/browser/react/context.ts +6 -0
  55. package/src/browser/react/filter-segment-order.ts +11 -0
  56. package/src/browser/react/index.ts +12 -12
  57. package/src/browser/react/location-state-shared.ts +95 -53
  58. package/src/browser/react/location-state.ts +60 -15
  59. package/src/browser/react/mount-context.ts +37 -0
  60. package/src/browser/react/nonce-context.ts +23 -0
  61. package/src/browser/react/shallow-equal.ts +27 -0
  62. package/src/browser/react/use-action.ts +29 -51
  63. package/src/browser/react/use-client-cache.ts +5 -3
  64. package/src/browser/react/use-handle.ts +49 -65
  65. package/src/browser/react/use-href.tsx +20 -188
  66. package/src/browser/react/use-link-status.ts +6 -5
  67. package/src/browser/react/use-mount.ts +31 -0
  68. package/src/browser/react/use-navigation.ts +27 -78
  69. package/src/browser/react/use-params.ts +65 -0
  70. package/src/browser/react/use-pathname.ts +47 -0
  71. package/src/browser/react/use-router.ts +63 -0
  72. package/src/browser/react/use-search-params.ts +56 -0
  73. package/src/browser/react/use-segments.ts +80 -97
  74. package/src/browser/response-adapter.ts +73 -0
  75. package/src/browser/rsc-router.tsx +111 -26
  76. package/src/browser/scroll-restoration.ts +92 -16
  77. package/src/browser/segment-reconciler.ts +216 -0
  78. package/src/browser/segment-structure-assert.ts +83 -0
  79. package/src/browser/server-action-bridge.ts +504 -584
  80. package/src/browser/shallow.ts +6 -1
  81. package/src/browser/types.ts +92 -57
  82. package/src/browser/validate-redirect-origin.ts +29 -0
  83. package/src/build/generate-manifest.ts +438 -0
  84. package/src/build/generate-route-types.ts +36 -0
  85. package/src/build/index.ts +35 -0
  86. package/src/build/route-trie.ts +265 -0
  87. package/src/build/route-types/ast-helpers.ts +25 -0
  88. package/src/build/route-types/ast-route-extraction.ts +98 -0
  89. package/src/build/route-types/codegen.ts +102 -0
  90. package/src/build/route-types/include-resolution.ts +411 -0
  91. package/src/build/route-types/param-extraction.ts +48 -0
  92. package/src/build/route-types/per-module-writer.ts +128 -0
  93. package/src/build/route-types/router-processing.ts +469 -0
  94. package/src/build/route-types/scan-filter.ts +78 -0
  95. package/src/build/runtime-discovery.ts +231 -0
  96. package/src/cache/background-task.ts +34 -0
  97. package/src/cache/cache-key-utils.ts +44 -0
  98. package/src/cache/cache-policy.ts +125 -0
  99. package/src/cache/cache-runtime.ts +338 -0
  100. package/src/cache/cache-scope.ts +120 -303
  101. package/src/cache/cf/cf-cache-store.ts +119 -7
  102. package/src/cache/cf/index.ts +8 -2
  103. package/src/cache/document-cache.ts +101 -72
  104. package/src/cache/handle-capture.ts +81 -0
  105. package/src/cache/handle-snapshot.ts +41 -0
  106. package/src/cache/index.ts +0 -15
  107. package/src/cache/memory-segment-store.ts +191 -13
  108. package/src/cache/profile-registry.ts +73 -0
  109. package/src/cache/read-through-swr.ts +134 -0
  110. package/src/cache/segment-codec.ts +256 -0
  111. package/src/cache/taint.ts +98 -0
  112. package/src/cache/types.ts +72 -122
  113. package/src/client.rsc.tsx +10 -15
  114. package/src/client.tsx +114 -135
  115. package/src/component-utils.ts +4 -4
  116. package/src/components/DefaultDocument.tsx +5 -1
  117. package/src/context-var.ts +86 -0
  118. package/src/debug.ts +17 -7
  119. package/src/errors.ts +108 -2
  120. package/src/handle.ts +34 -19
  121. package/src/handles/MetaTags.tsx +73 -20
  122. package/src/handles/meta.ts +30 -13
  123. package/src/host/cookie-handler.ts +165 -0
  124. package/src/host/errors.ts +97 -0
  125. package/src/host/index.ts +53 -0
  126. package/src/host/pattern-matcher.ts +214 -0
  127. package/src/host/router.ts +352 -0
  128. package/src/host/testing.ts +79 -0
  129. package/src/host/types.ts +146 -0
  130. package/src/host/utils.ts +25 -0
  131. package/src/href-client.ts +135 -49
  132. package/src/index.rsc.ts +182 -17
  133. package/src/index.ts +238 -24
  134. package/src/internal-debug.ts +11 -0
  135. package/src/loader.rsc.ts +27 -142
  136. package/src/loader.ts +27 -10
  137. package/src/network-error-thrower.tsx +3 -1
  138. package/src/outlet-provider.tsx +45 -0
  139. package/src/prerender/param-hash.ts +37 -0
  140. package/src/prerender/store.ts +185 -0
  141. package/src/prerender.ts +463 -0
  142. package/src/reverse.ts +330 -0
  143. package/src/root-error-boundary.tsx +41 -29
  144. package/src/route-content-wrapper.tsx +9 -11
  145. package/src/route-definition/dsl-helpers.ts +934 -0
  146. package/src/route-definition/helper-factories.ts +200 -0
  147. package/src/route-definition/helpers-types.ts +430 -0
  148. package/src/route-definition/index.ts +52 -0
  149. package/src/route-definition/redirect.ts +93 -0
  150. package/src/route-definition.ts +1 -1388
  151. package/src/route-map-builder.ts +241 -112
  152. package/src/route-name.ts +53 -0
  153. package/src/route-types.ts +70 -9
  154. package/src/router/content-negotiation.ts +116 -0
  155. package/src/router/debug-manifest.ts +72 -0
  156. package/src/router/error-handling.ts +9 -9
  157. package/src/router/find-match.ts +158 -0
  158. package/src/router/handler-context.ts +371 -81
  159. package/src/router/intercept-resolution.ts +395 -0
  160. package/src/router/lazy-includes.ts +234 -0
  161. package/src/router/loader-resolution.ts +215 -122
  162. package/src/router/logging.ts +248 -0
  163. package/src/router/manifest.ts +155 -32
  164. package/src/router/match-api.ts +620 -0
  165. package/src/router/match-context.ts +5 -3
  166. package/src/router/match-handlers.ts +440 -0
  167. package/src/router/match-middleware/background-revalidation.ts +80 -93
  168. package/src/router/match-middleware/cache-lookup.ts +382 -9
  169. package/src/router/match-middleware/cache-store.ts +51 -22
  170. package/src/router/match-middleware/intercept-resolution.ts +55 -17
  171. package/src/router/match-middleware/segment-resolution.ts +24 -6
  172. package/src/router/match-pipelines.ts +10 -45
  173. package/src/router/match-result.ts +34 -29
  174. package/src/router/metrics.ts +235 -15
  175. package/src/router/middleware-cookies.ts +55 -0
  176. package/src/router/middleware-types.ts +222 -0
  177. package/src/router/middleware.ts +324 -367
  178. package/src/router/pattern-matching.ts +321 -30
  179. package/src/router/prerender-match.ts +400 -0
  180. package/src/router/preview-match.ts +170 -0
  181. package/src/router/revalidation.ts +137 -38
  182. package/src/router/router-context.ts +36 -21
  183. package/src/router/router-interfaces.ts +452 -0
  184. package/src/router/router-options.ts +592 -0
  185. package/src/router/router-registry.ts +24 -0
  186. package/src/router/segment-resolution/fresh.ts +570 -0
  187. package/src/router/segment-resolution/helpers.ts +263 -0
  188. package/src/router/segment-resolution/loader-cache.ts +198 -0
  189. package/src/router/segment-resolution/revalidation.ts +1241 -0
  190. package/src/router/segment-resolution/static-store.ts +67 -0
  191. package/src/router/segment-resolution.ts +21 -0
  192. package/src/router/segment-wrappers.ts +289 -0
  193. package/src/router/telemetry-otel.ts +299 -0
  194. package/src/router/telemetry.ts +300 -0
  195. package/src/router/timeout.ts +148 -0
  196. package/src/router/trie-matching.ts +239 -0
  197. package/src/router/types.ts +77 -3
  198. package/src/router.ts +688 -3656
  199. package/src/rsc/handler-context.ts +45 -0
  200. package/src/rsc/handler.ts +786 -760
  201. package/src/rsc/helpers.ts +140 -6
  202. package/src/rsc/index.ts +5 -25
  203. package/src/rsc/loader-fetch.ts +209 -0
  204. package/src/rsc/manifest-init.ts +86 -0
  205. package/src/rsc/nonce.ts +14 -0
  206. package/src/rsc/origin-guard.ts +141 -0
  207. package/src/rsc/progressive-enhancement.ts +379 -0
  208. package/src/rsc/response-error.ts +37 -0
  209. package/src/rsc/response-route-handler.ts +347 -0
  210. package/src/rsc/rsc-rendering.ts +235 -0
  211. package/src/rsc/runtime-warnings.ts +42 -0
  212. package/src/rsc/server-action.ts +348 -0
  213. package/src/rsc/ssr-setup.ts +128 -0
  214. package/src/rsc/types.ts +40 -14
  215. package/src/search-params.ts +230 -0
  216. package/src/segment-system.tsx +57 -61
  217. package/src/server/context.ts +202 -51
  218. package/src/server/cookie-store.ts +190 -0
  219. package/src/server/fetchable-loader-store.ts +37 -0
  220. package/src/server/handle-store.ts +94 -15
  221. package/src/server/loader-registry.ts +15 -56
  222. package/src/server/request-context.ts +422 -70
  223. package/src/server.ts +36 -120
  224. package/src/ssr/index.tsx +157 -26
  225. package/src/static-handler.ts +114 -0
  226. package/src/theme/ThemeProvider.tsx +21 -15
  227. package/src/theme/ThemeScript.tsx +5 -5
  228. package/src/theme/constants.ts +5 -2
  229. package/src/theme/index.ts +4 -14
  230. package/src/theme/theme-context.ts +4 -30
  231. package/src/theme/theme-script.ts +21 -18
  232. package/src/types/boundaries.ts +158 -0
  233. package/src/types/cache-types.ts +198 -0
  234. package/src/types/error-types.ts +192 -0
  235. package/src/types/global-namespace.ts +100 -0
  236. package/src/types/handler-context.ts +687 -0
  237. package/src/types/index.ts +88 -0
  238. package/src/types/loader-types.ts +183 -0
  239. package/src/types/route-config.ts +170 -0
  240. package/src/types/route-entry.ts +102 -0
  241. package/src/types/segments.ts +148 -0
  242. package/src/types.ts +1 -1577
  243. package/src/urls/include-helper.ts +197 -0
  244. package/src/urls/index.ts +53 -0
  245. package/src/urls/path-helper-types.ts +339 -0
  246. package/src/urls/path-helper.ts +329 -0
  247. package/src/urls/pattern-types.ts +95 -0
  248. package/src/urls/response-types.ts +106 -0
  249. package/src/urls/type-extraction.ts +372 -0
  250. package/src/urls/urls-function.ts +98 -0
  251. package/src/urls.ts +1 -726
  252. package/src/use-loader.tsx +85 -77
  253. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  254. package/src/vite/discovery/discover-routers.ts +344 -0
  255. package/src/vite/discovery/prerender-collection.ts +385 -0
  256. package/src/vite/discovery/route-types-writer.ts +258 -0
  257. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  258. package/src/vite/discovery/state.ts +110 -0
  259. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  260. package/src/vite/index.ts +11 -782
  261. package/src/vite/plugin-types.ts +131 -0
  262. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  263. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  264. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  265. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -51
  266. package/src/vite/plugins/expose-id-utils.ts +287 -0
  267. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  268. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  269. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  270. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  271. package/src/vite/plugins/expose-ids/types.ts +45 -0
  272. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  273. package/src/vite/plugins/refresh-cmd.ts +65 -0
  274. package/src/vite/plugins/use-cache-transform.ts +323 -0
  275. package/src/vite/plugins/version-injector.ts +83 -0
  276. package/src/vite/plugins/version-plugin.ts +254 -0
  277. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +29 -15
  278. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  279. package/src/vite/rango.ts +510 -0
  280. package/src/vite/router-discovery.ts +785 -0
  281. package/src/vite/utils/ast-handler-extract.ts +517 -0
  282. package/src/vite/utils/banner.ts +36 -0
  283. package/src/vite/utils/bundle-analysis.ts +137 -0
  284. package/src/vite/utils/manifest-utils.ts +70 -0
  285. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  286. package/src/vite/utils/prerender-utils.ts +189 -0
  287. package/src/vite/utils/shared-utils.ts +169 -0
  288. package/CLAUDE.md +0 -3
  289. package/src/browser/lru-cache.ts +0 -69
  290. package/src/browser/request-controller.ts +0 -164
  291. package/src/cache/memory-store.ts +0 -253
  292. package/src/href-context.ts +0 -33
  293. package/src/href.ts +0 -255
  294. package/src/vite/expose-handle-id.ts +0 -209
  295. package/src/vite/expose-loader-id.ts +0 -357
  296. package/src/vite/expose-location-state-id.ts +0 -177
  297. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -1,209 +0,0 @@
1
- import type { Plugin, ResolvedConfig } from "vite";
2
- import MagicString from "magic-string";
3
- import path from "node:path";
4
- import crypto from "node:crypto";
5
-
6
- /**
7
- * Normalize path to forward slashes
8
- */
9
- function normalizePath(p: string): string {
10
- return p.split(path.sep).join("/");
11
- }
12
-
13
- /**
14
- * Generate a short hash for a handle ID
15
- * Uses first 8 chars of SHA-256 hash for uniqueness while keeping IDs short
16
- * Appends export name for easier debugging: "abc123#Breadcrumbs"
17
- */
18
- function hashHandleId(filePath: string, exportName: string): string {
19
- const input = `${filePath}#${exportName}`;
20
- const hash = crypto.createHash("sha256").update(input).digest("hex");
21
- return `${hash.slice(0, 8)}#${exportName}`;
22
- }
23
-
24
- /**
25
- * Check if file imports createHandle from rsc-router
26
- */
27
- function hasCreateHandleImport(code: string): boolean {
28
- // Match: import { createHandle } from "@rangojs/router" or "@rangojs/router/..."
29
- const pattern =
30
- /import\s*\{[^}]*\bcreateHandle\b[^}]*\}\s*from\s*["']@rangojs\/router(?:\/[^"']+)?["']/;
31
- return pattern.test(code);
32
- }
33
-
34
- /**
35
- * Analyze createHandle arguments to determine injection strategy
36
- * Returns: { hasArgs: boolean, firstArgIsString: boolean, firstArgIsFunction: boolean }
37
- */
38
- function analyzeCreateHandleArgs(
39
- code: string,
40
- startPos: number,
41
- endPos: number
42
- ): { hasArgs: boolean; firstArgIsString: boolean; firstArgIsFunction: boolean } {
43
- // Extract the content between parentheses
44
- const content = code.slice(startPos, endPos).trim();
45
-
46
- if (!content) {
47
- return { hasArgs: false, firstArgIsString: false, firstArgIsFunction: false };
48
- }
49
-
50
- // Check if first arg starts with a quote (string literal)
51
- const firstArgIsString = /^["']/.test(content);
52
-
53
- // Check if first arg starts with ( for arrow function or function keyword
54
- const firstArgIsFunction =
55
- content.startsWith("(") ||
56
- content.startsWith("function") ||
57
- // Check for identifier that could be a collect function reference
58
- /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*(?:,|$)/.test(content);
59
-
60
- return { hasArgs: true, firstArgIsString, firstArgIsFunction };
61
- }
62
-
63
- /**
64
- * Transform export const X = createHandle(...) patterns to inject $$id
65
- *
66
- * Handles these cases:
67
- * 1. createHandle() - no args -> inject (undefined, "id")
68
- * 2. createHandle("name") - string name -> inject (, "id") after existing arg
69
- * 3. createHandle(collectFn) - collect function -> inject (collectFn, "id")
70
- * 4. createHandle("name", collectFn) - both -> inject (, "id") after existing args
71
- */
72
- function transformHandleExports(
73
- code: string,
74
- filePath: string,
75
- sourceId?: string,
76
- isBuild: boolean = false
77
- ): { code: string; map: ReturnType<MagicString["generateMap"]> } | null {
78
- // Quick bail-out
79
- if (!code.includes("createHandle")) {
80
- return null;
81
- }
82
-
83
- // Must have direct import from rsc-router
84
- if (!hasCreateHandleImport(code)) {
85
- return null;
86
- }
87
-
88
- // Match: export const X = createHandle<...>(
89
- // Captures the export name (X)
90
- const pattern = /export\s+const\s+(\w+)\s*=\s*createHandle\s*(?:<[^>]*>)?\s*\(/g;
91
-
92
- const s = new MagicString(code);
93
- let hasChanges = false;
94
- let match: RegExpExecArray | null;
95
-
96
- while ((match = pattern.exec(code)) !== null) {
97
- const exportName = match[1];
98
- const matchEnd = match.index + match[0].length;
99
-
100
- // Find the end of the createHandle(...) call
101
- let parenDepth = 1;
102
- let i = matchEnd;
103
- while (i < code.length && parenDepth > 0) {
104
- if (code[i] === "(") parenDepth++;
105
- if (code[i] === ")") parenDepth--;
106
- i++;
107
- }
108
-
109
- // i now points just after the closing )
110
- const closeParenPos = i - 1;
111
-
112
- // Analyze what arguments exist
113
- const args = analyzeCreateHandleArgs(code, matchEnd, closeParenPos);
114
-
115
- // Find the semicolon or end of statement
116
- let statementEnd = i;
117
- while (statementEnd < code.length && /\s/.test(code[statementEnd])) {
118
- statementEnd++;
119
- }
120
- if (code[statementEnd] === ";") {
121
- statementEnd++;
122
- }
123
-
124
- // Generate ID: hashed in production, readable in dev
125
- const handleId = isBuild
126
- ? hashHandleId(filePath, exportName)
127
- : `${filePath}#${exportName}`;
128
-
129
- // Inject $$id as the last parameter
130
- let paramInjection: string;
131
- if (!args.hasArgs) {
132
- // No args: createHandle() -> createHandle(undefined, "id")
133
- paramInjection = `undefined, "${handleId}"`;
134
- } else {
135
- // Has args: createHandle(x) -> createHandle(x, "id")
136
- paramInjection = `, "${handleId}"`;
137
- }
138
- s.appendLeft(closeParenPos, paramInjection);
139
-
140
- // Also set $$id property for external access
141
- const propInjection = `\n${exportName}.$$id = "${handleId}";`;
142
- s.appendRight(statementEnd, propInjection);
143
- hasChanges = true;
144
- }
145
-
146
- if (!hasChanges) {
147
- return null;
148
- }
149
-
150
- return {
151
- code: s.toString(),
152
- map: s.generateMap({ source: sourceId, includeContent: true }),
153
- };
154
- }
155
-
156
- /**
157
- * Vite plugin that exposes $$id on createHandle calls.
158
- *
159
- * When users create handles with createHandle(), this plugin:
160
- * 1. Injects a $$id as the last parameter (used as the handle name)
161
- * 2. Sets $$id property on the exported constant for external access
162
- *
163
- * This allows handles to be created without explicit names:
164
- * - Before: export const Breadcrumbs = createHandle<Item>("breadcrumbs")
165
- * - After: export const Breadcrumbs = createHandle<Item>()
166
- *
167
- * The name is auto-generated from file path + export name.
168
- *
169
- * Requirements:
170
- * - Must use direct import: import { createHandle } from "@rangojs/router"
171
- * - Must use named export: export const MyHandle = createHandle(...)
172
- */
173
- export function exposeHandleId(): Plugin {
174
- let config: ResolvedConfig;
175
- let isBuild = false;
176
-
177
- return {
178
- name: "@rangojs/router:expose-handle-id",
179
- enforce: "post",
180
-
181
- configResolved(resolvedConfig) {
182
- config = resolvedConfig;
183
- isBuild = config.command === "build";
184
- },
185
-
186
- transform(code, id) {
187
- // Skip node_modules
188
- if (id.includes("/node_modules/")) {
189
- return;
190
- }
191
-
192
- // Quick bail-out
193
- if (!code.includes("createHandle")) {
194
- return;
195
- }
196
-
197
- // Must have direct import from rsc-router
198
- if (!hasCreateHandleImport(code)) {
199
- return;
200
- }
201
-
202
- // Get relative path for the ID
203
- const relativePath = normalizePath(path.relative(config.root, id));
204
-
205
- // Transform: inject $$id
206
- return transformHandleExports(code, relativePath, id, isBuild);
207
- },
208
- };
209
- }
@@ -1,357 +0,0 @@
1
- import type { Plugin, ResolvedConfig } from "vite";
2
- import MagicString from "magic-string";
3
- import path from "node:path";
4
- import crypto from "node:crypto";
5
-
6
- /**
7
- * Normalize path to forward slashes
8
- */
9
- function normalizePath(p: string): string {
10
- return p.split(path.sep).join("/");
11
- }
12
-
13
- /**
14
- * Generate a short hash for a loader ID
15
- * Uses first 8 chars of SHA-256 hash for uniqueness while keeping IDs short
16
- * Appends export name for easier debugging in production: "abc123#CartLoader"
17
- */
18
- function hashLoaderId(filePath: string, exportName: string): string {
19
- const input = `${filePath}#${exportName}`;
20
- const hash = crypto.createHash("sha256").update(input).digest("hex");
21
- return `${hash.slice(0, 8)}#${exportName}`;
22
- }
23
-
24
- /**
25
- * Check if file imports createLoader from rsc-router
26
- */
27
- function hasCreateLoaderImport(code: string): boolean {
28
- // Match: import { createLoader } from "@rangojs/router" or "@rangojs/router/server"
29
- // Must be exact - no aliasing support
30
- const pattern =
31
- /import\s*\{[^}]*\bcreateLoader\b[^}]*\}\s*from\s*["']@rangojs\/router(?:\/server)?["']/;
32
- return pattern.test(code);
33
- }
34
-
35
- /**
36
- * Count the number of arguments in a createLoader call
37
- * Returns the count of top-level arguments (not counting nested commas)
38
- */
39
- function countCreateLoaderArgs(code: string, startPos: number, endPos: number): number {
40
- let depth = 0;
41
- let argCount = 0;
42
- let hasContent = false;
43
-
44
- for (let i = startPos; i < endPos; i++) {
45
- const char = code[i];
46
-
47
- // Track nested structures
48
- if (char === "(" || char === "[" || char === "{") {
49
- depth++;
50
- hasContent = true;
51
- } else if (char === ")" || char === "]" || char === "}") {
52
- depth--;
53
- } else if (char === "," && depth === 0) {
54
- // Top-level comma means another argument
55
- argCount++;
56
- } else if (!/\s/.test(char)) {
57
- hasContent = true;
58
- }
59
- }
60
-
61
- // If there's content, we have at least one argument
62
- return hasContent ? argCount + 1 : 0;
63
- }
64
-
65
- /**
66
- * Find all export const X = createLoader(...) patterns and inject $$id
67
- * In production, IDs are hashed to avoid exposing file paths.
68
- * In dev, IDs use filePath#exportName for easier debugging.
69
- *
70
- * The ID is injected in two ways:
71
- * 1. As a hidden third parameter to createLoader() for registry registration
72
- * 2. As a property assignment X.$$id = "..." for external access
73
- *
74
- * IMPORTANT: The $$id must always be the THIRD parameter to createLoader.
75
- * createLoader(fn, fetchable?, __injectedId?)
76
- * If the user only provides fn, we inject: undefined, "id"
77
- * If the user provides fn and fetchable, we inject: , "id"
78
- */
79
- function transformLoaderExports(
80
- code: string,
81
- filePath: string,
82
- sourceId?: string,
83
- isBuild: boolean = false
84
- ): { code: string; map: ReturnType<MagicString["generateMap"]> } | null {
85
- // Quick bail-out
86
- if (!code.includes("createLoader")) {
87
- return null;
88
- }
89
-
90
- // Must have direct import from rsc-router
91
- if (!hasCreateLoaderImport(code)) {
92
- return null;
93
- }
94
-
95
- // Match: export const X = createLoader(
96
- // Captures the export name (X)
97
- const pattern = /export\s+const\s+(\w+)\s*=\s*createLoader\s*\(/g;
98
-
99
- const s = new MagicString(code);
100
- let hasChanges = false;
101
- let match: RegExpExecArray | null;
102
-
103
- while ((match = pattern.exec(code)) !== null) {
104
- const exportName = match[1];
105
- const matchEnd = match.index + match[0].length;
106
-
107
- // Find the end of the createLoader(...) call
108
- // Need to count parentheses to find matching close
109
- let parenDepth = 1;
110
- let i = matchEnd;
111
- while (i < code.length && parenDepth > 0) {
112
- if (code[i] === "(") parenDepth++;
113
- if (code[i] === ")") parenDepth--;
114
- i++;
115
- }
116
-
117
- // i now points just after the closing )
118
- const closeParenPos = i - 1;
119
-
120
- // Count existing arguments
121
- const argCount = countCreateLoaderArgs(code, matchEnd, closeParenPos);
122
-
123
- // Find the semicolon or end of statement
124
- let statementEnd = i;
125
- while (statementEnd < code.length && /\s/.test(code[statementEnd])) {
126
- statementEnd++;
127
- }
128
- if (code[statementEnd] === ";") {
129
- statementEnd++;
130
- }
131
-
132
- // In production: hash ID to avoid exposing file paths
133
- // In dev: use readable format for easier debugging
134
- const loaderId = isBuild
135
- ? hashLoaderId(filePath, exportName)
136
- : `${filePath}#${exportName}`;
137
-
138
- // Inject $$id as hidden third parameter before the closing paren
139
- // If user only has 1 arg (fn), we need to add undefined for fetchable
140
- // createLoader(fn) -> createLoader(fn, undefined, "id")
141
- // createLoader(fn, true) -> createLoader(fn, true, "id")
142
- const paramInjection = argCount === 1
143
- ? `, undefined, "${loaderId}"`
144
- : `, "${loaderId}"`;
145
- s.appendLeft(closeParenPos, paramInjection);
146
-
147
- // Also set $$id property for external access (useLoader, useFetchLoader)
148
- const propInjection = `\n${exportName}.$$id = "${loaderId}";`;
149
- s.appendRight(statementEnd, propInjection);
150
- hasChanges = true;
151
- }
152
-
153
- if (!hasChanges) {
154
- return null;
155
- }
156
-
157
- return {
158
- code: s.toString(),
159
- map: s.generateMap({ source: sourceId, includeContent: true }),
160
- };
161
- }
162
-
163
- const VIRTUAL_LOADER_MANIFEST = "virtual:rsc-router/loader-manifest";
164
- const RESOLVED_VIRTUAL_LOADER_MANIFEST = "\0" + VIRTUAL_LOADER_MANIFEST;
165
-
166
- // Store for deferred manifest generation - populated during transform, used after build
167
- let manifestGenerated = false;
168
-
169
- /**
170
- * Vite plugin that exposes $$id on createLoader calls and generates a loader manifest.
171
- *
172
- * When users create loaders with createLoader(), this plugin:
173
- * 1. Injects a $$id property containing the file path and export name
174
- * 2. Tracks all loaders and generates a virtual manifest module
175
- *
176
- * The manifest can be imported by the RSC handler to get all loaders.
177
- *
178
- * Requirements:
179
- * - Must use direct import: import { createLoader } from "@rangojs/router"
180
- * - No aliasing support (import { createLoader as cl } won't work)
181
- * - Must use named export: export const MyLoader = createLoader(...)
182
- */
183
- export function exposeLoaderId(): Plugin {
184
- let config: ResolvedConfig;
185
- let isBuild = false;
186
-
187
- // Track discovered loaders: hashedId -> { filePath, exportName }
188
- const loaderRegistry = new Map<
189
- string,
190
- { filePath: string; exportName: string }
191
- >();
192
-
193
- // For build mode: pre-scan for loaders during buildStart
194
- const pendingLoaderScans = new Map<string, Promise<void>>();
195
-
196
- return {
197
- name: "@rangojs/router:expose-loader-id",
198
- enforce: "post",
199
-
200
- configResolved(resolvedConfig) {
201
- config = resolvedConfig;
202
- isBuild = config.command === "build";
203
- },
204
-
205
- async buildStart() {
206
- if (!isBuild) return;
207
-
208
- // Pre-scan for loader files to populate registry before manifest is loaded
209
- // This runs before module resolution, so manifest will have access to all loaders
210
- const fs = await import("node:fs/promises");
211
-
212
- async function scanDir(dir: string): Promise<string[]> {
213
- const results: string[] = [];
214
- try {
215
- const entries = await fs.readdir(dir, { withFileTypes: true });
216
- for (const entry of entries) {
217
- const fullPath = path.join(dir, entry.name);
218
- if (entry.isDirectory()) {
219
- if (entry.name !== "node_modules") {
220
- results.push(...(await scanDir(fullPath)));
221
- }
222
- } else if (/\.(ts|tsx|js|jsx)$/.test(entry.name)) {
223
- results.push(fullPath);
224
- }
225
- }
226
- } catch {
227
- // Directory doesn't exist or not readable
228
- }
229
- return results;
230
- }
231
-
232
- try {
233
- const srcDir = path.join(config.root, "src");
234
- const files = await scanDir(srcDir);
235
-
236
- for (const filePath of files) {
237
- const content = await fs.readFile(filePath, "utf-8");
238
-
239
- // Quick check for createLoader
240
- if (!content.includes("createLoader")) continue;
241
- if (!hasCreateLoaderImport(content)) continue;
242
-
243
- // Extract loader exports
244
- const pattern = /export\s+const\s+(\w+)\s*=\s*createLoader\s*\(/g;
245
- const relativePath = normalizePath(
246
- path.relative(config.root, filePath)
247
- );
248
- let match: RegExpExecArray | null;
249
-
250
- while ((match = pattern.exec(content)) !== null) {
251
- const exportName = match[1];
252
- const hashedId = hashLoaderId(relativePath, exportName);
253
- loaderRegistry.set(hashedId, {
254
- filePath: relativePath,
255
- exportName,
256
- });
257
- }
258
- }
259
- } catch (error) {
260
- // Fall back to transform-time discovery
261
- console.warn("[exposeLoaderId] Pre-scan failed:", error);
262
- }
263
- },
264
-
265
- resolveId(id) {
266
- if (id === VIRTUAL_LOADER_MANIFEST) {
267
- return RESOLVED_VIRTUAL_LOADER_MANIFEST;
268
- }
269
- },
270
-
271
- load(id) {
272
- if (id === RESOLVED_VIRTUAL_LOADER_MANIFEST) {
273
- // Generate a lazy import map for on-demand loader loading
274
- // This avoids importing all loader modules at startup
275
-
276
- if (!isBuild) {
277
- // Dev mode: empty map - use fallback path parsing in loader registry
278
- // IDs in dev mode are "filePath#exportName" format for easier debugging
279
- return `import { setLoaderImports } from "@rangojs/router/server";
280
-
281
- // Dev mode: empty map, loaders are resolved dynamically via path parsing
282
- setLoaderImports({});
283
- `;
284
- }
285
-
286
- // Build mode: generate lazy import map
287
- // Each loader is only imported when first requested
288
- // Keys are hashed IDs to avoid exposing file paths
289
- const lazyImports: string[] = [];
290
-
291
- for (const [hashedId, { filePath, exportName }] of loaderRegistry) {
292
- // Create a lazy import function for each loader
293
- lazyImports.push(
294
- ` "${hashedId}": () => import("/${filePath}").then(m => m.${exportName})`
295
- );
296
- }
297
-
298
- // If no loaders discovered, set empty map
299
- if (lazyImports.length === 0) {
300
- return `import { setLoaderImports } from "@rangojs/router/server";
301
-
302
- // No fetchable loaders discovered during build
303
- setLoaderImports({});
304
- `;
305
- }
306
-
307
- const code = `import { setLoaderImports } from "@rangojs/router/server";
308
-
309
- // Lazy import map - loaders are loaded on-demand when first requested
310
- setLoaderImports({
311
- ${lazyImports.join(",\n")}
312
- });
313
- `;
314
- return code;
315
- }
316
- },
317
-
318
- transform(code, id) {
319
- // Skip node_modules
320
- if (id.includes("/node_modules/")) {
321
- return;
322
- }
323
-
324
- // Quick bail-out
325
- if (!code.includes("createLoader")) {
326
- return;
327
- }
328
-
329
- // Must have direct import from rsc-router
330
- if (!hasCreateLoaderImport(code)) {
331
- return;
332
- }
333
-
334
- // Check if we're in RSC environment (server-side)
335
- const envName = this.environment?.name;
336
- const isRscEnv = envName === "rsc";
337
-
338
- // Get relative path for the ID
339
- const relativePath = normalizePath(path.relative(config.root, id));
340
-
341
- // Track loaders for manifest (only in RSC env to avoid duplicate entries)
342
- if (isRscEnv) {
343
- const pattern = /export\s+const\s+(\w+)\s*=\s*createLoader\s*\(/g;
344
- let match: RegExpExecArray | null;
345
- while ((match = pattern.exec(code)) !== null) {
346
- const exportName = match[1];
347
- const hashedId = hashLoaderId(relativePath, exportName);
348
- loaderRegistry.set(hashedId, { filePath: relativePath, exportName });
349
- }
350
- }
351
-
352
- // Transform: inject $$id in all environments
353
- // In build mode, IDs are hashed; in dev mode, they're readable
354
- return transformLoaderExports(code, relativePath, id, isBuild);
355
- },
356
- };
357
- }