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

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.
@@ -1864,7 +1864,7 @@ import { resolve } from "node:path";
1864
1864
  // package.json
1865
1865
  var package_default = {
1866
1866
  name: "@rangojs/router",
1867
- version: "0.0.0-experimental.70",
1867
+ version: "0.0.0-experimental.72",
1868
1868
  description: "Django-inspired RSC router with composable URL patterns",
1869
1869
  keywords: [
1870
1870
  "react",
@@ -5289,6 +5289,14 @@ async function rango(options) {
5289
5289
  enforce: "pre",
5290
5290
  config() {
5291
5291
  return {
5292
+ // Wrangler/Miniflare mutates .wrangler state files (sqlite, WAL, etc.)
5293
+ // during normal dev operation. Those writes are not source changes,
5294
+ // but they can still wake Vite's watcher and create noisy HMR churn.
5295
+ server: {
5296
+ watch: {
5297
+ ignored: ["**/.wrangler/**"]
5298
+ }
5299
+ },
5292
5300
  // Exclude rsc-router modules from optimization to prevent module duplication
5293
5301
  // This ensures the same Context instance is used by both browser entry and RSC proxy modules
5294
5302
  optimizeDeps: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.0.0-experimental.70",
3
+ "version": "0.0.0-experimental.72",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -125,6 +125,69 @@ export async function collectSegments(
125
125
  return segments;
126
126
  }
127
127
 
128
+ /**
129
+ * Deduplicate inherited loader segments by loaderId.
130
+ *
131
+ * When a route has loaders and a child layout has parallel slots, the same
132
+ * loader is resolved twice: once for the route and once inherited into the
133
+ * layout (tagged with `_inherited`). The inherited copy is only needed when
134
+ * the route uses `loading()` — in that case, the loader data is inside a
135
+ * LoaderBoundary/Suspense that parallel slots can't reach through. Without
136
+ * loading(), useLoader() traverses parent contexts and finds the data.
137
+ */
138
+ function deduplicateLoaderSegments(
139
+ segments: ResolvedSegment[],
140
+ logPrefix: string,
141
+ ): ResolvedSegment[] {
142
+ // First pass: collect loaderIds of original (non-inherited) segments
143
+ // and whether their parent entry uses loading()
144
+ const originalLoaders = new Set<string>();
145
+ const loadersWithLoading = new Set<string>();
146
+ for (const s of segments) {
147
+ if (s.type === "loader" && s.loaderId && !s._inherited) {
148
+ originalLoaders.add(s.loaderId);
149
+ // If the segment has a sibling with loading, the parent uses loading()
150
+ // We detect this by checking if any non-loader segment in the same
151
+ // namespace has loading defined
152
+ }
153
+ }
154
+ // Check if any layout/route segment has loading — if a loader's namespace
155
+ // matches a segment with loading, the inherited copy is needed
156
+ for (const s of segments) {
157
+ if (s.type !== "loader" && s.loading !== undefined && s.loading !== false) {
158
+ // Find loaders in this namespace
159
+ for (const l of segments) {
160
+ if (l.type === "loader" && l.namespace === s.namespace && l.loaderId) {
161
+ loadersWithLoading.add(l.loaderId);
162
+ }
163
+ }
164
+ }
165
+ }
166
+
167
+ const result: ResolvedSegment[] = [];
168
+ let dedupCount = 0;
169
+
170
+ for (const s of segments) {
171
+ if (
172
+ s.type === "loader" &&
173
+ s.loaderId &&
174
+ s._inherited &&
175
+ originalLoaders.has(s.loaderId) &&
176
+ !loadersWithLoading.has(s.loaderId)
177
+ ) {
178
+ dedupCount++;
179
+ continue;
180
+ }
181
+ result.push(s);
182
+ }
183
+
184
+ if (dedupCount > 0) {
185
+ debugLog(logPrefix, `deduped ${dedupCount} inherited loader segment(s)`);
186
+ }
187
+
188
+ return result;
189
+ }
190
+
128
191
  /**
129
192
  * Build the final MatchResult from collected segments and context
130
193
  */
@@ -181,6 +244,11 @@ export function buildMatchResult<TEnv>(
181
244
  );
182
245
  }
183
246
 
247
+ const dedupedSegments = deduplicateLoaderSegments(
248
+ segmentsToRender,
249
+ logPrefix,
250
+ );
251
+
184
252
  debugLog(logPrefix, "all segments", {
185
253
  segments: allSegments.map((s) => ({
186
254
  id: s.id,
@@ -189,13 +257,23 @@ export function buildMatchResult<TEnv>(
189
257
  })),
190
258
  });
191
259
  debugLog(logPrefix, "segments to render", {
192
- segmentIds: segmentsToRender.map((s) => s.id),
260
+ segmentIds: dedupedSegments.map((s) => s.id),
193
261
  });
194
262
 
263
+ // Remove deduped loader IDs from matched so the client doesn't treat
264
+ // them as missing segments and trigger a fallback refetch.
265
+ const removedIds = new Set(
266
+ segmentsToRender
267
+ .filter((s) => !dedupedSegments.includes(s))
268
+ .map((s) => s.id),
269
+ );
270
+ const matchedIds =
271
+ removedIds.size > 0 ? allIds.filter((id) => !removedIds.has(id)) : allIds;
272
+
195
273
  return {
196
- segments: segmentsToRender,
197
- matched: allIds,
198
- diff: segmentsToRender.map((s) => s.id),
274
+ segments: dedupedSegments,
275
+ matched: matchedIds,
276
+ diff: dedupedSegments.map((s) => s.id),
199
277
  params: ctx.matched.params,
200
278
  routeName: ctx.routeKey,
201
279
  slots: Object.keys(state.slots).length > 0 ? state.slots : undefined,
@@ -419,6 +419,10 @@ export async function resolveOrphanLayout<TEnv>(
419
419
  deps,
420
420
  orphan.shortCode,
421
421
  );
422
+ // Tag as inherited so buildMatchResult can deduplicate when safe
423
+ for (const s of inheritedLoaders) {
424
+ s._inherited = true;
425
+ }
422
426
  segments.push(...inheritedLoaders);
423
427
  }
424
428
  }
@@ -728,6 +732,7 @@ export async function resolveLoadersOnly<TEnv>(
728
732
  for (const seg of inherited) {
729
733
  if (!seenIds.has(seg.id)) {
730
734
  seenIds.add(seg.id);
735
+ seg._inherited = true;
731
736
  loaderSegments.push(seg);
732
737
  }
733
738
  }
@@ -346,6 +346,7 @@ export async function resolveLoadersOnlyWithRevalidation<TEnv>(
346
346
  for (const seg of inherited.segments) {
347
347
  if (!seenIds.has(seg.id)) {
348
348
  seenIds.add(seg.id);
349
+ seg._inherited = true;
349
350
  allLoaderSegments.push(seg);
350
351
  }
351
352
  }
@@ -1036,6 +1037,10 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1036
1037
  orphan.shortCode,
1037
1038
  stale,
1038
1039
  );
1040
+ // Tag as inherited so buildMatchResult can deduplicate when safe
1041
+ for (const s of inheritedResult.segments) {
1042
+ s._inherited = true;
1043
+ }
1039
1044
  segments.push(...inheritedResult.segments);
1040
1045
  matchedIds.push(...inheritedResult.matchedIds);
1041
1046
  }
@@ -1126,6 +1131,7 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1126
1131
  );
1127
1132
 
1128
1133
  if (!resolvedParallelEntries.has(parallelEntry.id)) {
1134
+ // shortCodeOverride must match the parent layout, not the parallel entry.
1129
1135
  const loaderResult = await resolveLoadersWithRevalidation(
1130
1136
  parallelEntry,
1131
1137
  context,
@@ -1138,7 +1144,7 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1138
1144
  routeKey,
1139
1145
  deps,
1140
1146
  actionContext,
1141
- undefined,
1147
+ orphan.shortCode,
1142
1148
  stale,
1143
1149
  );
1144
1150
  segments.push(...loaderResult.segments);
@@ -50,6 +50,7 @@ export interface ResolvedSegment {
50
50
  parallelName?: string; // For parallels: the parallel group name (used to match with revalidations)
51
51
  // Loader-specific fields
52
52
  loaderId?: string; // For loaders: the loader $$id identifier
53
+ _inherited?: boolean; // For inherited loaders: dedup marker for buildMatchResult
53
54
  loaderData?: any; // For loaders: the resolved data from loader execution
54
55
  parallelLoading?: ReactNode; // For parallel-owned loaders: the parallel's loading fallback
55
56
  // Intercept loader fields (for streaming loader data in parallel segments)
package/src/vite/rango.ts CHANGED
@@ -102,6 +102,14 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
102
102
  config() {
103
103
  // Configure environments for cloudflare deployment
104
104
  return {
105
+ // Wrangler/Miniflare mutates .wrangler state files (sqlite, WAL, etc.)
106
+ // during normal dev operation. Those writes are not source changes,
107
+ // but they can still wake Vite's watcher and create noisy HMR churn.
108
+ server: {
109
+ watch: {
110
+ ignored: ["**/.wrangler/**"],
111
+ },
112
+ },
105
113
  // Exclude rsc-router modules from optimization to prevent module duplication
106
114
  // This ensures the same Context instance is used by both browser entry and RSC proxy modules
107
115
  optimizeDeps: {