@rangojs/router 0.0.0-experimental.43 → 0.0.0-experimental.44
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.
package/dist/bin/rango.js
CHANGED
|
@@ -1063,8 +1063,9 @@ function createVersionPlugin() {
|
|
|
1063
1063
|
let isDev = false;
|
|
1064
1064
|
let server = null;
|
|
1065
1065
|
const clientModuleSignatures = /* @__PURE__ */ new Map();
|
|
1066
|
+
let versionCounter = 0;
|
|
1066
1067
|
const bumpVersion = (reason) => {
|
|
1067
|
-
currentVersion = Date.now().toString(16);
|
|
1068
|
+
currentVersion = Date.now().toString(16) + String(++versionCounter);
|
|
1068
1069
|
console.log(`[rsc-router] ${reason}, version updated: ${currentVersion}`);
|
|
1069
1070
|
const rscEnv = server?.environments?.rsc;
|
|
1070
1071
|
const versionMod = rscEnv?.moduleGraph?.getModuleById(
|
|
@@ -1120,6 +1121,9 @@ function createVersionPlugin() {
|
|
|
1120
1121
|
if (!isDev) return;
|
|
1121
1122
|
const isRscModule = this.environment?.name === "rsc";
|
|
1122
1123
|
if (!isRscModule) return;
|
|
1124
|
+
if (ctx.modules.length === 1 && ctx.modules[0].id === "\0" + VIRTUAL_IDS.version) {
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1123
1127
|
if (isCodeModule(ctx.file)) {
|
|
1124
1128
|
const filePath = normalizeModuleId(ctx.file);
|
|
1125
1129
|
const previousSignature = clientModuleSignatures.get(filePath);
|
package/dist/vite/index.js
CHANGED
|
@@ -1745,7 +1745,7 @@ import { resolve } from "node:path";
|
|
|
1745
1745
|
// package.json
|
|
1746
1746
|
var package_default = {
|
|
1747
1747
|
name: "@rangojs/router",
|
|
1748
|
-
version: "0.0.0-experimental.
|
|
1748
|
+
version: "0.0.0-experimental.44",
|
|
1749
1749
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
1750
1750
|
keywords: [
|
|
1751
1751
|
"react",
|
|
@@ -2694,8 +2694,9 @@ function createVersionPlugin() {
|
|
|
2694
2694
|
let isDev = false;
|
|
2695
2695
|
let server = null;
|
|
2696
2696
|
const clientModuleSignatures = /* @__PURE__ */ new Map();
|
|
2697
|
+
let versionCounter = 0;
|
|
2697
2698
|
const bumpVersion = (reason) => {
|
|
2698
|
-
currentVersion = Date.now().toString(16);
|
|
2699
|
+
currentVersion = Date.now().toString(16) + String(++versionCounter);
|
|
2699
2700
|
console.log(`[rsc-router] ${reason}, version updated: ${currentVersion}`);
|
|
2700
2701
|
const rscEnv = server?.environments?.rsc;
|
|
2701
2702
|
const versionMod = rscEnv?.moduleGraph?.getModuleById(
|
|
@@ -2751,6 +2752,9 @@ function createVersionPlugin() {
|
|
|
2751
2752
|
if (!isDev) return;
|
|
2752
2753
|
const isRscModule = this.environment?.name === "rsc";
|
|
2753
2754
|
if (!isRscModule) return;
|
|
2755
|
+
if (ctx.modules.length === 1 && ctx.modules[0].id === "\0" + VIRTUAL_IDS.version) {
|
|
2756
|
+
return;
|
|
2757
|
+
}
|
|
2754
2758
|
if (isCodeModule(ctx.file)) {
|
|
2755
2759
|
const filePath = normalizeModuleId(ctx.file);
|
|
2756
2760
|
const previousSignature = clientModuleSignatures.get(filePath);
|
package/package.json
CHANGED
|
@@ -79,6 +79,8 @@ export interface DerivedNavigationState {
|
|
|
79
79
|
state: "idle" | "loading";
|
|
80
80
|
/** Whether any operation is streaming */
|
|
81
81
|
isStreaming: boolean;
|
|
82
|
+
/** Whether a navigation is active (fetching or streaming, before commit) */
|
|
83
|
+
isNavigating: boolean;
|
|
82
84
|
/** Current committed location */
|
|
83
85
|
location: NavigationLocation;
|
|
84
86
|
/** URL being navigated to (null if idle) */
|
|
@@ -389,6 +391,9 @@ export function createEventController(
|
|
|
389
391
|
return {
|
|
390
392
|
state,
|
|
391
393
|
isStreaming,
|
|
394
|
+
// True when a navigation is active (fetching or streaming, before
|
|
395
|
+
// commit). Broader than pendingUrl which clears during streaming.
|
|
396
|
+
isNavigating: currentNavigation !== null,
|
|
392
397
|
location,
|
|
393
398
|
// pendingUrl only during fetching phase - once streaming starts (URL changed), not pending.
|
|
394
399
|
// Background revalidations (skipLoadingState) don't expose a pending URL.
|
|
@@ -263,71 +263,123 @@ export async function initBrowserApp(
|
|
|
263
263
|
// Build initial tree with rootLayout
|
|
264
264
|
const initialTree = renderSegments(initialPayload.metadata!.segments);
|
|
265
265
|
|
|
266
|
-
// Setup HMR
|
|
266
|
+
// Setup HMR with debounce — burst saves (format-on-save, rapid edits)
|
|
267
|
+
// fire many rsc:update events in quick succession. Without debouncing,
|
|
268
|
+
// each event triggers a fetchPartial() which on slow routes can pile up
|
|
269
|
+
// and overwhelm the worker (cross-request promise issues, 500s).
|
|
267
270
|
if (import.meta.hot) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const handle = eventController.startNavigation(window.location.href, {
|
|
272
|
-
replace: true,
|
|
273
|
-
});
|
|
274
|
-
const streamingToken = handle.startStreaming();
|
|
275
|
-
|
|
276
|
-
const interceptSourceUrl = store.getInterceptSourceUrl();
|
|
277
|
-
|
|
278
|
-
try {
|
|
279
|
-
const { payload, streamComplete } = await client.fetchPartial({
|
|
280
|
-
targetUrl: window.location.href,
|
|
281
|
-
segmentIds: [],
|
|
282
|
-
previousUrl: store.getSegmentState().currentUrl,
|
|
283
|
-
interceptSourceUrl: interceptSourceUrl || undefined,
|
|
284
|
-
hmr: true,
|
|
285
|
-
});
|
|
271
|
+
let hmrTimer: ReturnType<typeof setTimeout> | null = null;
|
|
272
|
+
let hmrAbort: AbortController | null = null;
|
|
286
273
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
274
|
+
import.meta.hot.on("rsc:update", () => {
|
|
275
|
+
// Cancel any pending debounce timer
|
|
276
|
+
if (hmrTimer !== null) {
|
|
277
|
+
clearTimeout(hmrTimer);
|
|
278
|
+
}
|
|
290
279
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
280
|
+
// Abort any in-flight HMR fetch so it doesn't race with the next one
|
|
281
|
+
if (hmrAbort) {
|
|
282
|
+
hmrAbort.abort();
|
|
283
|
+
hmrAbort = null;
|
|
284
|
+
}
|
|
295
285
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
286
|
+
// Debounce: wait 200ms of quiet before fetching
|
|
287
|
+
hmrTimer = setTimeout(async () => {
|
|
288
|
+
hmrTimer = null;
|
|
289
|
+
|
|
290
|
+
// Don't interrupt an active user navigation — startNavigation()
|
|
291
|
+
// would abort it and refetch the old URL (window.location.href
|
|
292
|
+
// hasn't updated yet). The user's navigation will pick up the
|
|
293
|
+
// new server code when it completes. isNavigating covers the
|
|
294
|
+
// full lifecycle (fetching + streaming, before commit) without
|
|
295
|
+
// blocking on server actions.
|
|
296
|
+
if (eventController.getState().isNavigating) {
|
|
297
|
+
console.log("[RSCRouter] HMR: Skipping — navigation in progress");
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
300
|
|
|
301
|
-
|
|
302
|
-
store.setCurrentUrl(window.location.href);
|
|
301
|
+
console.log("[RSCRouter] HMR: Server update, refetching RSC");
|
|
303
302
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
303
|
+
const abort = new AbortController();
|
|
304
|
+
hmrAbort = abort;
|
|
305
|
+
|
|
306
|
+
const handle = eventController.startNavigation(window.location.href, {
|
|
307
|
+
replace: true,
|
|
308
|
+
});
|
|
309
|
+
const streamingToken = handle.startStreaming();
|
|
310
|
+
|
|
311
|
+
const interceptSourceUrl = store.getInterceptSourceUrl();
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
const { payload, streamComplete } = await client.fetchPartial({
|
|
315
|
+
targetUrl: window.location.href,
|
|
316
|
+
segmentIds: [],
|
|
317
|
+
previousUrl: store.getSegmentState().currentUrl,
|
|
318
|
+
interceptSourceUrl: interceptSourceUrl || undefined,
|
|
319
|
+
hmr: true,
|
|
320
|
+
signal: abort.signal,
|
|
321
321
|
});
|
|
322
|
-
}
|
|
323
322
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
323
|
+
if (abort.signal.aborted) return;
|
|
324
|
+
|
|
325
|
+
// If the server returned a non-RSC response (404, 500 without
|
|
326
|
+
// error boundary), the payload won't have valid metadata.
|
|
327
|
+
// Reload to recover rather than leaving the page stale.
|
|
328
|
+
if (!payload.metadata) {
|
|
329
|
+
throw new Error("HMR refetch returned invalid payload");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (payload.metadata?.isPartial) {
|
|
333
|
+
const segments = payload.metadata.segments || [];
|
|
334
|
+
const matched = payload.metadata.matched || [];
|
|
335
|
+
|
|
336
|
+
// Derive intercept state from the returned payload, not the
|
|
337
|
+
// pre-fetch store snapshot. If the HMR edit removed intercept
|
|
338
|
+
// behavior, the response won't contain intercept segments.
|
|
339
|
+
const responseIsIntercept = segments.some(isInterceptSegment);
|
|
340
|
+
|
|
341
|
+
// Sync store intercept state with what the server returned
|
|
342
|
+
if (!responseIsIntercept && interceptSourceUrl) {
|
|
343
|
+
store.setInterceptSourceUrl(null);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
store.setSegmentIds(matched);
|
|
347
|
+
store.setCurrentUrl(window.location.href);
|
|
348
|
+
|
|
349
|
+
const historyKey = generateHistoryKey(window.location.href, {
|
|
350
|
+
intercept: responseIsIntercept,
|
|
351
|
+
});
|
|
352
|
+
store.setHistoryKey(historyKey);
|
|
353
|
+
const currentHandleData = eventController.getHandleState().data;
|
|
354
|
+
store.cacheSegmentsForHistory(
|
|
355
|
+
historyKey,
|
|
356
|
+
segments,
|
|
357
|
+
currentHandleData,
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
const { main, intercept } = splitInterceptSegments(segments);
|
|
361
|
+
store.emitUpdate({
|
|
362
|
+
root: renderSegments(main, {
|
|
363
|
+
interceptSegments: intercept.length > 0 ? intercept : undefined,
|
|
364
|
+
}),
|
|
365
|
+
metadata: payload.metadata,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
await streamComplete;
|
|
370
|
+
handle.complete(new URL(window.location.href));
|
|
371
|
+
console.log("[RSCRouter] HMR: RSC stream complete");
|
|
372
|
+
} catch (err) {
|
|
373
|
+
if (abort.signal.aborted) return;
|
|
374
|
+
console.warn("[RSCRouter] HMR: Refetch failed, reloading page", err);
|
|
375
|
+
window.location.reload();
|
|
376
|
+
return;
|
|
377
|
+
} finally {
|
|
378
|
+
if (hmrAbort === abort) hmrAbort = null;
|
|
379
|
+
streamingToken.end();
|
|
380
|
+
handle[Symbol.dispose]();
|
|
381
|
+
}
|
|
382
|
+
}, 200);
|
|
331
383
|
});
|
|
332
384
|
}
|
|
333
385
|
|
package/src/ssr/index.tsx
CHANGED
|
@@ -135,8 +135,11 @@ export function createVersionPlugin(): Plugin {
|
|
|
135
135
|
let server: any = null;
|
|
136
136
|
const clientModuleSignatures = new Map<string, ClientModuleSignature>();
|
|
137
137
|
|
|
138
|
+
let versionCounter = 0;
|
|
138
139
|
const bumpVersion = (reason: string) => {
|
|
139
|
-
|
|
140
|
+
// Use timestamp + counter to guarantee uniqueness even when multiple
|
|
141
|
+
// bumps happen within the same millisecond (e.g. cascading HMR events).
|
|
142
|
+
currentVersion = Date.now().toString(16) + String(++versionCounter);
|
|
140
143
|
console.log(`[rsc-router] ${reason}, version updated: ${currentVersion}`);
|
|
141
144
|
|
|
142
145
|
const rscEnv = server?.environments?.rsc;
|
|
@@ -211,6 +214,15 @@ export function createVersionPlugin(): Plugin {
|
|
|
211
214
|
|
|
212
215
|
if (!isRscModule) return;
|
|
213
216
|
|
|
217
|
+
// Skip re-bumping when the version virtual module itself is invalidated
|
|
218
|
+
// (our own bumpVersion() invalidates it, which re-triggers hotUpdate).
|
|
219
|
+
if (
|
|
220
|
+
ctx.modules.length === 1 &&
|
|
221
|
+
ctx.modules[0].id === "\0" + VIRTUAL_IDS.version
|
|
222
|
+
) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
214
226
|
if (isCodeModule(ctx.file)) {
|
|
215
227
|
const filePath = normalizeModuleId(ctx.file);
|
|
216
228
|
const previousSignature = clientModuleSignatures.get(filePath);
|