@rangojs/router 0.0.0-experimental.91 → 0.0.0-experimental.93
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/vite/index.js +335 -53
- package/package.json +1 -1
- package/skills/loader/SKILL.md +31 -0
- package/skills/parallel/SKILL.md +7 -0
- package/src/route-definition/helpers-types.ts +6 -1
- package/src/types/handler-context.ts +10 -5
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/plugins/version-injector.ts +37 -11
- package/src/vite/router-discovery.ts +354 -42
package/dist/vite/index.js
CHANGED
|
@@ -2040,7 +2040,7 @@ import { resolve } from "node:path";
|
|
|
2040
2040
|
// package.json
|
|
2041
2041
|
var package_default = {
|
|
2042
2042
|
name: "@rangojs/router",
|
|
2043
|
-
version: "0.0.0-experimental.
|
|
2043
|
+
version: "0.0.0-experimental.93",
|
|
2044
2044
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
2045
2045
|
keywords: [
|
|
2046
2046
|
"react",
|
|
@@ -3376,11 +3376,10 @@ function createVersionInjectorPlugin(rscEntryPath) {
|
|
|
3376
3376
|
if (normalizedId !== normalizedEntry) {
|
|
3377
3377
|
return null;
|
|
3378
3378
|
}
|
|
3379
|
-
const prepend = [
|
|
3379
|
+
const prepend = [
|
|
3380
|
+
`import "virtual:rsc-router/routes-manifest";`
|
|
3381
|
+
];
|
|
3380
3382
|
let newCode = code;
|
|
3381
|
-
if (!code.includes("virtual:rsc-router/routes-manifest")) {
|
|
3382
|
-
prepend.push(`import "virtual:rsc-router/routes-manifest";`);
|
|
3383
|
-
}
|
|
3384
3383
|
const needsVersion = code.includes("createRSCHandler") && !code.includes("@rangojs/router:version") && /createRSCHandler\s*\(\s*\{/.test(code);
|
|
3385
3384
|
if (needsVersion) {
|
|
3386
3385
|
prepend.push(`import { VERSION } from "@rangojs/router:version";`);
|
|
@@ -3389,8 +3388,21 @@ function createVersionInjectorPlugin(rscEntryPath) {
|
|
|
3389
3388
|
"createRSCHandler({\n version: VERSION,"
|
|
3390
3389
|
);
|
|
3391
3390
|
}
|
|
3392
|
-
|
|
3393
|
-
|
|
3391
|
+
const lines = newCode.split("\n");
|
|
3392
|
+
let insertAt = 0;
|
|
3393
|
+
while (insertAt < lines.length) {
|
|
3394
|
+
const trimmed = lines[insertAt].trim();
|
|
3395
|
+
if (trimmed === "" || /^\/\/\/\s*<reference\b/.test(trimmed)) {
|
|
3396
|
+
insertAt++;
|
|
3397
|
+
} else {
|
|
3398
|
+
break;
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
newCode = [
|
|
3402
|
+
...lines.slice(0, insertAt),
|
|
3403
|
+
...prepend,
|
|
3404
|
+
...lines.slice(insertAt)
|
|
3405
|
+
].join("\n");
|
|
3394
3406
|
return {
|
|
3395
3407
|
code: newCode,
|
|
3396
3408
|
map: null
|
|
@@ -3784,6 +3796,12 @@ function markSelfGenWrite(state, filePath, content) {
|
|
|
3784
3796
|
state.selfWrittenGenFiles.set(filePath, { at: Date.now(), hash });
|
|
3785
3797
|
}
|
|
3786
3798
|
function consumeSelfGenWrite(state, filePath) {
|
|
3799
|
+
return checkSelfGenWrite(state, filePath, true);
|
|
3800
|
+
}
|
|
3801
|
+
function peekSelfGenWrite(state, filePath) {
|
|
3802
|
+
return checkSelfGenWrite(state, filePath, false);
|
|
3803
|
+
}
|
|
3804
|
+
function checkSelfGenWrite(state, filePath, consume) {
|
|
3787
3805
|
const info = state.selfWrittenGenFiles.get(filePath);
|
|
3788
3806
|
if (!info) return false;
|
|
3789
3807
|
if (Date.now() - info.at > state.SELF_WRITE_WINDOW_MS) {
|
|
@@ -3794,7 +3812,7 @@ function consumeSelfGenWrite(state, filePath) {
|
|
|
3794
3812
|
const current = readFileSync3(filePath, "utf-8");
|
|
3795
3813
|
const currentHash = createHash3("sha256").update(current).digest("hex");
|
|
3796
3814
|
if (currentHash === info.hash) {
|
|
3797
|
-
state.selfWrittenGenFiles.delete(filePath);
|
|
3815
|
+
if (consume) state.selfWrittenGenFiles.delete(filePath);
|
|
3798
3816
|
return true;
|
|
3799
3817
|
}
|
|
3800
3818
|
return false;
|
|
@@ -5067,6 +5085,80 @@ function postprocessBundle(state) {
|
|
|
5067
5085
|
}
|
|
5068
5086
|
}
|
|
5069
5087
|
|
|
5088
|
+
// src/vite/discovery/gate-state.ts
|
|
5089
|
+
function createDiscoveryGate(s, debug11) {
|
|
5090
|
+
let gatePending = false;
|
|
5091
|
+
let gateResolver = () => {
|
|
5092
|
+
};
|
|
5093
|
+
let inProgress = false;
|
|
5094
|
+
let queued = false;
|
|
5095
|
+
let pendingEvents = false;
|
|
5096
|
+
const beginGate = () => {
|
|
5097
|
+
if (gatePending) return;
|
|
5098
|
+
s.discoveryDone = new Promise((resolve10) => {
|
|
5099
|
+
gateResolver = resolve10;
|
|
5100
|
+
});
|
|
5101
|
+
gatePending = true;
|
|
5102
|
+
};
|
|
5103
|
+
const resolveGate = () => {
|
|
5104
|
+
if (!gatePending) return;
|
|
5105
|
+
if (inProgress || queued || pendingEvents) {
|
|
5106
|
+
debug11?.(
|
|
5107
|
+
"hmr: resolveGate deferred \u2014 work in flight (inProgress=%s queued=%s pendingEvents=%s)",
|
|
5108
|
+
inProgress,
|
|
5109
|
+
queued,
|
|
5110
|
+
pendingEvents
|
|
5111
|
+
);
|
|
5112
|
+
return;
|
|
5113
|
+
}
|
|
5114
|
+
gatePending = false;
|
|
5115
|
+
debug11?.("hmr: discoveryDone resolved");
|
|
5116
|
+
gateResolver();
|
|
5117
|
+
};
|
|
5118
|
+
const noteRouteEvent = () => {
|
|
5119
|
+
pendingEvents = true;
|
|
5120
|
+
beginGate();
|
|
5121
|
+
};
|
|
5122
|
+
const runRefreshCycle = async (work) => {
|
|
5123
|
+
if (inProgress) {
|
|
5124
|
+
queued = true;
|
|
5125
|
+
debug11?.("hmr: rediscovery in flight \u2014 queued for a follow-up cycle");
|
|
5126
|
+
return;
|
|
5127
|
+
}
|
|
5128
|
+
pendingEvents = false;
|
|
5129
|
+
inProgress = true;
|
|
5130
|
+
try {
|
|
5131
|
+
await work();
|
|
5132
|
+
} finally {
|
|
5133
|
+
inProgress = false;
|
|
5134
|
+
if (queued) {
|
|
5135
|
+
queued = false;
|
|
5136
|
+
debug11?.("hmr: consuming queued rediscovery");
|
|
5137
|
+
runRefreshCycle(work).catch((err) => {
|
|
5138
|
+
debug11?.(
|
|
5139
|
+
"hmr: queued cycle rejected \u2014 releasing gate (%s)",
|
|
5140
|
+
err instanceof Error ? err.message : String(err)
|
|
5141
|
+
);
|
|
5142
|
+
resolveGate();
|
|
5143
|
+
});
|
|
5144
|
+
} else if (pendingEvents) {
|
|
5145
|
+
debug11?.(
|
|
5146
|
+
"hmr: holding gate for pending events (debounce not yet fired)"
|
|
5147
|
+
);
|
|
5148
|
+
} else {
|
|
5149
|
+
resolveGate();
|
|
5150
|
+
}
|
|
5151
|
+
}
|
|
5152
|
+
};
|
|
5153
|
+
return {
|
|
5154
|
+
beginGate,
|
|
5155
|
+
resolveGate,
|
|
5156
|
+
noteRouteEvent,
|
|
5157
|
+
runRefreshCycle,
|
|
5158
|
+
state: () => ({ gatePending, inProgress, queued, pendingEvents })
|
|
5159
|
+
};
|
|
5160
|
+
}
|
|
5161
|
+
|
|
5070
5162
|
// src/vite/router-discovery.ts
|
|
5071
5163
|
var debugDiscovery = createRangoDebugger(NS.discovery);
|
|
5072
5164
|
var debugRoutes = createRangoDebugger(NS.routes);
|
|
@@ -5231,6 +5323,9 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
|
|
|
5231
5323
|
const discoveryPromise = new Promise((resolve10) => {
|
|
5232
5324
|
resolveDiscovery = resolve10;
|
|
5233
5325
|
});
|
|
5326
|
+
const gate = createDiscoveryGate(s, debugDiscovery);
|
|
5327
|
+
const beginDiscoveryGate = gate.beginGate;
|
|
5328
|
+
const resolveDiscoveryGate = gate.resolveGate;
|
|
5234
5329
|
const getDevServerOrigin = () => server.resolvedUrls?.local?.[0]?.replace(/\/$/, "") || `http://localhost:${server.config.server.port || 5173}`;
|
|
5235
5330
|
let prerenderTempServer = null;
|
|
5236
5331
|
let prerenderNodeRegistry = null;
|
|
@@ -5243,35 +5338,157 @@ function createRouterDiscoveryPlugin(entryPath, opts) {
|
|
|
5243
5338
|
releaseBuildEnv(s).catch(() => {
|
|
5244
5339
|
});
|
|
5245
5340
|
});
|
|
5341
|
+
async function importEntryAndRegistry(tempRscEnv) {
|
|
5342
|
+
const flagAlreadySet = !!globalThis.__rscRouterDiscoveryActive;
|
|
5343
|
+
if (!flagAlreadySet) {
|
|
5344
|
+
globalThis.__rscRouterDiscoveryActive = true;
|
|
5345
|
+
}
|
|
5346
|
+
try {
|
|
5347
|
+
debugDiscovery?.(
|
|
5348
|
+
"importEntryAndRegistry: importing entry (flag=%s)",
|
|
5349
|
+
globalThis.__rscRouterDiscoveryActive ?? false
|
|
5350
|
+
);
|
|
5351
|
+
await tempRscEnv.runner.import(s.resolvedEntryPath);
|
|
5352
|
+
debugDiscovery?.(
|
|
5353
|
+
"importEntryAndRegistry: entry import OK, fetching RouterRegistry"
|
|
5354
|
+
);
|
|
5355
|
+
const serverMod = await tempRscEnv.runner.import(
|
|
5356
|
+
"@rangojs/router/server"
|
|
5357
|
+
);
|
|
5358
|
+
prerenderNodeRegistry = serverMod.RouterRegistry;
|
|
5359
|
+
debugDiscovery?.(
|
|
5360
|
+
"importEntryAndRegistry: registry size=%d",
|
|
5361
|
+
prerenderNodeRegistry?.size ?? 0
|
|
5362
|
+
);
|
|
5363
|
+
} finally {
|
|
5364
|
+
if (!flagAlreadySet) {
|
|
5365
|
+
delete globalThis.__rscRouterDiscoveryActive;
|
|
5366
|
+
debugDiscovery?.(
|
|
5367
|
+
"importEntryAndRegistry: cleared __rscRouterDiscoveryActive"
|
|
5368
|
+
);
|
|
5369
|
+
}
|
|
5370
|
+
}
|
|
5371
|
+
}
|
|
5246
5372
|
async function getOrCreateTempServer() {
|
|
5247
|
-
if (
|
|
5248
|
-
|
|
5373
|
+
if (prerenderTempServer) {
|
|
5374
|
+
const existingEnv = prerenderTempServer.environments?.rsc;
|
|
5375
|
+
if (existingEnv?.runner) {
|
|
5376
|
+
if (prerenderNodeRegistry) {
|
|
5377
|
+
debugDiscovery?.(
|
|
5378
|
+
"getOrCreateTempServer: cached temp runner reused"
|
|
5379
|
+
);
|
|
5380
|
+
return existingEnv;
|
|
5381
|
+
}
|
|
5382
|
+
debugDiscovery?.(
|
|
5383
|
+
"getOrCreateTempServer: server alive but registry missing \u2014 re-importing"
|
|
5384
|
+
);
|
|
5385
|
+
try {
|
|
5386
|
+
await importEntryAndRegistry(existingEnv);
|
|
5387
|
+
return existingEnv;
|
|
5388
|
+
} catch (err) {
|
|
5389
|
+
debugDiscovery?.(
|
|
5390
|
+
"getOrCreateTempServer: reuse import failed (%s) \u2014 closing orphan and creating fresh",
|
|
5391
|
+
err?.message ?? String(err)
|
|
5392
|
+
);
|
|
5393
|
+
await prerenderTempServer.close().catch(() => {
|
|
5394
|
+
});
|
|
5395
|
+
prerenderTempServer = null;
|
|
5396
|
+
prerenderNodeRegistry = null;
|
|
5397
|
+
}
|
|
5398
|
+
} else {
|
|
5399
|
+
debugDiscovery?.(
|
|
5400
|
+
"getOrCreateTempServer: existing server has no rsc.runner \u2014 closing and recreating"
|
|
5401
|
+
);
|
|
5402
|
+
await prerenderTempServer.close().catch(() => {
|
|
5403
|
+
});
|
|
5404
|
+
prerenderTempServer = null;
|
|
5405
|
+
prerenderNodeRegistry = null;
|
|
5406
|
+
}
|
|
5249
5407
|
}
|
|
5408
|
+
debugDiscovery?.(
|
|
5409
|
+
"getOrCreateTempServer: creating new temp server, entry=%s",
|
|
5410
|
+
s.resolvedEntryPath ?? "(unset)"
|
|
5411
|
+
);
|
|
5250
5412
|
try {
|
|
5251
5413
|
prerenderTempServer = await createTempRscServer(s, {
|
|
5252
5414
|
cacheDir: "node_modules/.vite_prerender"
|
|
5253
5415
|
});
|
|
5254
5416
|
const tempRscEnv = prerenderTempServer.environments?.rsc;
|
|
5255
5417
|
if (tempRscEnv?.runner) {
|
|
5256
|
-
await tempRscEnv
|
|
5257
|
-
const serverMod = await tempRscEnv.runner.import(
|
|
5258
|
-
"@rangojs/router/server"
|
|
5259
|
-
);
|
|
5260
|
-
prerenderNodeRegistry = serverMod.RouterRegistry;
|
|
5418
|
+
await importEntryAndRegistry(tempRscEnv);
|
|
5261
5419
|
return tempRscEnv;
|
|
5262
5420
|
}
|
|
5421
|
+
debugDiscovery?.(
|
|
5422
|
+
"getOrCreateTempServer: tempRscEnv.runner unavailable"
|
|
5423
|
+
);
|
|
5263
5424
|
} catch (err) {
|
|
5425
|
+
debugDiscovery?.(
|
|
5426
|
+
"getOrCreateTempServer: FAILED message=%s",
|
|
5427
|
+
err.message
|
|
5428
|
+
);
|
|
5264
5429
|
console.warn(
|
|
5265
5430
|
`[rsc-router] Failed to create temp runner: ${err.message}`
|
|
5266
5431
|
);
|
|
5267
5432
|
}
|
|
5268
5433
|
return null;
|
|
5269
5434
|
}
|
|
5435
|
+
async function clearTempRegistries(tempRscEnv) {
|
|
5436
|
+
try {
|
|
5437
|
+
const serverMod = await tempRscEnv.runner.import(
|
|
5438
|
+
"@rangojs/router/server"
|
|
5439
|
+
);
|
|
5440
|
+
if (typeof serverMod?.RouterRegistry?.clear === "function") {
|
|
5441
|
+
serverMod.RouterRegistry.clear();
|
|
5442
|
+
}
|
|
5443
|
+
if (typeof serverMod?.HostRouterRegistry?.clear === "function") {
|
|
5444
|
+
serverMod.HostRouterRegistry.clear();
|
|
5445
|
+
}
|
|
5446
|
+
debugDiscovery?.(
|
|
5447
|
+
"clearTempRegistries: cleared RouterRegistry + HostRouterRegistry"
|
|
5448
|
+
);
|
|
5449
|
+
} catch (err) {
|
|
5450
|
+
debugDiscovery?.(
|
|
5451
|
+
"clearTempRegistries: import @rangojs/router/server failed (%s)",
|
|
5452
|
+
err?.message ?? String(err)
|
|
5453
|
+
);
|
|
5454
|
+
}
|
|
5455
|
+
}
|
|
5456
|
+
async function refreshTempRscEnv() {
|
|
5457
|
+
let tempRscEnv = await getOrCreateTempServer();
|
|
5458
|
+
if (!tempRscEnv) return null;
|
|
5459
|
+
const envGraph = tempRscEnv.moduleGraph;
|
|
5460
|
+
const serverGraph = prerenderTempServer?.moduleGraph;
|
|
5461
|
+
const target = envGraph?.invalidateAll ? envGraph : serverGraph?.invalidateAll ? serverGraph : null;
|
|
5462
|
+
if (!target) {
|
|
5463
|
+
debugDiscovery?.(
|
|
5464
|
+
"refreshTempRscEnv: invalidateAll unavailable on env+server graphs, falling back to close+recreate"
|
|
5465
|
+
);
|
|
5466
|
+
if (prerenderTempServer) {
|
|
5467
|
+
await prerenderTempServer.close().catch(() => {
|
|
5468
|
+
});
|
|
5469
|
+
prerenderTempServer = null;
|
|
5470
|
+
prerenderNodeRegistry = null;
|
|
5471
|
+
}
|
|
5472
|
+
return await getOrCreateTempServer();
|
|
5473
|
+
}
|
|
5474
|
+
debugDiscovery?.(
|
|
5475
|
+
"refreshTempRscEnv: invalidating module graph (%s)",
|
|
5476
|
+
envGraph?.invalidateAll ? "env" : "server"
|
|
5477
|
+
);
|
|
5478
|
+
target.invalidateAll();
|
|
5479
|
+
prerenderNodeRegistry = null;
|
|
5480
|
+
await clearTempRegistries(tempRscEnv);
|
|
5481
|
+
await importEntryAndRegistry(tempRscEnv);
|
|
5482
|
+
return tempRscEnv;
|
|
5483
|
+
}
|
|
5270
5484
|
const discover = async () => {
|
|
5271
5485
|
const discoverStart = performance.now();
|
|
5272
5486
|
const rscEnv = server.environments?.rsc;
|
|
5273
5487
|
if (!rscEnv?.runner) {
|
|
5274
|
-
debugDiscovery?.(
|
|
5488
|
+
debugDiscovery?.(
|
|
5489
|
+
"dev: cloudflare path start, __rscRouterDiscoveryActive=%s",
|
|
5490
|
+
globalThis.__rscRouterDiscoveryActive ?? false
|
|
5491
|
+
);
|
|
5275
5492
|
s.devServerOrigin = getDevServerOrigin();
|
|
5276
5493
|
try {
|
|
5277
5494
|
await timed(
|
|
@@ -5353,9 +5570,11 @@ ${err.stack}`
|
|
|
5353
5570
|
resolveDiscovery();
|
|
5354
5571
|
}
|
|
5355
5572
|
};
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5573
|
+
beginDiscoveryGate();
|
|
5574
|
+
setTimeout(
|
|
5575
|
+
() => discover().then(resolveDiscoveryGate, resolveDiscoveryGate),
|
|
5576
|
+
0
|
|
5577
|
+
);
|
|
5359
5578
|
let mainRegistry = null;
|
|
5360
5579
|
const propagateDiscoveryState = async (rscEnv) => {
|
|
5361
5580
|
const serverMod = await rscEnv.runner.import("@rangojs/router/server");
|
|
@@ -5504,49 +5723,80 @@ ${err.stack}`
|
|
|
5504
5723
|
return true;
|
|
5505
5724
|
};
|
|
5506
5725
|
let routeChangeTimer;
|
|
5507
|
-
let runtimeRediscoveryInProgress = false;
|
|
5508
5726
|
const refreshRuntimeDiscovery = async () => {
|
|
5509
5727
|
const rscEnv = server.environments?.rsc;
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5728
|
+
const hasMainRunner = !!rscEnv?.runner;
|
|
5729
|
+
if (!hasMainRunner && s.perRouterManifests.length === 0) return;
|
|
5730
|
+
await gate.runRefreshCycle(async () => {
|
|
5731
|
+
const hmrStart = performance.now();
|
|
5732
|
+
try {
|
|
5733
|
+
if (hasMainRunner) {
|
|
5734
|
+
await timed(
|
|
5735
|
+
debugDiscovery,
|
|
5736
|
+
"hmr discoverRouters",
|
|
5737
|
+
() => discoverRouters(s, rscEnv)
|
|
5738
|
+
);
|
|
5739
|
+
timedSync(
|
|
5740
|
+
debugDiscovery,
|
|
5741
|
+
"hmr writeRouteTypesFiles",
|
|
5742
|
+
() => writeRouteTypesFiles(s)
|
|
5743
|
+
);
|
|
5744
|
+
await timed(
|
|
5745
|
+
debugDiscovery,
|
|
5746
|
+
"hmr propagateDiscoveryState",
|
|
5747
|
+
() => propagateDiscoveryState(rscEnv)
|
|
5748
|
+
);
|
|
5749
|
+
} else {
|
|
5750
|
+
const tempRscEnv = await timed(
|
|
5751
|
+
debugDiscovery,
|
|
5752
|
+
"hmr refreshTempRscEnv (cloudflare)",
|
|
5753
|
+
() => refreshTempRscEnv()
|
|
5754
|
+
);
|
|
5755
|
+
if (!tempRscEnv) {
|
|
5756
|
+
throw new Error(
|
|
5757
|
+
"temp runner unavailable for cloudflare HMR rediscovery"
|
|
5758
|
+
);
|
|
5759
|
+
}
|
|
5760
|
+
await timed(
|
|
5761
|
+
debugDiscovery,
|
|
5762
|
+
"hmr discoverRouters (cloudflare)",
|
|
5763
|
+
() => discoverRouters(s, tempRscEnv)
|
|
5764
|
+
);
|
|
5765
|
+
timedSync(
|
|
5766
|
+
debugDiscovery,
|
|
5767
|
+
"hmr writeRouteTypesFiles",
|
|
5768
|
+
() => writeRouteTypesFiles(s)
|
|
5769
|
+
);
|
|
5770
|
+
}
|
|
5771
|
+
} catch (err) {
|
|
5772
|
+
console.warn(
|
|
5773
|
+
`[rsc-router] Runtime re-discovery failed: ${err.message}`
|
|
5774
|
+
);
|
|
5775
|
+
} finally {
|
|
5776
|
+
debugDiscovery?.(
|
|
5777
|
+
"hmr re-discovery done (%sms)",
|
|
5778
|
+
(performance.now() - hmrStart).toFixed(1)
|
|
5779
|
+
);
|
|
5780
|
+
}
|
|
5781
|
+
});
|
|
5540
5782
|
};
|
|
5541
5783
|
const scheduleRouteRegeneration = () => {
|
|
5542
5784
|
clearTimeout(routeChangeTimer);
|
|
5543
5785
|
routeChangeTimer = setTimeout(() => {
|
|
5544
5786
|
routeChangeTimer = void 0;
|
|
5545
5787
|
const regenStart = debugDiscovery ? performance.now() : 0;
|
|
5788
|
+
const rscEnv = server.environments?.rsc;
|
|
5789
|
+
const skipStaticWrite = !rscEnv?.runner && s.perRouterManifests.length > 0;
|
|
5546
5790
|
try {
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5791
|
+
if (skipStaticWrite) {
|
|
5792
|
+
debugDiscovery?.(
|
|
5793
|
+
"watcher: skipping static write (cloudflare HMR \u2014 runtime rediscovery owns gen file)"
|
|
5794
|
+
);
|
|
5795
|
+
} else {
|
|
5796
|
+
writeCombinedRouteTypesWithTracking(s);
|
|
5797
|
+
if (s.perRouterManifests.length > 0) {
|
|
5798
|
+
supplementGenFilesWithRuntimeRoutes(s);
|
|
5799
|
+
}
|
|
5550
5800
|
}
|
|
5551
5801
|
} catch (err) {
|
|
5552
5802
|
console.error(
|
|
@@ -5562,6 +5812,7 @@ ${err.stack}`
|
|
|
5562
5812
|
console.warn(
|
|
5563
5813
|
`[rsc-router] Runtime re-discovery error: ${err.message}`
|
|
5564
5814
|
);
|
|
5815
|
+
resolveDiscoveryGate();
|
|
5565
5816
|
});
|
|
5566
5817
|
}
|
|
5567
5818
|
}, 100);
|
|
@@ -5598,6 +5849,9 @@ ${err.stack}`
|
|
|
5598
5849
|
}
|
|
5599
5850
|
s.cachedRouterFiles = void 0;
|
|
5600
5851
|
}
|
|
5852
|
+
if (s.perRouterManifests.length > 0) {
|
|
5853
|
+
gate.noteRouteEvent();
|
|
5854
|
+
}
|
|
5601
5855
|
scheduleRouteRegeneration();
|
|
5602
5856
|
} catch {
|
|
5603
5857
|
}
|
|
@@ -5696,6 +5950,34 @@ ${details}`
|
|
|
5696
5950
|
);
|
|
5697
5951
|
}
|
|
5698
5952
|
},
|
|
5953
|
+
// Suppress vite's HMR cascade for our own gen-file writes.
|
|
5954
|
+
//
|
|
5955
|
+
// After every cf HMR cycle, refreshTempRscEnv → writeRouteTypesFiles
|
|
5956
|
+
// writes the configured gen files (default `router.named-routes.gen.ts`,
|
|
5957
|
+
// but the source filenames and gen suffix are user-configurable). The
|
|
5958
|
+
// chokidar watcher then fires twice independently: our
|
|
5959
|
+
// `handleRouteFileChange` (already short-circuited by
|
|
5960
|
+
// `consumeSelfGenWrite` inside `maybeHandleGeneratedRouteFileMutation`),
|
|
5961
|
+
// AND vite's own HMR pipeline (which invalidates the gen file's
|
|
5962
|
+
// importers and triggers a second workerd full reload — visible to the
|
|
5963
|
+
// user as a duplicate "[RSCRouter] HMR: version changed" on the client).
|
|
5964
|
+
//
|
|
5965
|
+
// `peekSelfGenWrite` is the authoritative filter: its map only contains
|
|
5966
|
+
// paths that `markSelfGenWrite` has registered, so it natively works
|
|
5967
|
+
// for any configured gen-file name. It is non-consuming so the chokidar
|
|
5968
|
+
// handler that fires later can still consume the same entry. Returning
|
|
5969
|
+
// [] tells vite "no modules invalidated by this change" — safe because
|
|
5970
|
+
// `s.perRouterManifests` is already up-to-date (the write that just
|
|
5971
|
+
// happened is the consequence of our just-completed rediscovery).
|
|
5972
|
+
handleHotUpdate(ctx) {
|
|
5973
|
+
if (peekSelfGenWrite(s, ctx.file)) {
|
|
5974
|
+
debugDiscovery?.(
|
|
5975
|
+
"handleHotUpdate: suppressing self-write HMR cascade for %s",
|
|
5976
|
+
ctx.file
|
|
5977
|
+
);
|
|
5978
|
+
return [];
|
|
5979
|
+
}
|
|
5980
|
+
},
|
|
5699
5981
|
// Virtual module: provides the pre-generated route manifest as a JS module
|
|
5700
5982
|
// that calls setCachedManifest() at import time.
|
|
5701
5983
|
resolveId(id) {
|
package/package.json
CHANGED
package/skills/loader/SKILL.md
CHANGED
|
@@ -248,6 +248,37 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
248
248
|
]);
|
|
249
249
|
```
|
|
250
250
|
|
|
251
|
+
### `revalidate()` return shapes
|
|
252
|
+
|
|
253
|
+
A `revalidate(fn)` callback can return one of four shapes. The chain
|
|
254
|
+
processes revalidators in order; each call's return controls how the
|
|
255
|
+
chain continues:
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
// 1) Hard decision — short-circuits the chain, used as the final answer.
|
|
259
|
+
revalidate(() => true);
|
|
260
|
+
revalidate(({ actionId }) => actionId?.includes("Cart") ?? false);
|
|
261
|
+
|
|
262
|
+
// 2) Soft decision — updates the running suggestion for downstream
|
|
263
|
+
// revalidators on the same segment, chain continues.
|
|
264
|
+
revalidate(({ defaultShouldRevalidate }) => ({
|
|
265
|
+
defaultShouldRevalidate: !defaultShouldRevalidate,
|
|
266
|
+
}));
|
|
267
|
+
|
|
268
|
+
// 3) Defer (no opinion) — leaves the running suggestion unchanged and
|
|
269
|
+
// continues to the next revalidator. Implicit return / null /
|
|
270
|
+
// undefined are all equivalent and consumer-friendly.
|
|
271
|
+
revalidate(({ actionId }) => {
|
|
272
|
+
if (actionId?.includes("Cart")) return true; // hard for this branch only
|
|
273
|
+
// implicit return — let downstream revalidators or the segment default decide
|
|
274
|
+
});
|
|
275
|
+
revalidate(() => undefined); // explicit defer
|
|
276
|
+
revalidate(() => null); // explicit defer
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
If every revalidator on a segment defers, the segment-type default
|
|
280
|
+
(e.g. params-changed for routes, `false` for parallels) is used.
|
|
281
|
+
|
|
251
282
|
### Revalidation Contracts for Loader Dependencies
|
|
252
283
|
|
|
253
284
|
If a loader reads `ctx.get()` data produced by an outer handler/layout, share
|
package/skills/parallel/SKILL.md
CHANGED
|
@@ -347,6 +347,13 @@ Revalidating only the parallel does not re-run outer handlers/layouts.
|
|
|
347
347
|
If the slot reads `ctx.get()` data established above it, opt the outer
|
|
348
348
|
segment into revalidation as well.
|
|
349
349
|
|
|
350
|
+
A `revalidate()` callback may return a hard `boolean`, a soft
|
|
351
|
+
`{ defaultShouldRevalidate }` object, or nothing (`void` / `null` /
|
|
352
|
+
`undefined`) to defer to the next revalidator. See
|
|
353
|
+
[loader/SKILL.md#revalidate-return-shapes](../loader/SKILL.md#revalidate-return-shapes)
|
|
354
|
+
for the full contract — it's the same across `loader()`, `path()`,
|
|
355
|
+
`layout()`, `parallel()`, and `intercept()`.
|
|
356
|
+
|
|
350
357
|
### Revalidation Contracts for Parallel Dependencies
|
|
351
358
|
|
|
352
359
|
Prefer named revalidation contracts shared by both the upstream producer and
|
|
@@ -259,7 +259,12 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
259
259
|
* ({ defaultShouldRevalidate: true })
|
|
260
260
|
* )
|
|
261
261
|
* ```
|
|
262
|
-
* @param fn - Function
|
|
262
|
+
* @param fn - Function returning either:
|
|
263
|
+
* - `boolean` (hard decision — short-circuits the chain),
|
|
264
|
+
* - `{ defaultShouldRevalidate: boolean }` (soft — updates the suggestion
|
|
265
|
+
* for downstream revalidators),
|
|
266
|
+
* - or nothing / `null` / `undefined` (defer — leaves the suggestion
|
|
267
|
+
* unchanged and continues to the next revalidator).
|
|
263
268
|
*/
|
|
264
269
|
revalidate: (fn: ShouldRevalidateFn<any, TEnv>) => RevalidateItem;
|
|
265
270
|
/**
|
|
@@ -471,13 +471,16 @@ export type RevalidateParams<TParams = GenericParams, TEnv = any> = Parameters<
|
|
|
471
471
|
* **Return Types:**
|
|
472
472
|
* - `boolean` - Hard decision: immediately returns this value (short-circuits)
|
|
473
473
|
* - `{ defaultShouldRevalidate: boolean }` - Soft decision: updates suggestion for next revalidator
|
|
474
|
+
* - `void` / `null` / `undefined` - Defer to the current suggestion (no opinion); the
|
|
475
|
+
* loop continues to the next revalidator without changing the running default
|
|
474
476
|
*
|
|
475
477
|
* **Execution Flow:**
|
|
476
478
|
* 1. Start with built-in `defaultShouldRevalidate` (true if params changed)
|
|
477
479
|
* 2. Execute global revalidators first, then route-specific
|
|
478
480
|
* 3. Hard decision (boolean): stop immediately and use that value
|
|
479
481
|
* 4. Soft decision (object): update suggestion and continue to next revalidator
|
|
480
|
-
* 5.
|
|
482
|
+
* 5. Defer (`void` / `null` / `undefined`): leave suggestion unchanged and continue
|
|
483
|
+
* 6. If no hard decision was returned: use the final running suggestion
|
|
481
484
|
*
|
|
482
485
|
* @param args.currentParams - Previous route params (generic by default, can be narrowed)
|
|
483
486
|
* @param args.currentUrl - Previous URL
|
|
@@ -489,7 +492,8 @@ export type RevalidateParams<TParams = GenericParams, TEnv = any> = Parameters<
|
|
|
489
492
|
* @param args.formData - Form data from action (future support)
|
|
490
493
|
* @param args.formMethod - HTTP method from action (future support)
|
|
491
494
|
*
|
|
492
|
-
* @returns Hard decision (boolean)
|
|
495
|
+
* @returns Hard decision (boolean), soft suggestion (object), or defer
|
|
496
|
+
* (`void` / `null` / `undefined`) to keep the running suggestion as-is.
|
|
493
497
|
*
|
|
494
498
|
* @example
|
|
495
499
|
* ```typescript
|
|
@@ -514,8 +518,9 @@ export type RevalidateParams<TParams = GenericParams, TEnv = any> = Parameters<
|
|
|
514
518
|
* a segment (layout, route, parallel slot, or loader) should be re-rendered.
|
|
515
519
|
*
|
|
516
520
|
* Return `true` to re-render, `false` to skip (keep client's current version),
|
|
517
|
-
*
|
|
518
|
-
* downstream
|
|
521
|
+
* `{ defaultShouldRevalidate: boolean }` to update the running suggestion for
|
|
522
|
+
* downstream revalidators, or nothing (`void` / `null` / `undefined`) to defer
|
|
523
|
+
* to the current suggestion without changing it.
|
|
519
524
|
*
|
|
520
525
|
* @example
|
|
521
526
|
* ```ts
|
|
@@ -615,7 +620,7 @@ export type ShouldRevalidateFn<TParams = GenericParams, TEnv = any> = (args: {
|
|
|
615
620
|
* action that may have mutated backend state.
|
|
616
621
|
*/
|
|
617
622
|
stale?: boolean;
|
|
618
|
-
}) => boolean | { defaultShouldRevalidate: boolean };
|
|
623
|
+
}) => boolean | { defaultShouldRevalidate: boolean } | null | void;
|
|
619
624
|
|
|
620
625
|
// MiddlewareFn is imported from "../router/middleware.js" and re-exported
|
|
621
626
|
|