@rangojs/router 0.0.0-experimental.259 → 0.0.0-experimental.26
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/README.md +294 -28
- package/dist/bin/rango.js +355 -47
- package/dist/vite/index.js +1658 -1239
- package/package.json +3 -3
- package/skills/cache-guide/SKILL.md +9 -5
- package/skills/caching/SKILL.md +4 -4
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/hooks/SKILL.md +40 -29
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +79 -0
- package/skills/layout/SKILL.md +62 -2
- package/skills/loader/SKILL.md +229 -15
- package/skills/middleware/SKILL.md +109 -30
- package/skills/parallel/SKILL.md +57 -2
- package/skills/prerender/SKILL.md +189 -19
- package/skills/rango/SKILL.md +1 -2
- package/skills/response-routes/SKILL.md +3 -3
- package/skills/route/SKILL.md +44 -3
- package/skills/router-setup/SKILL.md +80 -3
- package/skills/theme/SKILL.md +5 -4
- package/skills/typesafety/SKILL.md +59 -16
- package/skills/use-cache/SKILL.md +16 -2
- package/src/__internal.ts +1 -1
- package/src/bin/rango.ts +56 -19
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/event-controller.ts +29 -48
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +1 -1
- package/src/browser/link-interceptor.ts +19 -3
- package/src/browser/merge-segment-loaders.ts +9 -2
- package/src/browser/navigation-bridge.ts +66 -443
- package/src/browser/navigation-client.ts +34 -62
- package/src/browser/navigation-store.ts +4 -33
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/partial-update.ts +103 -151
- package/src/browser/prefetch/cache.ts +67 -0
- package/src/browser/prefetch/fetch.ts +137 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/prefetch/queue.ts +88 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +154 -44
- package/src/browser/react/NavigationProvider.tsx +32 -0
- package/src/browser/react/context.ts +6 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +2 -6
- package/src/browser/react/location-state-shared.ts +29 -11
- package/src/browser/react/location-state.ts +6 -4
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +23 -45
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +21 -64
- package/src/browser/react/use-navigation.ts +7 -32
- package/src/browser/react/use-params.ts +5 -34
- package/src/browser/react/use-pathname.ts +2 -3
- package/src/browser/react/use-router.ts +3 -6
- package/src/browser/react/use-search-params.ts +2 -1
- package/src/browser/react/use-segments.ts +75 -114
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +46 -22
- package/src/browser/scroll-restoration.ts +10 -7
- package/src/browser/server-action-bridge.ts +458 -405
- package/src/browser/types.ts +21 -35
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +38 -13
- package/src/build/generate-route-types.ts +4 -0
- package/src/build/index.ts +1 -0
- package/src/build/route-trie.ts +19 -3
- package/src/build/route-types/codegen.ts +13 -4
- package/src/build/route-types/include-resolution.ts +13 -0
- package/src/build/route-types/per-module-writer.ts +15 -3
- package/src/build/route-types/router-processing.ts +170 -18
- package/src/build/runtime-discovery.ts +13 -1
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +136 -123
- package/src/cache/cache-scope.ts +76 -83
- package/src/cache/cf/cf-cache-store.ts +12 -7
- package/src/cache/document-cache.ts +93 -69
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/index.ts +0 -15
- package/src/cache/memory-segment-store.ts +43 -69
- package/src/cache/profile-registry.ts +43 -8
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +140 -117
- package/src/cache/taint.ts +30 -3
- package/src/cache/types.ts +1 -115
- package/src/client.rsc.tsx +0 -1
- package/src/client.tsx +53 -76
- package/src/errors.ts +6 -1
- package/src/handle.ts +1 -1
- package/src/handles/MetaTags.tsx +5 -2
- package/src/host/cookie-handler.ts +8 -3
- package/src/host/index.ts +0 -3
- package/src/host/router.ts +14 -1
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +53 -10
- package/src/index.ts +73 -43
- package/src/loader.rsc.ts +12 -4
- package/src/loader.ts +8 -0
- package/src/prerender/store.ts +60 -18
- package/src/prerender.ts +76 -18
- package/src/reverse.ts +11 -7
- package/src/root-error-boundary.tsx +30 -26
- package/src/route-definition/dsl-helpers.ts +9 -6
- package/src/route-definition/index.ts +0 -3
- package/src/route-definition/redirect.ts +15 -3
- package/src/route-map-builder.ts +38 -2
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +7 -0
- package/src/router/content-negotiation.ts +1 -1
- package/src/router/debug-manifest.ts +16 -3
- package/src/router/handler-context.ts +96 -17
- package/src/router/intercept-resolution.ts +6 -4
- package/src/router/lazy-includes.ts +4 -0
- package/src/router/loader-resolution.ts +6 -11
- package/src/router/logging.ts +100 -3
- package/src/router/manifest.ts +32 -3
- package/src/router/match-api.ts +62 -54
- package/src/router/match-context.ts +3 -0
- package/src/router/match-handlers.ts +185 -11
- package/src/router/match-middleware/background-revalidation.ts +65 -85
- package/src/router/match-middleware/cache-lookup.ts +78 -10
- package/src/router/match-middleware/cache-store.ts +2 -0
- package/src/router/match-pipelines.ts +8 -43
- package/src/router/match-result.ts +0 -9
- package/src/router/metrics.ts +233 -13
- package/src/router/middleware-types.ts +34 -39
- package/src/router/middleware.ts +290 -130
- package/src/router/pattern-matching.ts +61 -10
- package/src/router/prerender-match.ts +36 -6
- package/src/router/preview-match.ts +7 -1
- package/src/router/revalidation.ts +61 -2
- package/src/router/router-context.ts +15 -0
- package/src/router/router-interfaces.ts +158 -40
- package/src/router/router-options.ts +223 -1
- package/src/router/router-registry.ts +5 -2
- package/src/router/segment-resolution/fresh.ts +165 -242
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +102 -98
- package/src/router/segment-resolution/revalidation.ts +394 -272
- package/src/router/segment-resolution/static-store.ts +2 -2
- package/src/router/segment-resolution.ts +1 -3
- package/src/router/segment-wrappers.ts +3 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +20 -2
- package/src/router/types.ts +7 -1
- package/src/router.ts +203 -18
- package/src/rsc/handler-context.ts +13 -2
- package/src/rsc/handler.ts +489 -438
- package/src/rsc/helpers.ts +125 -5
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +84 -42
- package/src/rsc/manifest-init.ts +3 -2
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +245 -19
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +47 -43
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +166 -66
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +20 -2
- package/src/search-params.ts +38 -23
- package/src/server/context.ts +61 -7
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +84 -12
- package/src/server/loader-registry.ts +11 -46
- package/src/server/request-context.ts +275 -49
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +67 -28
- package/src/static-handler.ts +7 -0
- package/src/theme/ThemeProvider.tsx +6 -1
- package/src/theme/index.ts +4 -18
- package/src/theme/theme-context.ts +1 -28
- package/src/theme/theme-script.ts +2 -1
- package/src/types/cache-types.ts +6 -1
- package/src/types/error-types.ts +3 -0
- package/src/types/global-namespace.ts +22 -0
- package/src/types/handler-context.ts +103 -16
- package/src/types/index.ts +1 -1
- package/src/types/loader-types.ts +9 -6
- package/src/types/route-config.ts +17 -26
- package/src/types/route-entry.ts +28 -0
- package/src/types/segments.ts +0 -5
- package/src/urls/include-helper.ts +49 -8
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +30 -12
- package/src/urls/path-helper.ts +17 -2
- package/src/urls/pattern-types.ts +21 -1
- package/src/urls/response-types.ts +29 -7
- package/src/urls/type-extraction.ts +23 -15
- package/src/use-loader.tsx +27 -9
- package/src/vite/discovery/bundle-postprocess.ts +32 -52
- package/src/vite/discovery/discover-routers.ts +52 -26
- package/src/vite/discovery/prerender-collection.ts +58 -41
- package/src/vite/discovery/route-types-writer.ts +7 -7
- package/src/vite/discovery/state.ts +7 -7
- package/src/vite/discovery/virtual-module-codegen.ts +5 -2
- package/src/vite/index.ts +10 -51
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +3 -3
- package/src/vite/plugins/expose-internal-ids.ts +4 -3
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +91 -3
- package/src/vite/plugins/version-plugin.ts +188 -18
- package/src/vite/rango.ts +61 -36
- package/src/vite/router-discovery.ts +173 -100
- package/src/vite/utils/prerender-utils.ts +81 -0
- package/src/vite/utils/shared-utils.ts +19 -9
- package/skills/testing/SKILL.md +0 -226
- package/src/browser/lru-cache.ts +0 -61
- package/src/browser/react/prefetch.ts +0 -27
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/route-definition/route-function.ts +0 -119
- package/src/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- /package/{CLAUDE.md → AGENTS.md} +0 -0
|
@@ -17,8 +17,7 @@ import { urlpatterns } from "./urls";
|
|
|
17
17
|
|
|
18
18
|
const router = createRouter<AppBindings>({
|
|
19
19
|
document: Document,
|
|
20
|
-
|
|
21
|
-
});
|
|
20
|
+
}).routes(urlpatterns);
|
|
22
21
|
|
|
23
22
|
// Server-side named-route reverse (type-safe via routeMap)
|
|
24
23
|
export const reverse = router.reverse;
|
|
@@ -26,6 +25,39 @@ export const reverse = router.reverse;
|
|
|
26
25
|
export default router;
|
|
27
26
|
```
|
|
28
27
|
|
|
28
|
+
### Which global type should I use?
|
|
29
|
+
|
|
30
|
+
Use the generated route map by default. Manual `RegisteredRoutes` augmentation
|
|
31
|
+
is only needed when you want the richer `typeof router.routeMap` shape
|
|
32
|
+
available globally.
|
|
33
|
+
|
|
34
|
+
- `GeneratedRouteMap` — auto-registered by `router.named-routes.gen.ts`
|
|
35
|
+
Use for `Handler<"name">`, `Prerender<"name">`, server `ctx.reverse()`,
|
|
36
|
+
and named-route param/search inference.
|
|
37
|
+
- `typeof router.routeMap` — the real merged route map from your router
|
|
38
|
+
instance, including response-route metadata such as `{ path, response }`.
|
|
39
|
+
- `RegisteredRoutes` — manual global hook for exposing `typeof router.routeMap`
|
|
40
|
+
to utilities like `href()`, `ValidPaths`, and `PathResponse`.
|
|
41
|
+
|
|
42
|
+
Recommended setup:
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
// router.tsx
|
|
46
|
+
import { createRouter } from "@rangojs/router";
|
|
47
|
+
import { urlpatterns } from "./urls";
|
|
48
|
+
import type { AppBindings, AppVars } from "./env";
|
|
49
|
+
|
|
50
|
+
export const router = createRouter<AppBindings>({}).routes(urlpatterns);
|
|
51
|
+
|
|
52
|
+
declare global {
|
|
53
|
+
namespace RSCRouter {
|
|
54
|
+
interface Env extends AppBindings {}
|
|
55
|
+
interface Vars extends AppVars {}
|
|
56
|
+
interface RegisteredRoutes extends typeof router.routeMap {}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
29
61
|
## Route Definition with Type-Safe Names
|
|
30
62
|
|
|
31
63
|
```typescript
|
|
@@ -95,6 +127,17 @@ function ShopNav() {
|
|
|
95
127
|
}
|
|
96
128
|
```
|
|
97
129
|
|
|
130
|
+
`href()` and path-based response utilities read from `RegisteredRoutes`, so if
|
|
131
|
+
you want them typed globally you should augment:
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
declare global {
|
|
135
|
+
namespace RSCRouter {
|
|
136
|
+
interface RegisteredRoutes extends typeof router.routeMap {}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
98
141
|
See `/links` for full URL generation guide.
|
|
99
142
|
|
|
100
143
|
## Environment Type Setup
|
|
@@ -128,8 +171,7 @@ import type { AppBindings, AppVariables } from "./env";
|
|
|
128
171
|
|
|
129
172
|
const router = createRouter<AppBindings>({
|
|
130
173
|
document: Document,
|
|
131
|
-
|
|
132
|
-
});
|
|
174
|
+
}).routes(urlpatterns);
|
|
133
175
|
|
|
134
176
|
// Register bindings and variables globally for implicit typing
|
|
135
177
|
declare global {
|
|
@@ -140,19 +182,19 @@ declare global {
|
|
|
140
182
|
}
|
|
141
183
|
|
|
142
184
|
// middleware - typed via ctx.set / ctx.get
|
|
143
|
-
import {
|
|
185
|
+
import type { Middleware } from "@rangojs/router";
|
|
144
186
|
|
|
145
|
-
export const authMiddleware =
|
|
187
|
+
export const authMiddleware: Middleware = async (ctx, next) => {
|
|
146
188
|
ctx.set("user", {
|
|
147
189
|
id: "123",
|
|
148
190
|
email: "user@example.com",
|
|
149
191
|
role: "admin",
|
|
150
192
|
});
|
|
151
193
|
await next();
|
|
152
|
-
}
|
|
194
|
+
};
|
|
153
195
|
|
|
154
196
|
// loaders - typed context
|
|
155
|
-
export const UserLoader = createLoader(
|
|
197
|
+
export const UserLoader = createLoader(async (ctx) => {
|
|
156
198
|
const db = ctx.env.DB; // D1Database (plain bindings)
|
|
157
199
|
const userId = ctx.get("user")?.id; // from RSCRouter.Vars
|
|
158
200
|
return db.prepare("SELECT * FROM users WHERE id = ?").bind(userId).first();
|
|
@@ -177,7 +219,7 @@ Now handlers have typed context without explicit imports:
|
|
|
177
219
|
|
|
178
220
|
```typescript
|
|
179
221
|
// In loaders
|
|
180
|
-
export const DashboardLoader = createLoader(
|
|
222
|
+
export const DashboardLoader = createLoader(async (ctx) => {
|
|
181
223
|
// ctx.env.DB is typed from global RSCRouter.Env
|
|
182
224
|
// ctx.get("user") is typed from global RSCRouter.Vars
|
|
183
225
|
const user = ctx.get("user");
|
|
@@ -239,7 +281,7 @@ import type { RouteSearchParams, RouteParams } from "@rangojs/router";
|
|
|
239
281
|
|
|
240
282
|
// RouteSearchParams<"name"> resolves the search schema to a typed object
|
|
241
283
|
type SP = RouteSearchParams<"search">;
|
|
242
|
-
// { q: string; page?: number; sort?: string }
|
|
284
|
+
// { q: string | undefined; page?: number; sort?: string }
|
|
243
285
|
|
|
244
286
|
// RouteParams<"name"> resolves URL params from the route pattern
|
|
245
287
|
type P = RouteParams<"blogPost">;
|
|
@@ -283,7 +325,7 @@ Loaders have typed return values:
|
|
|
283
325
|
|
|
284
326
|
```typescript
|
|
285
327
|
// loaders/product.ts
|
|
286
|
-
export const ProductLoader = createLoader(
|
|
328
|
+
export const ProductLoader = createLoader(async (ctx) => {
|
|
287
329
|
return {
|
|
288
330
|
id: ctx.params.slug,
|
|
289
331
|
name: "Widget",
|
|
@@ -292,7 +334,7 @@ export const ProductLoader = createLoader("product", async (ctx) => {
|
|
|
292
334
|
});
|
|
293
335
|
|
|
294
336
|
// In server component - type is inferred
|
|
295
|
-
import { useLoader } from "@rangojs/router";
|
|
337
|
+
import { useLoader } from "@rangojs/router/client";
|
|
296
338
|
|
|
297
339
|
async function ProductPage() {
|
|
298
340
|
const product = await useLoader(ProductLoader);
|
|
@@ -302,11 +344,12 @@ async function ProductPage() {
|
|
|
302
344
|
|
|
303
345
|
// In client component - same type
|
|
304
346
|
"use client";
|
|
305
|
-
import {
|
|
347
|
+
import { useLoader } from "@rangojs/router/client";
|
|
306
348
|
|
|
307
349
|
function ProductPrice() {
|
|
308
|
-
const {
|
|
309
|
-
//
|
|
350
|
+
const { data } = useLoader(ProductLoader);
|
|
351
|
+
// data: { id: string; name: string; price: number }
|
|
352
|
+
const product = data;
|
|
310
353
|
return <span>${product.price}</span>;
|
|
311
354
|
}
|
|
312
355
|
```
|
|
@@ -559,7 +602,7 @@ export default router;
|
|
|
559
602
|
// No manual RegisteredRoutes declaration needed.
|
|
560
603
|
|
|
561
604
|
// 5. loaders/*.ts - Type-safe loaders
|
|
562
|
-
export const ProductLoader = createLoader(
|
|
605
|
+
export const ProductLoader = createLoader(async (ctx) => {
|
|
563
606
|
// ctx.params: { slug: string }
|
|
564
607
|
// ctx.get("user"): User | undefined (from RSCRouter.Vars)
|
|
565
608
|
// ctx.env.DB: D1Database (plain bindings from RSCRouter.Env)
|
|
@@ -102,14 +102,28 @@ export async function getProductData(ctx) {
|
|
|
102
102
|
// On hit: return value restored, breadcrumb replayed.
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
##
|
|
105
|
+
## Request-Scoped Guards
|
|
106
|
+
|
|
107
|
+
### Read Guards
|
|
108
|
+
|
|
109
|
+
`cookies()` and `headers()` **throw** inside a `"use cache"` function because
|
|
110
|
+
per-request values (cookies, headers) are not reflected in the cache key. Without
|
|
111
|
+
this guard, one user's data would be served to another.
|
|
112
|
+
|
|
113
|
+
Extract the value before the cached function and pass it as an argument:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
const locale = cookies().get("locale")?.value ?? "en";
|
|
117
|
+
const data = await getCachedData(locale); // locale is now in the cache key
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Side-Effect Guards
|
|
106
121
|
|
|
107
122
|
These ctx methods **throw** inside a `"use cache"` function because their effects
|
|
108
123
|
are lost on cache hit (the function body is skipped):
|
|
109
124
|
|
|
110
125
|
- `ctx.set()` / `ctx.get()` for passing values to children
|
|
111
126
|
- `ctx.header()`
|
|
112
|
-
- `ctx.setCookie()` / `ctx.deleteCookie()`
|
|
113
127
|
- `ctx.setTheme()`
|
|
114
128
|
- `ctx.setLocationState()`
|
|
115
129
|
- `ctx.onResponse()`
|
package/src/__internal.ts
CHANGED
|
@@ -160,7 +160,7 @@ export type {
|
|
|
160
160
|
/**
|
|
161
161
|
* @internal
|
|
162
162
|
* Internal handler context with additional props for router internals.
|
|
163
|
-
* Includes `
|
|
163
|
+
* Includes `_currentSegmentId` and `_responseType`.
|
|
164
164
|
*/
|
|
165
165
|
export type { InternalHandlerContext } from "./types.js";
|
|
166
166
|
|
package/src/bin/rango.ts
CHANGED
|
@@ -5,6 +5,9 @@ import {
|
|
|
5
5
|
writePerModuleRouteTypesForFile,
|
|
6
6
|
writeCombinedRouteTypes,
|
|
7
7
|
detectUnresolvableIncludes,
|
|
8
|
+
detectUnresolvableIncludesForUrlsFile,
|
|
9
|
+
findNestedRouterConflict,
|
|
10
|
+
formatNestedRouterConflictError,
|
|
8
11
|
type UnresolvableInclude,
|
|
9
12
|
} from "../build/generate-route-types.ts";
|
|
10
13
|
|
|
@@ -131,22 +134,18 @@ function runStaticGeneration(args: string[], mode: "default" | "static") {
|
|
|
131
134
|
process.exit(0);
|
|
132
135
|
}
|
|
133
136
|
|
|
137
|
+
// Phase 1: Classify files
|
|
134
138
|
const routerFiles: string[] = [];
|
|
139
|
+
const urlsFiles: string[] = [];
|
|
135
140
|
|
|
136
141
|
for (const filePath of files) {
|
|
137
142
|
try {
|
|
138
143
|
const source = readFileSync(filePath, "utf-8");
|
|
139
|
-
|
|
140
|
-
// Detect file type and generate accordingly
|
|
141
|
-
const isRouter = /\bcreateRouter\s*[<(]/.test(source);
|
|
142
|
-
const isUrls = source.includes("urls(");
|
|
143
|
-
|
|
144
|
-
if (isRouter) {
|
|
144
|
+
if (/\bcreateRouter\s*[<(]/.test(source)) {
|
|
145
145
|
routerFiles.push(filePath);
|
|
146
146
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
writePerModuleRouteTypesForFile(filePath);
|
|
147
|
+
if (source.includes("urls(")) {
|
|
148
|
+
urlsFiles.push(filePath);
|
|
150
149
|
}
|
|
151
150
|
} catch (err) {
|
|
152
151
|
console.warn(
|
|
@@ -155,9 +154,10 @@ function runStaticGeneration(args: string[], mode: "default" | "static") {
|
|
|
155
154
|
}
|
|
156
155
|
}
|
|
157
156
|
|
|
158
|
-
//
|
|
157
|
+
// Phase 2: Collect diagnostics from all files BEFORE writing anything
|
|
159
158
|
const allDiagnostics: Array<UnresolvableInclude & { routerFile: string }> =
|
|
160
159
|
[];
|
|
160
|
+
|
|
161
161
|
for (const routerFile of routerFiles) {
|
|
162
162
|
const diagnostics = detectUnresolvableIncludes(routerFile);
|
|
163
163
|
for (const d of diagnostics) {
|
|
@@ -165,10 +165,29 @@ function runStaticGeneration(args: string[], mode: "default" | "static") {
|
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
|
|
169
|
-
|
|
168
|
+
// Also check standalone urls files not covered by router-level detection
|
|
169
|
+
const routerFileSet = new Set(routerFiles);
|
|
170
|
+
for (const urlsFile of urlsFiles) {
|
|
171
|
+
if (routerFileSet.has(urlsFile)) continue;
|
|
172
|
+
const diagnostics = detectUnresolvableIncludesForUrlsFile(urlsFile);
|
|
173
|
+
for (const d of diagnostics) {
|
|
174
|
+
allDiagnostics.push({ ...d, routerFile: urlsFile });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Deduplicate diagnostics (router and urls detection may find the same issue)
|
|
179
|
+
const seen = new Set<string>();
|
|
180
|
+
const uniqueDiagnostics = allDiagnostics.filter((d) => {
|
|
181
|
+
const key = `${d.sourceFile}:${d.pathPrefix}:${d.reason}`;
|
|
182
|
+
if (seen.has(key)) return false;
|
|
183
|
+
seen.add(key);
|
|
184
|
+
return true;
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (uniqueDiagnostics.length > 0 && mode === "default") {
|
|
188
|
+
// Hard error: no files written
|
|
170
189
|
console.error("\n[rango] Unresolvable includes detected:\n");
|
|
171
|
-
formatDiagnostics(
|
|
190
|
+
formatDiagnostics(uniqueDiagnostics);
|
|
172
191
|
console.error(
|
|
173
192
|
"\nThe static parser cannot resolve these includes because they use " +
|
|
174
193
|
"factory functions or dynamic expressions.\n\n" +
|
|
@@ -179,16 +198,28 @@ function runStaticGeneration(args: string[], mode: "default" | "static") {
|
|
|
179
198
|
process.exit(1);
|
|
180
199
|
}
|
|
181
200
|
|
|
182
|
-
if (
|
|
201
|
+
if (uniqueDiagnostics.length > 0 && mode === "static") {
|
|
183
202
|
// Warning: partial output accepted
|
|
184
203
|
console.warn(
|
|
185
204
|
"\n[rango] Warning: partial output (unresolvable includes):\n",
|
|
186
205
|
);
|
|
187
|
-
formatDiagnostics(
|
|
206
|
+
formatDiagnostics(uniqueDiagnostics);
|
|
188
207
|
console.warn("");
|
|
189
208
|
}
|
|
190
209
|
|
|
191
|
-
|
|
210
|
+
const nestedRouterConflict = findNestedRouterConflict(routerFiles);
|
|
211
|
+
if (nestedRouterConflict) {
|
|
212
|
+
console.error(
|
|
213
|
+
`\n${formatNestedRouterConflictError(nestedRouterConflict, "[rango]")}\n`,
|
|
214
|
+
);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Phase 3: Write all outputs (only reached if diagnostics pass or --static)
|
|
219
|
+
for (const urlsFile of urlsFiles) {
|
|
220
|
+
writePerModuleRouteTypesForFile(urlsFile);
|
|
221
|
+
}
|
|
222
|
+
|
|
192
223
|
for (const routerFile of routerFiles) {
|
|
193
224
|
const projectRoot = findProjectRoot(routerFile);
|
|
194
225
|
writeCombinedRouteTypes(projectRoot, [routerFile]);
|
|
@@ -238,6 +269,14 @@ async function runRuntimeDiscovery(args: string[], configFile?: string) {
|
|
|
238
269
|
process.exit(1);
|
|
239
270
|
}
|
|
240
271
|
|
|
272
|
+
const nestedRouterConflict = findNestedRouterConflict(routerEntries);
|
|
273
|
+
if (nestedRouterConflict) {
|
|
274
|
+
console.error(
|
|
275
|
+
`\n${formatNestedRouterConflictError(nestedRouterConflict, "[rango]")}\n`,
|
|
276
|
+
);
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
|
|
241
280
|
let discoverAndWriteRouteTypes: typeof import("../build/runtime-discovery.ts").discoverAndWriteRouteTypes;
|
|
242
281
|
try {
|
|
243
282
|
const mod = await import("../build/runtime-discovery.ts");
|
|
@@ -257,10 +296,8 @@ async function runRuntimeDiscovery(args: string[], configFile?: string) {
|
|
|
257
296
|
process.exit(1);
|
|
258
297
|
}
|
|
259
298
|
|
|
260
|
-
// Use a single project root for all routers (find from the first entry)
|
|
261
|
-
const projectRoot = findProjectRoot(routerEntries[0]);
|
|
262
|
-
|
|
263
299
|
for (const entry of routerEntries) {
|
|
300
|
+
const projectRoot = findProjectRoot(entry);
|
|
264
301
|
const result = await discoverAndWriteRouteTypes({
|
|
265
302
|
root: projectRoot,
|
|
266
303
|
configFile,
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {
|
|
2
|
+
classifyActionResponse,
|
|
3
|
+
type ActionScenario,
|
|
4
|
+
} from "./action-response-classifier.js";
|
|
5
|
+
import type { ActionEntry } from "./event-controller.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Plain data inputs for classifying a post-reconciliation action outcome.
|
|
9
|
+
* No browser objects or controller references — all values are snapshots.
|
|
10
|
+
*/
|
|
11
|
+
export interface ActionOutcomeInput {
|
|
12
|
+
/** This action's unique instance ID */
|
|
13
|
+
handleId: string;
|
|
14
|
+
/** All in-flight action entries (snapshot from event controller) */
|
|
15
|
+
inflightActions: Map<string, ActionEntry>;
|
|
16
|
+
/** Whether any concurrent actions occurred (controller-level shared flag) */
|
|
17
|
+
hadAnyConcurrentActions: boolean;
|
|
18
|
+
/** Segments revalidated by concurrent actions (from tracking set) */
|
|
19
|
+
revalidatedSegments: Set<string>;
|
|
20
|
+
/** window.location.pathname captured at action start */
|
|
21
|
+
actionStartPathname: string;
|
|
22
|
+
/** window.location.pathname at classification time */
|
|
23
|
+
currentPathname: string;
|
|
24
|
+
/** window.history.state?.key captured at action start */
|
|
25
|
+
actionStartLocationKey: string | undefined;
|
|
26
|
+
/** window.history.state?.key at classification time */
|
|
27
|
+
currentLocationKey: string | undefined;
|
|
28
|
+
/** Number of segments after reconciliation */
|
|
29
|
+
reconciledSegmentCount: number;
|
|
30
|
+
/** Number of matched segment IDs from server */
|
|
31
|
+
matchedCount: number;
|
|
32
|
+
/** Current intercept source URL (null when not on intercept route) */
|
|
33
|
+
currentInterceptSource: string | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Compute consolidation segments from concurrent action state.
|
|
38
|
+
*
|
|
39
|
+
* Returns segment IDs that need re-fetching when concurrent actions
|
|
40
|
+
* have each revalidated different parts of the tree, or null if
|
|
41
|
+
* consolidation is not needed.
|
|
42
|
+
*/
|
|
43
|
+
function computeConsolidationSegments(
|
|
44
|
+
input: ActionOutcomeInput,
|
|
45
|
+
): string[] | null {
|
|
46
|
+
if (!input.hadAnyConcurrentActions) return null;
|
|
47
|
+
if (input.revalidatedSegments.size === 0) return null;
|
|
48
|
+
|
|
49
|
+
// Can't consolidate while any action is still waiting for a server response
|
|
50
|
+
const stillFetchingCount = [...input.inflightActions.values()].filter(
|
|
51
|
+
(a) => a.phase === "fetching",
|
|
52
|
+
).length;
|
|
53
|
+
if (stillFetchingCount > 0) return null;
|
|
54
|
+
|
|
55
|
+
return Array.from(input.revalidatedSegments);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Count other actions still in "fetching" phase (excluding this handle).
|
|
60
|
+
*/
|
|
61
|
+
function countOtherFetchingActions(input: ActionOutcomeInput): number {
|
|
62
|
+
let count = 0;
|
|
63
|
+
for (const [, a] of input.inflightActions) {
|
|
64
|
+
if (a.phase === "fetching" && a.id !== input.handleId) {
|
|
65
|
+
count++;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return count;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Classify a post-reconciliation action outcome into one of 5 scenarios.
|
|
73
|
+
*
|
|
74
|
+
* This is the single entry point for post-action decision logic.
|
|
75
|
+
* It gathers consolidation and concurrency data from the plain inputs,
|
|
76
|
+
* then delegates to the pure classifyActionResponse function.
|
|
77
|
+
*
|
|
78
|
+
* The server-action-bridge calls this after reconciliation to decide
|
|
79
|
+
* whether to render, skip, consolidate, or refetch.
|
|
80
|
+
*/
|
|
81
|
+
export function classifyActionOutcome(
|
|
82
|
+
input: ActionOutcomeInput,
|
|
83
|
+
): ActionScenario {
|
|
84
|
+
return classifyActionResponse({
|
|
85
|
+
actionStartPathname: input.actionStartPathname,
|
|
86
|
+
currentPathname: input.currentPathname,
|
|
87
|
+
actionStartLocationKey: input.actionStartLocationKey,
|
|
88
|
+
currentLocationKey: input.currentLocationKey,
|
|
89
|
+
reconciledSegmentCount: input.reconciledSegmentCount,
|
|
90
|
+
matchedCount: input.matchedCount,
|
|
91
|
+
currentInterceptSource: input.currentInterceptSource,
|
|
92
|
+
consolidationSegments: computeConsolidationSegments(input),
|
|
93
|
+
otherFetchingActionCount: countOtherFetchingActions(input),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type { ActionScenario };
|
|
@@ -8,7 +8,9 @@ import type {
|
|
|
8
8
|
ResolvedSegment,
|
|
9
9
|
RscMetadata,
|
|
10
10
|
HandleData,
|
|
11
|
+
StreamingToken,
|
|
11
12
|
} from "./types.js";
|
|
13
|
+
import { filterSegmentOrder } from "./react/filter-segment-order.js";
|
|
12
14
|
|
|
13
15
|
// Polyfill Symbol.dispose for Safari and older browsers
|
|
14
16
|
if (typeof Symbol.dispose === "undefined") {
|
|
@@ -116,15 +118,6 @@ export interface HandleState {
|
|
|
116
118
|
segmentOrder: string[];
|
|
117
119
|
}
|
|
118
120
|
|
|
119
|
-
/**
|
|
120
|
-
* Token for tracking an active stream
|
|
121
|
-
* Call end() when the stream completes
|
|
122
|
-
*/
|
|
123
|
-
export interface StreamingToken {
|
|
124
|
-
/** End this streaming operation */
|
|
125
|
-
end(): void;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
121
|
/**
|
|
129
122
|
* Result from starting a navigation
|
|
130
123
|
* Implements Disposable for use with `using` keyword
|
|
@@ -165,8 +158,8 @@ export interface ActionHandle extends Disposable {
|
|
|
165
158
|
readonly settled: boolean;
|
|
166
159
|
/** Check if any concurrent actions were started */
|
|
167
160
|
hadConcurrentActions: boolean;
|
|
168
|
-
/** Get
|
|
169
|
-
|
|
161
|
+
/** Get raw set of segments revalidated by concurrent actions */
|
|
162
|
+
getRevalidatedSegments(): Set<string>;
|
|
170
163
|
/** Clear consolidation tracking */
|
|
171
164
|
clearConsolidation(): void;
|
|
172
165
|
}
|
|
@@ -189,6 +182,7 @@ export interface EventController {
|
|
|
189
182
|
// State access
|
|
190
183
|
getState(): DerivedNavigationState;
|
|
191
184
|
getActionState(actionId: string): TrackedActionState;
|
|
185
|
+
getLocation(): NavigationLocation;
|
|
192
186
|
|
|
193
187
|
// Location updates (for popstate where navigation doesn't go through startNavigation)
|
|
194
188
|
setLocation(location: NavigationLocation): void;
|
|
@@ -216,6 +210,8 @@ export interface EventController {
|
|
|
216
210
|
// Direct state access for advanced use
|
|
217
211
|
getCurrentNavigation(): NavigationEntry | null;
|
|
218
212
|
getInflightActions(): Map<string, ActionEntry>;
|
|
213
|
+
/** Whether any concurrent actions have occurred (shared across all handles) */
|
|
214
|
+
hadAnyConcurrentActions(): boolean;
|
|
219
215
|
}
|
|
220
216
|
|
|
221
217
|
// ============================================================================
|
|
@@ -394,8 +390,8 @@ export function createEventController(
|
|
|
394
390
|
state,
|
|
395
391
|
isStreaming,
|
|
396
392
|
location,
|
|
397
|
-
// pendingUrl only during fetching phase - once streaming starts (URL changed), not pending
|
|
398
|
-
// Background revalidations don't expose a pending URL
|
|
393
|
+
// pendingUrl only during fetching phase - once streaming starts (URL changed), not pending.
|
|
394
|
+
// Background revalidations (skipLoadingState) don't expose a pending URL.
|
|
399
395
|
pendingUrl:
|
|
400
396
|
currentNavigation?.phase === "fetching" &&
|
|
401
397
|
!currentNavigation.options?.skipLoadingState
|
|
@@ -488,6 +484,7 @@ export function createEventController(
|
|
|
488
484
|
|
|
489
485
|
startStreaming(): StreamingToken {
|
|
490
486
|
let ended = false;
|
|
487
|
+
entry.phase = "streaming";
|
|
491
488
|
activeStreamCount++;
|
|
492
489
|
notify();
|
|
493
490
|
|
|
@@ -675,24 +672,8 @@ export function createEventController(
|
|
|
675
672
|
// If streaming is in progress, tryFinalize() will be called when streaming ends
|
|
676
673
|
},
|
|
677
674
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
// We don't need to wait for streaming to complete since we're refetching anyway
|
|
681
|
-
// Count actions that are still fetching (waiting for server response)
|
|
682
|
-
const stillFetchingCount = [...inflightActions.values()].filter(
|
|
683
|
-
(a) => a.phase === "fetching",
|
|
684
|
-
).length;
|
|
685
|
-
|
|
686
|
-
if (stillFetchingCount > 0) {
|
|
687
|
-
return null; // Some actions still waiting for server response
|
|
688
|
-
}
|
|
689
|
-
if (!hadAnyConcurrentActions) {
|
|
690
|
-
return null; // No concurrent actions occurred
|
|
691
|
-
}
|
|
692
|
-
if (concurrentRevalidatedSegments.size === 0) {
|
|
693
|
-
return null; // No segments to consolidate
|
|
694
|
-
}
|
|
695
|
-
return Array.from(concurrentRevalidatedSegments);
|
|
675
|
+
getRevalidatedSegments(): Set<string> {
|
|
676
|
+
return concurrentRevalidatedSegments;
|
|
696
677
|
},
|
|
697
678
|
|
|
698
679
|
clearConsolidation() {
|
|
@@ -727,16 +708,26 @@ export function createEventController(
|
|
|
727
708
|
}
|
|
728
709
|
|
|
729
710
|
function abortAllActions() {
|
|
730
|
-
for (const entry of inflightActions
|
|
711
|
+
for (const [id, entry] of inflightActions) {
|
|
712
|
+
// Preserve settling entries — they have already been handled by
|
|
713
|
+
// fail()/complete() and will self-cleanup via the settlement timeout.
|
|
714
|
+
// Clearing them here would prevent debounced notifications from
|
|
715
|
+
// delivering the error/result state to subscribers.
|
|
716
|
+
if (entry.phase === "settling") continue;
|
|
731
717
|
entry.abort.abort();
|
|
718
|
+
inflightActions.delete(id);
|
|
732
719
|
}
|
|
733
|
-
inflightActions.clear();
|
|
734
720
|
hadAnyConcurrentActions = false;
|
|
735
721
|
concurrentRevalidatedSegments.clear();
|
|
736
722
|
notify();
|
|
737
|
-
// Notify all action listeners
|
|
738
|
-
|
|
739
|
-
|
|
723
|
+
// Notify all action listeners directly by subscription ID.
|
|
724
|
+
// actionListeners keys are subscription IDs (possibly short names like
|
|
725
|
+
// "addToCart"), not full entry actionIds. Passing them to notifyAction
|
|
726
|
+
// would fail the suffix matcher — instead, notify each subscriber with
|
|
727
|
+
// its own state.
|
|
728
|
+
for (const [subscriptionId, listeners] of actionListeners) {
|
|
729
|
+
const state = getActionState(subscriptionId);
|
|
730
|
+
listeners.forEach((listener) => listener(state));
|
|
740
731
|
}
|
|
741
732
|
}
|
|
742
733
|
|
|
@@ -744,18 +735,6 @@ export function createEventController(
|
|
|
744
735
|
// Handle Operations
|
|
745
736
|
// ========================================================================
|
|
746
737
|
|
|
747
|
-
/**
|
|
748
|
-
* Filter segment IDs to only include routes and layouts.
|
|
749
|
-
* Excludes parallels (contain .@) and loaders (contain D followed by digit).
|
|
750
|
-
*/
|
|
751
|
-
function filterSegmentOrder(matched: string[]): string[] {
|
|
752
|
-
return matched.filter((id) => {
|
|
753
|
-
if (id.includes(".@")) return false;
|
|
754
|
-
if (/D\d+\./.test(id)) return false;
|
|
755
|
-
return true;
|
|
756
|
-
});
|
|
757
|
-
}
|
|
758
|
-
|
|
759
738
|
function setHandleData(
|
|
760
739
|
data: HandleData,
|
|
761
740
|
matched?: string[],
|
|
@@ -859,6 +838,7 @@ export function createEventController(
|
|
|
859
838
|
// State
|
|
860
839
|
getState,
|
|
861
840
|
getActionState,
|
|
841
|
+
getLocation: () => location,
|
|
862
842
|
setLocation,
|
|
863
843
|
|
|
864
844
|
// Handles
|
|
@@ -877,6 +857,7 @@ export function createEventController(
|
|
|
877
857
|
// Direct access
|
|
878
858
|
getCurrentNavigation: () => currentNavigation,
|
|
879
859
|
getInflightActions: () => inflightActions,
|
|
860
|
+
hadAnyConcurrentActions: () => hadAnyConcurrentActions,
|
|
880
861
|
};
|
|
881
862
|
}
|
|
882
863
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isLocationStateEntry,
|
|
3
|
+
resolveLocationStateEntries,
|
|
4
|
+
} from "./react/location-state-shared.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check if state is from typed LocationStateEntry[] (has __rsc_ls_ keys)
|
|
8
|
+
*/
|
|
9
|
+
function isTypedLocationState(
|
|
10
|
+
state: unknown,
|
|
11
|
+
): state is Record<string, unknown> {
|
|
12
|
+
if (state === null || typeof state !== "object") return false;
|
|
13
|
+
return Object.keys(state).some((key) => key.startsWith("__rsc_ls_"));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolve navigation state - handles both LocationStateEntry[] and plain formats
|
|
18
|
+
*/
|
|
19
|
+
export function resolveNavigationState(state: unknown): unknown {
|
|
20
|
+
if (
|
|
21
|
+
Array.isArray(state) &&
|
|
22
|
+
state.length > 0 &&
|
|
23
|
+
isLocationStateEntry(state[0])
|
|
24
|
+
) {
|
|
25
|
+
return resolveLocationStateEntries(state);
|
|
26
|
+
}
|
|
27
|
+
return state;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build history state object from user state
|
|
32
|
+
* - Typed state: spread directly into history.state
|
|
33
|
+
* - Plain state: store in history.state.state
|
|
34
|
+
*/
|
|
35
|
+
export function buildHistoryState(
|
|
36
|
+
userState: unknown,
|
|
37
|
+
routerState?: { intercept?: boolean; sourceUrl?: string },
|
|
38
|
+
serverState?: Record<string, unknown>,
|
|
39
|
+
): Record<string, unknown> | null {
|
|
40
|
+
const result: Record<string, unknown> = {};
|
|
41
|
+
|
|
42
|
+
if (routerState?.intercept) {
|
|
43
|
+
result.intercept = true;
|
|
44
|
+
if (routerState.sourceUrl) {
|
|
45
|
+
result.sourceUrl = routerState.sourceUrl;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (userState !== undefined) {
|
|
50
|
+
if (isTypedLocationState(userState)) {
|
|
51
|
+
Object.assign(result, userState);
|
|
52
|
+
} else {
|
|
53
|
+
result.state = userState;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (serverState) {
|
|
58
|
+
Object.assign(result, serverState);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return Object.keys(result).length > 0 ? result : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Merge server-set location state into the current history entry.
|
|
66
|
+
* Replaces the current history state and dispatches notification event
|
|
67
|
+
* so useLocationState hooks re-read from history.state.
|
|
68
|
+
*/
|
|
69
|
+
export function mergeLocationState(
|
|
70
|
+
locationState: Record<string, unknown>,
|
|
71
|
+
): void {
|
|
72
|
+
const merged = {
|
|
73
|
+
...window.history.state,
|
|
74
|
+
...locationState,
|
|
75
|
+
};
|
|
76
|
+
window.history.replaceState(merged, "", window.location.href);
|
|
77
|
+
if (Object.keys(locationState).some((k) => k.startsWith("__rsc_ls_"))) {
|
|
78
|
+
window.dispatchEvent(new Event("__rsc_locationstate"));
|
|
79
|
+
}
|
|
80
|
+
}
|