@rangojs/router 0.5.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/dist/bin/rango.js +343 -125
  2. package/dist/types/browser/react/use-router.d.ts +10 -3
  3. package/dist/types/browser/react/use-search-params.d.ts +57 -10
  4. package/dist/types/browser/types.d.ts +22 -0
  5. package/dist/types/build/merge-full-manifests.d.ts +3 -0
  6. package/dist/types/build/route-trie.d.ts +4 -73
  7. package/dist/types/build/route-types/per-module-writer.d.ts +6 -4
  8. package/dist/types/build/route-types/router-processing.d.ts +2 -3
  9. package/dist/types/cache/cache-exec-scope.d.ts +31 -0
  10. package/dist/types/cache/taint.d.ts +12 -6
  11. package/dist/types/client-urls/client-root.d.ts +38 -0
  12. package/dist/types/client-urls/client-urls.d.ts +5 -0
  13. package/dist/types/client-urls/navigation.d.ts +38 -0
  14. package/dist/types/client-urls/revalidation-protocol.d.ts +25 -0
  15. package/dist/types/client-urls/server-projection.d.ts +71 -0
  16. package/dist/types/client-urls/types.d.ts +147 -0
  17. package/dist/types/client.d.ts +12 -4
  18. package/dist/types/client.rsc.d.ts +4 -1
  19. package/dist/types/decode-loader-results.d.ts +37 -0
  20. package/dist/types/errors.d.ts +1 -0
  21. package/dist/types/index.d.ts +1 -1
  22. package/dist/types/loader-redirect.d.ts +27 -0
  23. package/dist/types/outlet-context.d.ts +12 -0
  24. package/dist/types/outlet-provider.d.ts +3 -1
  25. package/dist/types/redirect-origin.d.ts +4 -0
  26. package/dist/types/route-content-wrapper.d.ts +42 -1
  27. package/dist/types/route-definition/helpers-types.d.ts +13 -2
  28. package/dist/types/router/error-handling.d.ts +35 -1
  29. package/dist/types/router/intercept-resolution.d.ts +12 -0
  30. package/dist/types/router/loader-resolution.d.ts +24 -2
  31. package/dist/types/router/revalidation.d.ts +7 -0
  32. package/dist/types/router/route-trie-builder.d.ts +77 -0
  33. package/dist/types/router/router-interfaces.d.ts +20 -0
  34. package/dist/types/router/segment-resolution/helpers.d.ts +1 -1
  35. package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -9
  36. package/dist/types/router/trie-matching.d.ts +1 -1
  37. package/dist/types/rsc/manifest-init.d.ts +5 -5
  38. package/dist/types/rsc/shell-capture.d.ts +9 -0
  39. package/dist/types/rsc/shell-serve.d.ts +11 -0
  40. package/dist/types/rsc/types.d.ts +30 -0
  41. package/dist/types/segment-system.d.ts +2 -0
  42. package/dist/types/server/context.d.ts +10 -0
  43. package/dist/types/server/handle-store.d.ts +34 -3
  44. package/dist/types/server/request-context.d.ts +11 -1
  45. package/dist/types/server.d.ts +1 -0
  46. package/dist/types/ssr/index.d.ts +22 -0
  47. package/dist/types/ssr/ssr-root.d.ts +10 -0
  48. package/dist/types/testing/dom.entry.d.ts +1 -1
  49. package/dist/types/testing/render-route.d.ts +16 -6
  50. package/dist/types/testing/run-loader.d.ts +9 -0
  51. package/dist/types/types/boundaries.d.ts +22 -0
  52. package/dist/types/types/index.d.ts +1 -1
  53. package/dist/types/types/loader-types.d.ts +57 -5
  54. package/dist/types/types/segments.d.ts +7 -0
  55. package/dist/types/urls/path-helper-types.d.ts +13 -4
  56. package/dist/types/vite/discovery/client-urls-projection.d.ts +53 -0
  57. package/dist/types/vite/discovery/discover-routers.d.ts +1 -1
  58. package/dist/types/vite/discovery/state.d.ts +8 -1
  59. package/dist/types/vite/plugins/client-ref-dedup.d.ts +0 -11
  60. package/dist/vite/index.js +6159 -2959
  61. package/package.json +6 -5
  62. package/skills/breadcrumbs/SKILL.md +39 -9
  63. package/skills/catalog.json +7 -1
  64. package/skills/client-urls/SKILL.md +338 -0
  65. package/skills/comparison/references/framework-comparison.md +23 -9
  66. package/skills/hooks/SKILL.md +2 -2
  67. package/skills/hooks/data.md +11 -2
  68. package/skills/hooks/handle-and-actions.md +7 -0
  69. package/skills/hooks/outlets.md +26 -5
  70. package/skills/hooks/urls.md +40 -3
  71. package/skills/loader/SKILL.md +132 -20
  72. package/skills/migrate-nextjs/SKILL.md +70 -10
  73. package/skills/migrate-react-router/SKILL.md +49 -13
  74. package/skills/migrate-react-router/component-migration.md +18 -13
  75. package/skills/migrate-react-router/data-and-actions.md +14 -3
  76. package/skills/migrate-react-router/route-mapping.md +15 -2
  77. package/skills/parallel/SKILL.md +32 -1
  78. package/skills/ppr/SKILL.md +16 -6
  79. package/skills/prerender/SKILL.md +8 -4
  80. package/skills/rango/SKILL.md +21 -17
  81. package/skills/react-compiler/SKILL.md +3 -3
  82. package/skills/route/SKILL.md +5 -2
  83. package/skills/router-setup/SKILL.md +16 -2
  84. package/skills/scripts/SKILL.md +16 -6
  85. package/skills/shell-manifest/SKILL.md +16 -7
  86. package/skills/testing/SKILL.md +2 -2
  87. package/skills/testing/client-components.md +6 -0
  88. package/skills/testing/handles.md +30 -8
  89. package/skills/testing/loader.md +51 -49
  90. package/skills/testing/middleware.md +1 -1
  91. package/skills/theme/SKILL.md +8 -5
  92. package/src/bin/rango.ts +7 -3
  93. package/src/browser/navigation-bridge.ts +6 -0
  94. package/src/browser/navigation-client.ts +5 -0
  95. package/src/browser/partial-update.ts +65 -13
  96. package/src/browser/react/use-router.ts +40 -11
  97. package/src/browser/react/use-search-params.ts +140 -17
  98. package/src/browser/rsc-router.tsx +59 -0
  99. package/src/browser/server-action-bridge.ts +26 -0
  100. package/src/browser/types.ts +22 -0
  101. package/src/build/merge-full-manifests.ts +161 -0
  102. package/src/build/route-trie.ts +9 -332
  103. package/src/build/route-types/include-resolution.ts +66 -11
  104. package/src/build/route-types/per-module-writer.ts +11 -6
  105. package/src/build/route-types/router-processing.ts +184 -153
  106. package/src/build/runtime-discovery.ts +23 -12
  107. package/src/cache/cache-exec-scope.ts +47 -0
  108. package/src/cache/cache-runtime.ts +24 -25
  109. package/src/cache/taint.ts +28 -9
  110. package/src/client-urls/client-root.tsx +168 -0
  111. package/src/client-urls/client-urls.ts +776 -0
  112. package/src/client-urls/navigation.ts +237 -0
  113. package/src/client-urls/revalidation-protocol.ts +56 -0
  114. package/src/client-urls/server-projection.ts +670 -0
  115. package/src/client-urls/types.ts +201 -0
  116. package/src/client.rsc.tsx +12 -0
  117. package/src/client.tsx +49 -6
  118. package/src/decode-loader-results.ts +113 -0
  119. package/src/errors.ts +14 -0
  120. package/src/handles/deferred-resolution.ts +14 -7
  121. package/src/index.ts +1 -0
  122. package/src/loader-redirect.tsx +64 -0
  123. package/src/outlet-context.ts +12 -0
  124. package/src/outlet-provider.tsx +15 -1
  125. package/src/redirect-origin.ts +29 -0
  126. package/src/route-content-wrapper.tsx +96 -3
  127. package/src/route-definition/dsl-helpers.ts +28 -3
  128. package/src/route-definition/helpers-types.ts +13 -0
  129. package/src/route-definition/redirect.ts +17 -18
  130. package/src/router/error-handling.ts +65 -11
  131. package/src/router/intercept-resolution.ts +29 -0
  132. package/src/router/loader-resolution.ts +261 -28
  133. package/src/router/match-result.ts +7 -0
  134. package/src/router/revalidation.ts +24 -11
  135. package/src/router/route-trie-builder.ts +334 -0
  136. package/src/router/router-interfaces.ts +38 -0
  137. package/src/router/segment-resolution/fresh.ts +55 -2
  138. package/src/router/segment-resolution/helpers.ts +9 -11
  139. package/src/router/segment-resolution/loader-cache.ts +34 -23
  140. package/src/router/segment-resolution/loader-mask.ts +9 -9
  141. package/src/router/segment-resolution/revalidation.ts +23 -2
  142. package/src/router/trie-matching.ts +3 -3
  143. package/src/router.ts +46 -1
  144. package/src/rsc/full-payload.ts +6 -0
  145. package/src/rsc/handler.ts +10 -7
  146. package/src/rsc/loader-fetch.ts +2 -2
  147. package/src/rsc/manifest-init.ts +28 -9
  148. package/src/rsc/rsc-rendering.ts +15 -1
  149. package/src/rsc/shell-capture.ts +12 -0
  150. package/src/rsc/shell-serve.ts +15 -2
  151. package/src/rsc/ssr-setup.ts +10 -1
  152. package/src/rsc/types.ts +31 -2
  153. package/src/segment-system.tsx +83 -26
  154. package/src/server/context.ts +10 -0
  155. package/src/server/cookie-store.ts +19 -19
  156. package/src/server/handle-store.ts +185 -48
  157. package/src/server/request-context.ts +30 -6
  158. package/src/server.ts +7 -0
  159. package/src/ssr/index.tsx +37 -2
  160. package/src/ssr/ssr-root.tsx +29 -2
  161. package/src/testing/dom.entry.ts +1 -1
  162. package/src/testing/render-route.tsx +22 -8
  163. package/src/testing/run-loader.ts +51 -13
  164. package/src/types/boundaries.ts +19 -0
  165. package/src/types/index.ts +1 -0
  166. package/src/types/loader-types.ts +60 -5
  167. package/src/types/segments.ts +7 -0
  168. package/src/urls/include-helper.ts +22 -4
  169. package/src/urls/path-helper-types.ts +17 -1
  170. package/src/use-loader.tsx +67 -6
  171. package/src/vite/discovery/client-urls-projection.ts +322 -0
  172. package/src/vite/discovery/discover-routers.ts +43 -17
  173. package/src/vite/discovery/state.ts +11 -1
  174. package/src/vite/discovery/virtual-module-codegen.ts +20 -0
  175. package/src/vite/plugins/client-ref-dedup.ts +281 -19
  176. package/src/vite/plugins/expose-action-id.ts +45 -23
  177. package/src/vite/plugins/virtual-entries.ts +12 -3
  178. package/src/vite/router-discovery.ts +163 -12
@@ -44,6 +44,7 @@ export function generateRoutesManifestModule(state: DiscoveryState): string {
44
44
  const hasManifest =
45
45
  state.mergedRouteManifest &&
46
46
  Object.keys(state.mergedRouteManifest).length > 0;
47
+ const hasClientUrlModules = Boolean(state.clientUrlSourceByReferenceId?.size);
47
48
 
48
49
  if (hasManifest) {
49
50
  // Build gen file import statements for each router with a sourceFile.
@@ -83,6 +84,9 @@ export function generateRoutesManifestModule(state: DiscoveryState): string {
83
84
  "setRouterManifest",
84
85
  ...(state.isBuildMode ? ["registerRouterManifestLoader"] : []),
85
86
  "clearAllRouterData",
87
+ ...(hasClientUrlModules
88
+ ? ["clearClientUrlProjections", "setClientUrlProjection"]
89
+ : []),
86
90
  ];
87
91
  const lines = [
88
92
  `import { ${serverImports.join(", ")} } from "@rangojs/router/server";`,
@@ -95,6 +99,16 @@ export function generateRoutesManifestModule(state: DiscoveryState): string {
95
99
  `clearAllRouterData();`,
96
100
  ];
97
101
 
102
+ if (hasClientUrlModules) {
103
+ lines.push(`clearClientUrlProjections();`);
104
+ for (const [referenceId, projection] of state.clientUrlProjectionMap ??
105
+ []) {
106
+ lines.push(
107
+ `setClientUrlProjection(${JSON.stringify(referenceId)}, ${jsonParseExpression(projection)});`,
108
+ );
109
+ }
110
+ }
111
+
98
112
  if (varIdx > 0) {
99
113
  lines.push(
100
114
  `function __flat(r) { const o = {}; for (const [k, v] of Object.entries(r)) o[k] = typeof v === "string" ? v : v.path; return o; }`,
@@ -169,6 +183,12 @@ export function generateRoutesManifestModule(state: DiscoveryState): string {
169
183
  }
170
184
 
171
185
  const lines: string[] = [];
186
+ if (hasClientUrlModules) {
187
+ lines.push(
188
+ `import { clearClientUrlProjections } from "@rangojs/router/server";`,
189
+ `clearClientUrlProjections();`,
190
+ );
191
+ }
172
192
  if (!state.isBuildMode) {
173
193
  const origin =
174
194
  state.devServerOrigin ||
@@ -1,3 +1,5 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { isAbsolute, join, normalize, relative } from "node:path";
1
3
  import type { Plugin, ResolvedConfig } from "vite";
2
4
  import { createRangoDebugger, NS } from "../debug.js";
3
5
 
@@ -5,23 +7,21 @@ const debug = createRangoDebugger(NS.transform);
5
7
 
6
8
  const CLIENT_IN_SERVER_PROXY_PREFIX =
7
9
  "virtual:vite-rsc/client-in-server-package-proxy/";
10
+ const DEDUP_PREFIX = "\0rango:dedup/";
11
+
12
+ interface PackageMetadata {
13
+ root: string;
14
+ selfName: string;
15
+ exports: unknown;
16
+ }
17
+
18
+ type PackageResolver = ReturnType<ResolvedConfig["createResolver"]>;
8
19
 
9
20
  /**
10
21
  * Extract the bare package name from an absolute node_modules path.
11
22
  * Handles scoped packages (@org/name) and nested node_modules.
12
23
  * Returns null if the path doesn't contain a valid package reference.
13
24
  *
14
- * NOTE: This is a lossy transformation. It maps a specific submodule path
15
- * (e.g., pkg/internal/context.js) to the package root (pkg). The load()
16
- * hook then re-exports via the bare specifier, which resolves to the
17
- * package entry point. This works for packages that barrel-export their
18
- * "use client" symbols from the root, which covers the common case
19
- * (component libraries like @mantine/core, @chakra-ui/react, etc.).
20
- * Packages whose client symbols are only available from deep subpaths
21
- * (not re-exported from the root) would lose those symbols after the
22
- * rewrite. A more precise approach would resolve through the package's
23
- * exports map to find the correct entry point, but that adds significant
24
- * complexity for a rare edge case.
25
25
  * See: https://github.com/cloudflare/vinext/pull/413
26
26
  */
27
27
  export function extractPackageName(absolutePath: string): string | null {
@@ -44,6 +44,219 @@ export function extractPackageName(absolutePath: string): string | null {
44
44
  return name || null;
45
45
  }
46
46
 
47
+ function stripQueryAndHash(id: string): string {
48
+ const suffixIndex = id.search(/[?#]/);
49
+ return suffixIndex === -1 ? id : id.slice(0, suffixIndex);
50
+ }
51
+
52
+ function getPackageRoot(source: string): string | undefined {
53
+ const packageName = extractPackageName(source);
54
+ if (!packageName) return;
55
+
56
+ const marker = "/node_modules/";
57
+ const markerIndex = source.lastIndexOf(marker);
58
+ return source.slice(0, markerIndex + marker.length) + packageName;
59
+ }
60
+
61
+ function readPackageMetadata(
62
+ source: string,
63
+ cache: Map<string, PackageMetadata | null>,
64
+ ): PackageMetadata | undefined {
65
+ const root = getPackageRoot(source);
66
+ if (!root) return;
67
+
68
+ const cached = cache.get(root);
69
+ if (cached !== undefined) return cached ?? undefined;
70
+
71
+ const packageJsonPath = join(root, "package.json");
72
+ if (!existsSync(packageJsonPath)) {
73
+ cache.set(root, null);
74
+ return;
75
+ }
76
+
77
+ try {
78
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")) as {
79
+ name?: unknown;
80
+ exports?: unknown;
81
+ };
82
+ if (typeof packageJson.name !== "string" || !packageJson.name) {
83
+ cache.set(root, null);
84
+ return;
85
+ }
86
+ const metadata = {
87
+ root,
88
+ selfName: packageJson.name,
89
+ exports: packageJson.exports,
90
+ } satisfies PackageMetadata;
91
+ cache.set(root, metadata);
92
+ return metadata;
93
+ } catch {
94
+ cache.set(root, null);
95
+ return;
96
+ }
97
+ }
98
+
99
+ function collectStringTargets(value: unknown, targets: string[]): void {
100
+ if (typeof value === "string") {
101
+ targets.push(value);
102
+ return;
103
+ }
104
+ if (Array.isArray(value)) {
105
+ for (const item of value) collectStringTargets(item, targets);
106
+ return;
107
+ }
108
+ if (typeof value !== "object" || value === null) return;
109
+ for (const target of Object.values(value)) {
110
+ collectStringTargets(target, targets);
111
+ }
112
+ }
113
+
114
+ function singleStarParts(value: string): [string, string] | undefined {
115
+ const starIndex = value.indexOf("*");
116
+ if (starIndex === -1 || value.indexOf("*", starIndex + 1) !== -1) return;
117
+ return [value.slice(0, starIndex), value.slice(starIndex + 1)];
118
+ }
119
+
120
+ function targetCanMapToSource(target: string, sourceRelative: string): boolean {
121
+ const targetStar = singleStarParts(target);
122
+ if (!targetStar) return target === sourceRelative;
123
+ const [prefix, suffix] = targetStar;
124
+ return (
125
+ sourceRelative.startsWith(prefix) &&
126
+ sourceRelative.endsWith(suffix) &&
127
+ sourceRelative.length >= prefix.length + suffix.length
128
+ );
129
+ }
130
+
131
+ function getPublicSpecifierCandidates(
132
+ metadata: PackageMetadata,
133
+ packageSpecifier: string,
134
+ source: string,
135
+ ): string[] {
136
+ if (
137
+ typeof metadata.exports !== "object" ||
138
+ metadata.exports === null ||
139
+ Array.isArray(metadata.exports)
140
+ ) {
141
+ return [];
142
+ }
143
+
144
+ const candidates = new Set<string>();
145
+ const sourceRelative = `./${relative(metadata.root, source).replaceAll("\\", "/")}`;
146
+ if (
147
+ sourceRelative.startsWith("./../") ||
148
+ isAbsolute(sourceRelative.slice(2))
149
+ ) {
150
+ return [];
151
+ }
152
+
153
+ for (const [exportKey, target] of Object.entries(metadata.exports)) {
154
+ if (!exportKey.startsWith("./") || exportKey === ".") continue;
155
+
156
+ const keyStar = singleStarParts(exportKey);
157
+ if (!keyStar) {
158
+ const targets: string[] = [];
159
+ collectStringTargets(target, targets);
160
+ if (
161
+ targets.some((target) => targetCanMapToSource(target, sourceRelative))
162
+ ) {
163
+ candidates.add(`${packageSpecifier}${exportKey.slice(1)}`);
164
+ }
165
+ continue;
166
+ }
167
+
168
+ const captures = new Set<string>();
169
+ const targets: string[] = [];
170
+ collectStringTargets(target, targets);
171
+ for (const targetPattern of targets) {
172
+ const targetStar = singleStarParts(targetPattern);
173
+ if (!targetStar) continue;
174
+ const [prefix, suffix] = targetStar;
175
+ if (
176
+ !sourceRelative.startsWith(prefix) ||
177
+ !sourceRelative.endsWith(suffix) ||
178
+ sourceRelative.length < prefix.length + suffix.length
179
+ ) {
180
+ continue;
181
+ }
182
+ captures.add(
183
+ sourceRelative.slice(
184
+ prefix.length,
185
+ sourceRelative.length - suffix.length,
186
+ ),
187
+ );
188
+ }
189
+
190
+ if (captures.size === 1) {
191
+ const capture = captures.values().next().value;
192
+ if (capture !== undefined) {
193
+ const publicSubpath = `${keyStar[0]}${capture}${keyStar[1]}`;
194
+ candidates.add(`${packageSpecifier}${publicSubpath.slice(1)}`);
195
+ }
196
+ }
197
+ }
198
+
199
+ return [...candidates];
200
+ }
201
+
202
+ function belongsToPackage(resolvedId: string, packageRoot: string): boolean {
203
+ return normalize(getPackageRoot(resolvedId) ?? "") === normalize(packageRoot);
204
+ }
205
+
206
+ async function resolvePublicSpecifier(
207
+ source: string,
208
+ metadata: PackageMetadata,
209
+ installedSpecifier: string,
210
+ resolvePackage: PackageResolver,
211
+ importer: string,
212
+ ): Promise<string | undefined> {
213
+ const packageSpecifiers =
214
+ installedSpecifier === metadata.selfName
215
+ ? [installedSpecifier]
216
+ : [installedSpecifier, metadata.selfName];
217
+
218
+ for (const packageSpecifier of packageSpecifiers) {
219
+ for (const candidate of getPublicSpecifierCandidates(
220
+ metadata,
221
+ packageSpecifier,
222
+ source,
223
+ )) {
224
+ try {
225
+ const resolved = await resolvePackage(candidate, importer);
226
+ if (!resolved) continue;
227
+ const resolvedId = stripQueryAndHash(resolved);
228
+ if (
229
+ normalize(resolvedId) === normalize(source) &&
230
+ belongsToPackage(resolvedId, metadata.root)
231
+ ) {
232
+ return candidate;
233
+ }
234
+ } catch {
235
+ continue;
236
+ }
237
+ }
238
+
239
+ // Fallback: the pre-exports-map behavior, and it is LOSSY. When no public
240
+ // subpath maps back to this exact file, re-export from the bare package
241
+ // root — correct only for packages that barrel-export their "use client"
242
+ // symbols from the entry point (the common component-library shape).
243
+ // A deep module whose symbols are NOT re-exported from the root loses
244
+ // them silently after this rewrite; the exports-map resolution above
245
+ // exists precisely to make that case rare.
246
+ try {
247
+ const rootResolved = await resolvePackage(packageSpecifier, importer);
248
+ if (
249
+ rootResolved &&
250
+ belongsToPackage(stripQueryAndHash(rootResolved), metadata.root)
251
+ ) {
252
+ return packageSpecifier;
253
+ }
254
+ } catch {
255
+ continue;
256
+ }
257
+ }
258
+ }
259
+
47
260
  /**
48
261
  * Vite plugin that deduplicates client references from third-party packages
49
262
  * in dev mode.
@@ -65,7 +278,11 @@ export function extractPackageName(absolutePath: string): string | null {
65
278
  */
66
279
  export function clientRefDedup(): Plugin {
67
280
  let clientExclude: string[] = [];
281
+ let rootImporter = "";
282
+ let resolvePackage: PackageResolver | undefined;
68
283
  const dedupedPackages = new Set<string>();
284
+ const packageMetadataCache = new Map<string, PackageMetadata | null>();
285
+ const publicSpecifierCache = new Map<string, Promise<string | undefined>>();
69
286
 
70
287
  return {
71
288
  name: "@rangojs/router:client-ref-dedup",
@@ -76,6 +293,8 @@ export function clientRefDedup(): Plugin {
76
293
  const clientEnv = config.environments?.["client"];
77
294
  clientExclude =
78
295
  clientEnv?.optimizeDeps?.exclude ?? config.optimizeDeps?.exclude ?? [];
296
+ rootImporter = join(config.root, "index.html");
297
+ resolvePackage = config.createResolver({ scan: true });
79
298
  },
80
299
 
81
300
  buildEnd() {
@@ -95,24 +314,67 @@ export function clientRefDedup(): Plugin {
95
314
 
96
315
  if (!source.includes("/node_modules/")) return;
97
316
 
98
- const packageName = extractPackageName(source);
99
- if (!packageName) return;
317
+ const cleanSource = stripQueryAndHash(source);
318
+ const packageName = extractPackageName(cleanSource);
319
+ if (!packageName || !resolvePackage) return;
100
320
 
101
321
  if (clientExclude.includes(packageName)) return;
102
322
 
103
- if (debug) dedupedPackages.add(packageName);
323
+ const metadata = readPackageMetadata(cleanSource, packageMetadataCache);
324
+ if (!metadata || clientExclude.includes(metadata.selfName)) return;
325
+
326
+ let specifierPromise = publicSpecifierCache.get(cleanSource);
327
+ if (!specifierPromise) {
328
+ specifierPromise = resolvePublicSpecifier(
329
+ cleanSource,
330
+ metadata,
331
+ packageName,
332
+ resolvePackage,
333
+ rootImporter,
334
+ );
335
+ publicSpecifierCache.set(cleanSource, specifierPromise);
336
+ // Only SUCCESSFUL resolutions stay cached. A failed one is evicted so
337
+ // the next resolveId retries — deliberate: dev-time failures can be
338
+ // transient (a dependency installed mid-session, an exports map fixed
339
+ // on disk). The cost is a repeated candidate scan per unresolvable
340
+ // module, bounded by resolveId call frequency in dev.
341
+ void specifierPromise.then(
342
+ (specifier) => {
343
+ if (
344
+ !specifier &&
345
+ publicSpecifierCache.get(cleanSource) === specifierPromise
346
+ ) {
347
+ publicSpecifierCache.delete(cleanSource);
348
+ }
349
+ },
350
+ () => {
351
+ if (publicSpecifierCache.get(cleanSource) === specifierPromise) {
352
+ publicSpecifierCache.delete(cleanSource);
353
+ }
354
+ },
355
+ );
356
+ }
104
357
 
105
- return `\0rango:dedup/${packageName}`;
358
+ return specifierPromise.then((specifier) => {
359
+ if (!specifier) return;
360
+ if (debug) dedupedPackages.add(packageName);
361
+ return `${DEDUP_PREFIX}${encodeURIComponent(specifier)}`;
362
+ });
106
363
  },
107
364
 
108
365
  load(id) {
109
- if (!id.startsWith("\0rango:dedup/")) return;
366
+ if (!id.startsWith(DEDUP_PREFIX)) return;
110
367
 
111
- const packageName = id.slice("\0rango:dedup/".length);
368
+ let specifier: string;
369
+ try {
370
+ specifier = decodeURIComponent(id.slice(DEDUP_PREFIX.length));
371
+ } catch {
372
+ return;
373
+ }
112
374
 
113
375
  return [
114
- `export * from ${JSON.stringify(packageName)};`,
115
- `import * as __all__ from ${JSON.stringify(packageName)};`,
376
+ `export * from ${JSON.stringify(specifier)};`,
377
+ `import * as __all__ from ${JSON.stringify(specifier)};`,
116
378
  `export default __all__.default;`,
117
379
  ].join("\n");
118
380
  },
@@ -1,4 +1,5 @@
1
1
  import type { Plugin, ResolvedConfig } from "vite";
2
+ import { getPluginApi } from "@vitejs/plugin-rsc";
2
3
  import MagicString from "magic-string";
3
4
  import path from "node:path";
4
5
  import fs from "node:fs";
@@ -8,15 +9,15 @@ import { createRangoDebugger, createCounter, NS } from "../debug.js";
8
9
 
9
10
  const debug = createRangoDebugger(NS.transform);
10
11
 
12
+ interface ServerReferenceMeta {
13
+ importId: string;
14
+ referenceKey: string;
15
+ exportNames: string[];
16
+ }
17
+
11
18
  interface RscPluginManager {
12
- serverReferenceMetaMap: Record<
13
- string,
14
- {
15
- importId: string;
16
- referenceKey: string;
17
- exportNames: string[];
18
- }
19
- >;
19
+ serverReferenceMetaMap?: Record<string, ServerReferenceMeta>;
20
+ serverReferences?: { metaMap: Map<string, ServerReferenceMeta> };
20
21
  config: ResolvedConfig;
21
22
  }
22
23
 
@@ -24,21 +25,44 @@ interface RscPluginApi {
24
25
  manager: RscPluginManager;
25
26
  }
26
27
 
28
+ function getServerReferenceMetaEntries(
29
+ manager: RscPluginManager | undefined,
30
+ ): Iterable<[string, ServerReferenceMeta]> {
31
+ if (manager?.serverReferences?.metaMap) {
32
+ return manager.serverReferences.metaMap;
33
+ }
34
+
35
+ if (manager?.serverReferenceMetaMap) {
36
+ return Object.entries(manager.serverReferenceMetaMap);
37
+ }
38
+
39
+ throw new Error(
40
+ "[rango] Unsupported @vitejs/plugin-rsc server reference metadata shape. " +
41
+ "Expected manager.serverReferences.metaMap or manager.serverReferenceMetaMap.",
42
+ );
43
+ }
44
+
27
45
  function getRscPluginApi(config: ResolvedConfig): RscPluginApi | undefined {
28
- let plugin = config.plugins.find((p) => p.name === "rsc:minimal");
46
+ const pluginApi = getPluginApi(config) as RscPluginApi | undefined;
47
+ if (pluginApi) {
48
+ return pluginApi;
49
+ }
29
50
 
30
- if (!plugin) {
31
- plugin = config.plugins.find(
32
- (p) =>
33
- (p.api as RscPluginApi | undefined)?.manager?.serverReferenceMetaMap !==
34
- undefined,
35
- );
36
- if (plugin) {
37
- console.warn(
38
- `[rango:expose-action-id] RSC plugin found by API structure (name: "${plugin.name}"). ` +
39
- `Consider updating the name lookup if the plugin was renamed.`,
51
+ const plugin = config.plugins.find((p) => {
52
+ try {
53
+ getServerReferenceMetaEntries(
54
+ (p.api as RscPluginApi | undefined)?.manager,
40
55
  );
56
+ return true;
57
+ } catch {
58
+ return false;
41
59
  }
60
+ });
61
+ if (plugin) {
62
+ console.warn(
63
+ `[rango:expose-action-id] RSC plugin found by API structure (name: "${plugin.name}"). ` +
64
+ `Consider updating the name lookup if the plugin was renamed.`,
65
+ );
42
66
  }
43
67
 
44
68
  return plugin?.api as RscPluginApi | undefined;
@@ -233,10 +257,8 @@ export function exposeActionId(): Plugin {
233
257
  if (!isBuild) return;
234
258
 
235
259
  hashToFileMap = new Map();
236
- const { serverReferenceMetaMap } = rscPluginApi.manager;
237
-
238
- for (const [absolutePath, meta] of Object.entries(
239
- serverReferenceMetaMap,
260
+ for (const [absolutePath, meta] of getServerReferenceMetaEntries(
261
+ rscPluginApi.manager,
240
262
  )) {
241
263
  // Only include module-level "use server" files
242
264
  // Inline actions (defined in RSC components) should keep hashed IDs for client security
@@ -154,14 +154,23 @@ import {
154
154
  decodeAction,
155
155
  decodeFormState,
156
156
  } from "@rangojs/router/internal/deps/rsc";
157
- import { router } from "${routerPath}";
158
- import { createRSCHandler } from "@rangojs/router/internal/rsc-handler";
159
- import { VERSION } from "@rangojs/router:version";
160
157
 
161
158
  // Startup bootstrap imports (routes + loader manifests). See
162
159
  // RSC_ENTRY_BOOTSTRAP_IMPORTS — the same list the custom-entry injector uses.
160
+ //
161
+ // ORDER IS LOAD-BEARING: the bootstrap imports MUST precede the router import.
162
+ // The routes-manifest module installs client URL projections
163
+ // (setClientUrlProjection), and createRouter().routes(clientUrlsReference)
164
+ // consults them at router-module evaluation. Router-first would silently push
165
+ // every clientUrls() app onto the deferred-subscription path and change mount
166
+ // registration order with no error. The custom-entry injector keeps the same
167
+ // guarantee by PREPENDING this list at file top (version-injector.ts).
163
168
  ${bootstrapImports}
164
169
 
170
+ import { router } from "${routerPath}";
171
+ import { createRSCHandler } from "@rangojs/router/internal/rsc-handler";
172
+ import { VERSION } from "@rangojs/router:version";
173
+
165
174
  // Lazily create the handler on first request so that ESM live bindings
166
175
  // have resolved by the time we read \`router\`. During HMR the module may
167
176
  // re-evaluate before router.tsx finishes, leaving the import undefined.