@rangojs/router 0.0.0-experimental.fb4fdc18 → 0.0.0-experimental.fce7fbd1
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 +9 -9
- package/dist/bin/rango.js +147 -57
- package/dist/testing/vitest.js +48 -0
- package/dist/vite/index.js +914 -485
- package/package.json +55 -11
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +220 -30
- package/skills/caching/SKILL.md +116 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +3 -1
- package/skills/hooks/SKILL.md +214 -18
- package/skills/host-router/SKILL.md +45 -20
- package/skills/intercept/SKILL.md +26 -4
- package/skills/layout/SKILL.md +6 -7
- package/skills/links/SKILL.md +173 -17
- package/skills/loader/SKILL.md +149 -6
- package/skills/middleware/SKILL.md +13 -9
- package/skills/migrate-nextjs/SKILL.md +1 -1
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +5 -6
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +242 -26
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +58 -9
- package/skills/route/SKILL.md +13 -4
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +53 -41
- package/skills/testing/SKILL.md +599 -0
- package/skills/typesafety/SKILL.md +310 -26
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +117 -0
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/event-controller.ts +42 -66
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +6 -6
- package/src/browser/navigation-client.ts +12 -15
- package/src/browser/navigation-store.ts +7 -8
- package/src/browser/navigation-transaction.ts +10 -28
- package/src/browser/partial-update.ts +9 -19
- package/src/browser/react/NavigationProvider.tsx +29 -40
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/location-state-shared.ts +175 -4
- package/src/browser/react/location-state.ts +39 -13
- package/src/browser/react/use-handle.ts +17 -9
- package/src/browser/react/use-params.ts +3 -4
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +14 -1
- package/src/browser/response-adapter.ts +25 -0
- package/src/browser/rsc-router.tsx +30 -16
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +23 -30
- package/src/browser/types.ts +2 -0
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/index.ts +2 -0
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +55 -14
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-scope.ts +28 -42
- package/src/cache/cf/cf-cache-store.ts +49 -6
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +10 -8
- package/src/context-var.ts +5 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -1
- package/src/handle.ts +26 -13
- package/src/host/index.ts +2 -2
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +140 -20
- package/src/index.rsc.ts +6 -4
- package/src/index.ts +13 -6
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +2 -5
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/prerender.ts +4 -4
- package/src/response-utils.ts +9 -0
- package/src/reverse.ts +65 -41
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +238 -263
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +37 -14
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-types.ts +19 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +15 -2
- package/src/router/error-handling.ts +1 -1
- package/src/router/handler-context.ts +4 -42
- package/src/router/intercept-resolution.ts +4 -18
- package/src/router/lazy-includes.ts +2 -2
- package/src/router/loader-resolution.ts +16 -2
- package/src/router/match-handlers.ts +62 -20
- package/src/router/match-middleware/cache-lookup.ts +44 -91
- package/src/router/match-middleware/cache-store.ts +3 -2
- package/src/router/match-result.ts +32 -30
- package/src/router/metrics.ts +1 -1
- package/src/router/middleware-types.ts +1 -1
- package/src/router/middleware.ts +46 -78
- package/src/router/prerender-match.ts +1 -1
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +4 -28
- package/src/router/revalidation.ts +43 -1
- package/src/router/router-interfaces.ts +45 -28
- package/src/router/router-options.ts +40 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +19 -6
- package/src/router/segment-resolution/revalidation.ts +19 -6
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router/types.ts +8 -0
- package/src/router.ts +37 -21
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +20 -65
- package/src/rsc/helpers.ts +22 -2
- package/src/rsc/index.ts +1 -1
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/response-route-handler.ts +32 -52
- package/src/rsc/rsc-rendering.ts +27 -53
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +13 -37
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +2 -2
- package/src/search-params.ts +4 -4
- package/src/segment-system.tsx +121 -65
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +118 -51
- package/src/server/cookie-store.ts +28 -4
- package/src/server/request-context.ts +10 -0
- package/src/static-handler.ts +1 -1
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +440 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +154 -0
- package/src/testing/e2e/index.ts +149 -0
- package/src/testing/e2e/matchers.ts +51 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +306 -0
- package/src/testing/e2e/server.ts +183 -0
- package/src/testing/flight-matchers.ts +104 -0
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +22 -0
- package/src/testing/flight.ts +182 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +105 -0
- package/src/testing/internal/context.ts +193 -0
- package/src/testing/render-route.tsx +536 -0
- package/src/testing/run-loader.ts +296 -0
- package/src/testing/run-middleware.ts +170 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +183 -0
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +56 -11
- package/src/types/index.ts +1 -0
- package/src/types/segments.ts +18 -1
- package/src/urls/include-helper.ts +10 -53
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper-types.ts +11 -3
- package/src/urls/path-helper.ts +17 -52
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +20 -19
- package/src/urls/type-extraction.ts +26 -116
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +413 -42
- package/src/vite/debug.ts +1 -0
- package/src/vite/discovery/bundle-postprocess.ts +6 -6
- package/src/vite/discovery/discover-routers.ts +70 -48
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/prerender-collection.ts +19 -25
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/state.ts +33 -0
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +2 -0
- package/src/vite/plugin-types.ts +67 -0
- package/src/vite/plugins/cjs-to-esm.ts +3 -7
- package/src/vite/plugins/client-ref-hashing.ts +12 -1
- package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -1
- package/src/vite/plugins/expose-action-id.ts +2 -2
- package/src/vite/plugins/expose-id-utils.ts +12 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-internal-ids.ts +47 -67
- package/src/vite/plugins/performance-tracks.ts +12 -16
- package/src/vite/plugins/use-cache-transform.ts +13 -11
- package/src/vite/plugins/version-injector.ts +2 -12
- package/src/vite/plugins/version-plugin.ts +59 -2
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +67 -15
- package/src/vite/router-discovery.ts +208 -63
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/client-chunks.ts +190 -0
- package/src/vite/utils/forward-user-plugins.ts +193 -0
- package/src/vite/utils/manifest-utils.ts +21 -5
- package/src/vite/utils/shared-utils.ts +107 -26
- package/src/browser/action-response-classifier.ts +0 -99
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* No "use client" directive so it can be imported from RSC
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import type { ReactElement } from "react";
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* Internal entry representing a state value with its unique key.
|
|
8
10
|
* When __rsc_ls_lazy is true, __rsc_ls_value holds a getter function
|
|
@@ -22,6 +24,88 @@ export interface LocationStateOptions {
|
|
|
22
24
|
flash?: boolean;
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
type LocationStateUnsafeFn = (...args: never[]) => unknown;
|
|
28
|
+
|
|
29
|
+
// Broadest constructor signature (`abstract` covers both abstract and concrete
|
|
30
|
+
// classes). A class passed as state has a `new` signature, not a call signature,
|
|
31
|
+
// so it slips past LocationStateUnsafeFn; at runtime the lazy-getter path
|
|
32
|
+
// (`typeof value === "function"`) then mistakes it for a getter and throws.
|
|
33
|
+
type LocationStateUnsafeCtor = abstract new (...args: never[]) => unknown;
|
|
34
|
+
|
|
35
|
+
// `unknown` cannot be verified serializable, so it is rejected (callers must
|
|
36
|
+
// supply a concrete type). `any` deliberately defeats type checking and is NOT
|
|
37
|
+
// guardable — it is assignable to the branded error too, so the check always
|
|
38
|
+
// passes; it remains an explicit escape hatch.
|
|
39
|
+
type IsAny<T> = 0 extends 1 & T ? true : false;
|
|
40
|
+
type IsUnknown<T> =
|
|
41
|
+
IsAny<T> extends true ? false : unknown extends T ? true : false;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Branded error surfaced when a value that cannot live in location state is
|
|
45
|
+
* used. Location state is written into `history.state`, which uses the
|
|
46
|
+
* structured clone algorithm; React elements, functions, and symbols throw a
|
|
47
|
+
* `DataCloneError` at runtime. Carries a human-readable reason so the compile
|
|
48
|
+
* error explains the fix.
|
|
49
|
+
*/
|
|
50
|
+
export type LocationStateUnsafe<Reason extends string> = {
|
|
51
|
+
readonly __rango_location_state_unsafe: Reason;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Maps `T` to itself when it is safe to store in location state, or to a branded
|
|
56
|
+
* {@link LocationStateUnsafe} error for the disallowed parts: `unknown`, React
|
|
57
|
+
* elements (RSC/JSX content), functions, class constructors, and symbols.
|
|
58
|
+
* Recurses through arrays, `Map`, `Set`, and plain objects; structured-clone
|
|
59
|
+
* built-ins (`Date`, `RegExp`, typed arrays, `Blob`, `File`, `FormData`) pass
|
|
60
|
+
* through. Consumed by {@link ValidateLocationState}, which is intersected into a
|
|
61
|
+
* definition's value parameter so posting RSC content is a COMPILE error, not a
|
|
62
|
+
* runtime `DataCloneError`. (`any` is unguardable and remains an escape hatch.)
|
|
63
|
+
*/
|
|
64
|
+
export type LocationStateSafe<T> =
|
|
65
|
+
IsUnknown<T> extends true
|
|
66
|
+
? LocationStateUnsafe<"location state needs an explicit, concrete type; `unknown` cannot be verified as serializable">
|
|
67
|
+
: T extends LocationStateUnsafeFn
|
|
68
|
+
? LocationStateUnsafe<"functions cannot be stored in location state">
|
|
69
|
+
: T extends LocationStateUnsafeCtor
|
|
70
|
+
? LocationStateUnsafe<"class constructors cannot be stored in location state">
|
|
71
|
+
: T extends symbol
|
|
72
|
+
? LocationStateUnsafe<"symbols cannot be stored in location state">
|
|
73
|
+
: T extends ReactElement
|
|
74
|
+
? LocationStateUnsafe<"React/RSC content cannot be stored in location state; store plain data and render it on arrival">
|
|
75
|
+
: T extends string | number | boolean | bigint | null | undefined
|
|
76
|
+
? T
|
|
77
|
+
: T extends
|
|
78
|
+
| Date
|
|
79
|
+
| RegExp
|
|
80
|
+
| ArrayBuffer
|
|
81
|
+
| ArrayBufferView
|
|
82
|
+
| Blob
|
|
83
|
+
| File
|
|
84
|
+
| FormData
|
|
85
|
+
? T
|
|
86
|
+
: T extends ReadonlyMap<infer K, infer V>
|
|
87
|
+
? ReadonlyMap<LocationStateSafe<K>, LocationStateSafe<V>>
|
|
88
|
+
: T extends ReadonlySet<infer V>
|
|
89
|
+
? ReadonlySet<LocationStateSafe<V>>
|
|
90
|
+
: T extends readonly unknown[]
|
|
91
|
+
? { [K in keyof T]: LocationStateSafe<T[K]> }
|
|
92
|
+
: T extends object
|
|
93
|
+
? { [K in keyof T]: LocationStateSafe<T[K]> }
|
|
94
|
+
: T;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* `unknown` (a no-op) when `T` is safe to store in location state, otherwise a
|
|
98
|
+
* branded {@link LocationStateUnsafe} object. Intersected into the value
|
|
99
|
+
* parameter of a definition's call and `write()` so POSTING RSC content (or any
|
|
100
|
+
* non-serializable value) is a compile error whose text carries the reason —
|
|
101
|
+
* without a `TState extends ...` self-constraint, which TypeScript rejects as
|
|
102
|
+
* circular (TS2313). For safe `T`, `value & unknown` collapses back to `value`,
|
|
103
|
+
* so valid usage is unchanged.
|
|
104
|
+
*/
|
|
105
|
+
export type ValidateLocationState<T> = [T] extends [LocationStateSafe<T>]
|
|
106
|
+
? unknown
|
|
107
|
+
: LocationStateUnsafe<"location state must be serializable: React/RSC content, functions, and symbols cannot be stored — pass plain data and render it on arrival">;
|
|
108
|
+
|
|
25
109
|
/**
|
|
26
110
|
* Type-safe location state definition
|
|
27
111
|
*
|
|
@@ -34,8 +118,43 @@ export interface LocationStateDefinition<TArgs extends unknown[], TState> {
|
|
|
34
118
|
__rsc_ls_key: string;
|
|
35
119
|
/** Whether this state auto-clears after first read */
|
|
36
120
|
readonly __rsc_ls_flash: boolean;
|
|
37
|
-
/**
|
|
121
|
+
/**
|
|
122
|
+
* Read the current value from history.state.
|
|
123
|
+
*
|
|
124
|
+
* Returns undefined during SSR (no `window`). To stay hydration-safe, do
|
|
125
|
+
* NOT call read() inline during the initial render — the server returns
|
|
126
|
+
* undefined while the client may have a value preserved in history.state
|
|
127
|
+
* (e.g. after a hard reload of an entry that earlier called write()),
|
|
128
|
+
* which causes a hydration mismatch. Call read() inside an event handler
|
|
129
|
+
* or a useEffect post-mount instead, or use useLocationState() if you
|
|
130
|
+
* want React to manage subscription/hydration for you.
|
|
131
|
+
*/
|
|
38
132
|
read(): TState | undefined;
|
|
133
|
+
/**
|
|
134
|
+
* Statically write the value into the current history entry under this
|
|
135
|
+
* definition's key, preserving any other keys already on history.state
|
|
136
|
+
* (e.g. router bookkeeping, other LocationState slots).
|
|
137
|
+
*
|
|
138
|
+
* This is the non-reactive counterpart to read(): it does not dispatch any
|
|
139
|
+
* event, so components reading via useLocationState() will NOT re-render
|
|
140
|
+
* until the next navigation/popstate. Use it when you only need the value
|
|
141
|
+
* to be there on the next read() or on the next mount (including after
|
|
142
|
+
* back/forward and hard refresh of the same entry).
|
|
143
|
+
*
|
|
144
|
+
* Client-only: throws when called on the server (no history available).
|
|
145
|
+
*/
|
|
146
|
+
write(value: TState & ValidateLocationState<TState>): void;
|
|
147
|
+
/**
|
|
148
|
+
* Statically remove this definition's slot from the current history entry,
|
|
149
|
+
* leaving any other keys on history.state untouched. Idempotent: removing
|
|
150
|
+
* a slot that isn't present is a no-op.
|
|
151
|
+
*
|
|
152
|
+
* Same non-reactive semantics as write(): no event is dispatched, so
|
|
153
|
+
* useLocationState() readers will NOT re-render until the next navigation.
|
|
154
|
+
*
|
|
155
|
+
* Client-only: throws when called on the server (no history available).
|
|
156
|
+
*/
|
|
157
|
+
delete(): void;
|
|
39
158
|
}
|
|
40
159
|
|
|
41
160
|
/**
|
|
@@ -70,18 +189,30 @@ export interface LocationStateDefinition<TArgs extends unknown[], TState> {
|
|
|
70
189
|
*
|
|
71
190
|
* // Read without hook (snapshot, client-side only)
|
|
72
191
|
* const snap = ProductState.read();
|
|
192
|
+
*
|
|
193
|
+
* // Static write to current history entry (non-reactive, client-side only).
|
|
194
|
+
* // Survives back/forward and hard refresh; useLocationState() readers will
|
|
195
|
+
* // NOT see the new value until the next navigation. Pair with .read() or a
|
|
196
|
+
* // fresh mount.
|
|
197
|
+
* ProductState.write({ name: "Widget", price: 9.99 });
|
|
198
|
+
*
|
|
199
|
+
* // Manually clear the slot (non-reactive, client-side only).
|
|
200
|
+
* ProductState.delete();
|
|
73
201
|
* ```
|
|
74
202
|
*/
|
|
75
203
|
export function createLocationState<TState>(
|
|
76
204
|
options?: LocationStateOptions,
|
|
77
|
-
): LocationStateDefinition<
|
|
205
|
+
): LocationStateDefinition<
|
|
206
|
+
[(TState | (() => TState)) & ValidateLocationState<TState>],
|
|
207
|
+
TState
|
|
208
|
+
> {
|
|
78
209
|
const flash = options?.flash ?? false;
|
|
79
210
|
let _key: string | undefined;
|
|
80
211
|
|
|
81
212
|
function getKey(): string {
|
|
82
213
|
if (!_key && process.env.NODE_ENV === "development") {
|
|
83
214
|
throw new Error(
|
|
84
|
-
"[
|
|
215
|
+
"[rango] createLocationState key not set. " +
|
|
85
216
|
"Make sure the exposeInternalIds Vite plugin is enabled and " +
|
|
86
217
|
"the state is exported with: export const MyState = createLocationState(...)",
|
|
87
218
|
);
|
|
@@ -128,7 +259,47 @@ export function createLocationState<TState>(
|
|
|
128
259
|
enumerable: true,
|
|
129
260
|
});
|
|
130
261
|
|
|
131
|
-
|
|
262
|
+
Object.defineProperty(fn, "write", {
|
|
263
|
+
value: (value: TState): void => {
|
|
264
|
+
if (typeof window === "undefined") {
|
|
265
|
+
throw new Error(
|
|
266
|
+
"[rango] LocationState.write() is client-only. " +
|
|
267
|
+
"It mutates window.history.state and cannot run on the server.",
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
const key = getKey();
|
|
271
|
+
const current = window.history.state ?? {};
|
|
272
|
+
window.history.replaceState(
|
|
273
|
+
{ ...current, [key]: value },
|
|
274
|
+
"",
|
|
275
|
+
window.location.href,
|
|
276
|
+
);
|
|
277
|
+
},
|
|
278
|
+
enumerable: true,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
Object.defineProperty(fn, "delete", {
|
|
282
|
+
value: (): void => {
|
|
283
|
+
if (typeof window === "undefined") {
|
|
284
|
+
throw new Error(
|
|
285
|
+
"[rango] LocationState.delete() is client-only. " +
|
|
286
|
+
"It mutates window.history.state and cannot run on the server.",
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
const key = getKey();
|
|
290
|
+
const current = window.history.state;
|
|
291
|
+
if (current == null || !(key in current)) return;
|
|
292
|
+
const next = { ...current };
|
|
293
|
+
delete next[key];
|
|
294
|
+
window.history.replaceState(next, "", window.location.href);
|
|
295
|
+
},
|
|
296
|
+
enumerable: true,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
return fn as unknown as LocationStateDefinition<
|
|
300
|
+
[(TState | (() => TState)) & ValidateLocationState<TState>],
|
|
301
|
+
TState
|
|
302
|
+
>;
|
|
132
303
|
}
|
|
133
304
|
|
|
134
305
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect } from "react";
|
|
3
|
+
import { useState, useEffect, useRef } from "react";
|
|
4
4
|
import type { LocationStateDefinition } from "./location-state-shared.js";
|
|
5
5
|
|
|
6
6
|
// Re-export shared utilities and types
|
|
@@ -13,6 +13,24 @@ export {
|
|
|
13
13
|
type LocationStateOptions,
|
|
14
14
|
} from "./location-state-shared.js";
|
|
15
15
|
|
|
16
|
+
function readLocationStateValue<TState>(
|
|
17
|
+
key: string | undefined,
|
|
18
|
+
): TState | undefined {
|
|
19
|
+
if (typeof window === "undefined") return undefined;
|
|
20
|
+
if (key) {
|
|
21
|
+
return window.history.state?.[key] as TState | undefined;
|
|
22
|
+
}
|
|
23
|
+
// Plain state: stored under history.state.state
|
|
24
|
+
return window.history.state?.state as TState | undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function hasHydrated(): boolean {
|
|
28
|
+
return (
|
|
29
|
+
typeof document !== "undefined" &&
|
|
30
|
+
document.documentElement.hasAttribute("data-hydrated")
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
16
34
|
/**
|
|
17
35
|
* Hook to read location state from history.state
|
|
18
36
|
*
|
|
@@ -48,30 +66,33 @@ export function useLocationState<TArgs extends unknown[], TState>(
|
|
|
48
66
|
const key = definition?.__rsc_ls_key;
|
|
49
67
|
const isFlash = definition?.__rsc_ls_flash ?? false;
|
|
50
68
|
|
|
69
|
+
// Track whether the initial render returned undefined because the page
|
|
70
|
+
// hadn't hydrated yet. If so, the mount effect catches up by reading
|
|
71
|
+
// history.state once. If not, we already have the right value and must
|
|
72
|
+
// not re-read on mount — under StrictMode, the flash-cleanup effect runs
|
|
73
|
+
// before the second setup pass, so a re-read would clobber the captured
|
|
74
|
+
// value with the now-cleared `undefined`.
|
|
75
|
+
const initialReadDeferredRef = useRef(false);
|
|
76
|
+
|
|
51
77
|
const [state, setState] = useState<TState | undefined>(() => {
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
return
|
|
78
|
+
if (!hasHydrated()) {
|
|
79
|
+
initialReadDeferredRef.current = true;
|
|
80
|
+
return undefined;
|
|
55
81
|
}
|
|
56
|
-
|
|
57
|
-
return window.history.state?.state as TState | undefined;
|
|
82
|
+
return readLocationStateValue<TState>(key);
|
|
58
83
|
});
|
|
59
84
|
|
|
60
85
|
// Subscribe to popstate and programmatic state changes
|
|
61
86
|
useEffect(() => {
|
|
62
87
|
const handlePopstate = () => {
|
|
63
|
-
|
|
64
|
-
setState(window.history.state?.[key] as TState | undefined);
|
|
65
|
-
} else {
|
|
66
|
-
setState(window.history.state?.state as TState | undefined);
|
|
67
|
-
}
|
|
88
|
+
setState(readLocationStateValue<TState>(key));
|
|
68
89
|
};
|
|
69
90
|
|
|
70
91
|
// Handle programmatic state changes (same-page navigation with
|
|
71
92
|
// ctx.setLocationState where components don't remount)
|
|
72
93
|
const handleLocationState = () => {
|
|
73
94
|
if (key) {
|
|
74
|
-
const val =
|
|
95
|
+
const val = readLocationStateValue<TState>(key);
|
|
75
96
|
if (isFlash) {
|
|
76
97
|
// For flash state, only update if there's a new value
|
|
77
98
|
if (val !== undefined) {
|
|
@@ -81,10 +102,15 @@ export function useLocationState<TArgs extends unknown[], TState>(
|
|
|
81
102
|
setState(val);
|
|
82
103
|
}
|
|
83
104
|
} else {
|
|
84
|
-
setState(
|
|
105
|
+
setState(readLocationStateValue<TState>(key));
|
|
85
106
|
}
|
|
86
107
|
};
|
|
87
108
|
|
|
109
|
+
if (initialReadDeferredRef.current) {
|
|
110
|
+
initialReadDeferredRef.current = false;
|
|
111
|
+
setState(readLocationStateValue<TState>(key));
|
|
112
|
+
}
|
|
113
|
+
|
|
88
114
|
window.addEventListener("popstate", handlePopstate);
|
|
89
115
|
window.addEventListener("__rsc_locationstate", handleLocationState);
|
|
90
116
|
return () => {
|
|
@@ -32,27 +32,35 @@ import { shallowEqual } from "./shallow-equal.js";
|
|
|
32
32
|
* const lastCrumb = useHandle(Breadcrumbs, (data) => data.at(-1));
|
|
33
33
|
* ```
|
|
34
34
|
*/
|
|
35
|
-
export function useHandle<T, A>(handle: Handle<T, A>): A
|
|
35
|
+
export function useHandle<T, A>(handle: Handle<T, A>): Rango.FlightSerialize<A>;
|
|
36
36
|
export function useHandle<T, A, S>(
|
|
37
37
|
handle: Handle<T, A>,
|
|
38
|
-
selector: (data: A) => S,
|
|
38
|
+
selector: (data: Rango.FlightSerialize<A>) => S,
|
|
39
39
|
): S;
|
|
40
40
|
export function useHandle<T, A, S>(
|
|
41
41
|
handle: Handle<T, A>,
|
|
42
|
-
selector?: (data: A) => S,
|
|
43
|
-
): A | S {
|
|
42
|
+
selector?: (data: Rango.FlightSerialize<A>) => S,
|
|
43
|
+
): Rango.FlightSerialize<A> | S {
|
|
44
44
|
const ctx = useContext(NavigationStoreContext);
|
|
45
45
|
|
|
46
46
|
// Initial state from context event controller, or empty fallback without provider.
|
|
47
|
-
const [value, setValue] = useState<A | S>(() => {
|
|
47
|
+
const [value, setValue] = useState<Rango.FlightSerialize<A> | S>(() => {
|
|
48
48
|
if (!ctx) {
|
|
49
|
-
const collected = collectHandleData(
|
|
49
|
+
const collected = collectHandleData(
|
|
50
|
+
handle,
|
|
51
|
+
{},
|
|
52
|
+
[],
|
|
53
|
+
) as Rango.FlightSerialize<A>;
|
|
50
54
|
return selector ? selector(collected) : collected;
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
// On client, use event controller state
|
|
54
58
|
const state = ctx.eventController.getHandleState();
|
|
55
|
-
const collected = collectHandleData(
|
|
59
|
+
const collected = collectHandleData(
|
|
60
|
+
handle,
|
|
61
|
+
state.data,
|
|
62
|
+
state.segmentOrder,
|
|
63
|
+
) as Rango.FlightSerialize<A>;
|
|
56
64
|
return selector ? selector(collected) : collected;
|
|
57
65
|
});
|
|
58
66
|
const [optimisticValue, setOptimisticValue] = useOptimistic(value);
|
|
@@ -76,7 +84,7 @@ export function useHandle<T, A, S>(
|
|
|
76
84
|
handle,
|
|
77
85
|
currentHandleState.data,
|
|
78
86
|
currentHandleState.segmentOrder,
|
|
79
|
-
)
|
|
87
|
+
) as Rango.FlightSerialize<A>;
|
|
80
88
|
const currentValue = selectorRef.current
|
|
81
89
|
? selectorRef.current(currentCollected)
|
|
82
90
|
: currentCollected;
|
|
@@ -93,7 +101,7 @@ export function useHandle<T, A, S>(
|
|
|
93
101
|
handle,
|
|
94
102
|
state.data,
|
|
95
103
|
state.segmentOrder,
|
|
96
|
-
)
|
|
104
|
+
) as Rango.FlightSerialize<A>;
|
|
97
105
|
const nextValue = selectorRef.current
|
|
98
106
|
? selectorRef.current(collected)
|
|
99
107
|
: collected;
|
|
@@ -4,6 +4,8 @@ import { useContext, useState, useEffect, useRef } from "react";
|
|
|
4
4
|
import { NavigationStoreContext } from "./context.js";
|
|
5
5
|
import { shallowEqual } from "./shallow-equal.js";
|
|
6
6
|
|
|
7
|
+
const EMPTY_PARAMS: Record<string, string> = Object.freeze({});
|
|
8
|
+
|
|
7
9
|
/**
|
|
8
10
|
* Hook to access the current route params.
|
|
9
11
|
*
|
|
@@ -43,10 +45,7 @@ export function useParams<T>(
|
|
|
43
45
|
const ctx = useContext(NavigationStoreContext);
|
|
44
46
|
|
|
45
47
|
const [value, setValue] = useState<T | Record<string, string>>(() => {
|
|
46
|
-
|
|
47
|
-
return selector ? selector({}) : {};
|
|
48
|
-
}
|
|
49
|
-
const params = ctx.eventController.getParams();
|
|
48
|
+
const params = ctx ? ctx.eventController.getParams() : EMPTY_PARAMS;
|
|
50
49
|
return selector ? selector(params) : params;
|
|
51
50
|
});
|
|
52
51
|
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback } from "react";
|
|
4
|
+
import type { LocalReverseFunction } from "../../reverse.js";
|
|
5
|
+
import { substitutePatternParams } from "../../router/substitute-pattern-params.js";
|
|
6
|
+
import { serializeSearchParams } from "../../search-params.js";
|
|
7
|
+
import { useMount } from "./use-mount.js";
|
|
8
|
+
import { useParams } from "./use-params.js";
|
|
9
|
+
|
|
10
|
+
type RouteEntry = string | { readonly path: string };
|
|
11
|
+
type LocalRouteMap = Readonly<Record<string, RouteEntry>>;
|
|
12
|
+
|
|
13
|
+
function getPattern(entry: RouteEntry | undefined): string | undefined {
|
|
14
|
+
if (entry === undefined) return undefined;
|
|
15
|
+
return typeof entry === "string" ? entry : entry.path;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Join an include mount prefix with a mount-relative pattern.
|
|
20
|
+
*
|
|
21
|
+
* `pattern === "/"` is the index of the local module — under a non-root
|
|
22
|
+
* mount it must collapse so `/` under `/blog` becomes `/blog`, not
|
|
23
|
+
* `/blog/`. This matches `ctx.reverse(".index")` on the server.
|
|
24
|
+
*/
|
|
25
|
+
function joinMount(mount: string, pattern: string): string {
|
|
26
|
+
if (pattern === "/") {
|
|
27
|
+
if (mount === "" || mount === "/") return "/";
|
|
28
|
+
return mount.endsWith("/") ? mount.slice(0, -1) : mount;
|
|
29
|
+
}
|
|
30
|
+
const normalizedMount = mount === "/" ? "" : mount.replace(/\/+$/, "");
|
|
31
|
+
return normalizedMount + pattern;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Mount-aware reverse function for a locally-imported `routes` map.
|
|
36
|
+
*
|
|
37
|
+
* The `routes` map you pass IS the scope: `reverse("name")` looks the name up
|
|
38
|
+
* in that map (verbatim), prefixes the result with the surrounding `include()`
|
|
39
|
+
* mount path via `useMount()`, and substitutes params — auto-filling from the
|
|
40
|
+
* current matched route's params, with explicit params overriding. A module's
|
|
41
|
+
* components can therefore reverse their own routes without knowing where the
|
|
42
|
+
* module is mounted: include it under any prefix and the URLs resolve correctly.
|
|
43
|
+
*
|
|
44
|
+
* The leading dot is optional and cosmetic: `reverse("post")` and
|
|
45
|
+
* `reverse(".post")` resolve identically. The dot exists only as a readability
|
|
46
|
+
* convention and for parity with `ctx.reverse(".name")` on the server; here the
|
|
47
|
+
* passed map is the scope, so there is no separate global namespace to
|
|
48
|
+
* disambiguate and the dot carries no meaning.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```tsx
|
|
52
|
+
* "use client";
|
|
53
|
+
* import { Link, useReverse } from "@rangojs/router/client";
|
|
54
|
+
* import { routes as blogRoutes } from "../urls/blog.gen.js";
|
|
55
|
+
*
|
|
56
|
+
* function BlogNav() {
|
|
57
|
+
* const reverse = useReverse(blogRoutes);
|
|
58
|
+
* return (
|
|
59
|
+
* <>
|
|
60
|
+
* <Link to={reverse("index")}>Blog</Link>
|
|
61
|
+
* <Link to={reverse("post", { postId: "hello" })}>Post</Link>
|
|
62
|
+
* </>
|
|
63
|
+
* );
|
|
64
|
+
* }
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export function useReverse<const TRoutes extends LocalRouteMap>(
|
|
68
|
+
routes: TRoutes,
|
|
69
|
+
): LocalReverseFunction<TRoutes> {
|
|
70
|
+
const mount = useMount();
|
|
71
|
+
const currentParams = useParams();
|
|
72
|
+
|
|
73
|
+
return useCallback(
|
|
74
|
+
((
|
|
75
|
+
name: string,
|
|
76
|
+
explicitParams?: Record<string, string | undefined>,
|
|
77
|
+
search?: Record<string, unknown>,
|
|
78
|
+
): string => {
|
|
79
|
+
// The leading dot is optional. The passed map IS the scope, so a dot to
|
|
80
|
+
// signal "local" is unnecessary — "detail" and ".detail" resolve the same.
|
|
81
|
+
// A dot is accepted (and stripped) for readability / ctx.reverse parity.
|
|
82
|
+
const lookupName = name.startsWith(".") ? name.slice(1) : name;
|
|
83
|
+
const entry = (routes as LocalRouteMap)[lookupName];
|
|
84
|
+
const pattern = getPattern(entry);
|
|
85
|
+
if (pattern === undefined) {
|
|
86
|
+
throw new Error(`Unknown route: "${name}"`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const joined = joinMount(mount, pattern);
|
|
90
|
+
|
|
91
|
+
const mergedParams = explicitParams
|
|
92
|
+
? { ...currentParams, ...explicitParams }
|
|
93
|
+
: currentParams;
|
|
94
|
+
|
|
95
|
+
const substituted = substitutePatternParams(joined, mergedParams, name);
|
|
96
|
+
|
|
97
|
+
if (search) {
|
|
98
|
+
const qs = serializeSearchParams(search);
|
|
99
|
+
if (qs) return `${substituted}?${qs}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return substituted;
|
|
103
|
+
}) as LocalReverseFunction<TRoutes>,
|
|
104
|
+
[routes, mount, currentParams],
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -72,7 +72,20 @@ export function useRouter(): RouterInstance {
|
|
|
72
72
|
},
|
|
73
73
|
|
|
74
74
|
back(): void {
|
|
75
|
-
|
|
75
|
+
// Avoid escaping the host on the first entry of this session.
|
|
76
|
+
// Prefer the Navigation API; fall back to the router-stamped
|
|
77
|
+
// history.state.idx (set by pushHistoryWithIdx) for older browsers.
|
|
78
|
+
const nav = (window as { navigation?: { canGoBack: boolean } })
|
|
79
|
+
.navigation;
|
|
80
|
+
const canGoBack =
|
|
81
|
+
nav && typeof nav.canGoBack === "boolean"
|
|
82
|
+
? nav.canGoBack
|
|
83
|
+
: ((window.history.state as { idx?: number } | null)?.idx ?? 0) > 0;
|
|
84
|
+
if (canGoBack) {
|
|
85
|
+
window.history.back();
|
|
86
|
+
} else {
|
|
87
|
+
ctx.navigate(withBasename("/"), { replace: true });
|
|
88
|
+
}
|
|
76
89
|
},
|
|
77
90
|
|
|
78
91
|
forward(): void {
|
|
@@ -24,6 +24,31 @@ export function emptyResponse(): Response {
|
|
|
24
24
|
return new Response(null, { status: 200 });
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Handle the X-RSC-Reload control header (server requests a full page reload on
|
|
29
|
+
* a version mismatch). Returns a short-circuit response when the header is
|
|
30
|
+
* present -- emptyResponse() if the URL was blocked by origin validation, or a
|
|
31
|
+
* never-resolving promise while the page reloads -- and null when absent, so
|
|
32
|
+
* the caller continues processing (e.g. the X-RSC-Redirect check). Scoped to
|
|
33
|
+
* X-RSC-Reload only; redirect handling differs between callers.
|
|
34
|
+
*/
|
|
35
|
+
export function handleReloadHeader(
|
|
36
|
+
response: Response,
|
|
37
|
+
opts: { onBlocked: () => void; onReload: (url: string) => void },
|
|
38
|
+
): Response | Promise<Response> | null {
|
|
39
|
+
const reload = extractRscHeaderUrl(response, "X-RSC-Reload");
|
|
40
|
+
if (reload === "blocked") {
|
|
41
|
+
opts.onBlocked();
|
|
42
|
+
return emptyResponse();
|
|
43
|
+
}
|
|
44
|
+
if (reload) {
|
|
45
|
+
opts.onReload(reload.url);
|
|
46
|
+
window.location.href = reload.url;
|
|
47
|
+
return new Promise<Response>(() => {});
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
27
52
|
/**
|
|
28
53
|
* Tee a response body for RSC parsing and stream completion tracking.
|
|
29
54
|
* Returns a new Response with one branch; the other is consumed to detect
|