@rangojs/router 0.0.0-experimental.10 → 0.0.0-experimental.100
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/AGENTS.md +9 -0
- package/README.md +1037 -4
- package/dist/bin/rango.js +1619 -157
- package/dist/vite/index.js +5762 -2301
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +71 -63
- package/skills/breadcrumbs/SKILL.md +252 -0
- package/skills/cache-guide/SKILL.md +294 -0
- package/skills/caching/SKILL.md +93 -23
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +12 -8
- package/skills/document-cache/SKILL.md +18 -16
- package/skills/fonts/SKILL.md +6 -4
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +367 -71
- package/skills/host-router/SKILL.md +218 -0
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +176 -8
- package/skills/layout/SKILL.md +124 -3
- package/skills/links/SKILL.md +304 -25
- package/skills/loader/SKILL.md +474 -47
- package/skills/middleware/SKILL.md +207 -37
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +15 -11
- package/skills/parallel/SKILL.md +272 -1
- package/skills/prerender/SKILL.md +467 -65
- package/skills/rango/SKILL.md +89 -21
- package/skills/response-routes/SKILL.md +152 -91
- package/skills/route/SKILL.md +305 -14
- package/skills/router-setup/SKILL.md +210 -32
- package/skills/server-actions/SKILL.md +739 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/theme/SKILL.md +9 -8
- package/skills/typesafety/SKILL.md +333 -86
- package/skills/use-cache/SKILL.md +324 -0
- package/skills/view-transitions/SKILL.md +212 -0
- package/src/__internal.ts +102 -4
- package/src/bin/rango.ts +312 -15
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +136 -68
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +24 -4
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +374 -561
- package/src/browser/navigation-client.ts +228 -70
- package/src/browser/navigation-store.ts +97 -55
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +376 -315
- package/src/browser/prefetch/cache.ts +314 -0
- package/src/browser/prefetch/fetch.ts +282 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +191 -0
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +152 -0
- package/src/browser/react/Link.tsx +255 -71
- package/src/browser/react/NavigationProvider.tsx +152 -24
- package/src/browser/react/context.ts +11 -0
- package/src/browser/react/filter-segment-order.ts +55 -0
- package/src/browser/react/index.ts +15 -12
- package/src/browser/react/location-state-shared.ts +95 -53
- package/src/browser/react/location-state.ts +60 -15
- package/src/browser/react/mount-context.ts +6 -1
- 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 +29 -51
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +30 -120
- package/src/browser/react/use-link-status.ts +6 -5
- package/src/browser/react/use-navigation.ts +44 -65
- package/src/browser/react/use-params.ts +78 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-reverse.ts +99 -0
- package/src/browser/react/use-router.ts +83 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +85 -99
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +246 -64
- package/src/browser/scroll-restoration.ts +127 -52
- package/src/browser/segment-reconciler.ts +243 -0
- package/src/browser/segment-structure-assert.ts +16 -0
- package/src/browser/server-action-bridge.ts +510 -603
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +158 -48
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +84 -23
- package/src/build/generate-route-types.ts +39 -828
- package/src/build/index.ts +4 -5
- package/src/build/route-trie.ts +85 -32
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +98 -0
- package/src/build/route-types/codegen.ts +102 -0
- package/src/build/route-types/include-resolution.ts +418 -0
- package/src/build/route-types/param-extraction.ts +48 -0
- package/src/build/route-types/per-module-writer.ts +128 -0
- package/src/build/route-types/router-processing.ts +618 -0
- package/src/build/route-types/scan-filter.ts +85 -0
- package/src/build/runtime-discovery.ts +231 -0
- 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 +342 -0
- package/src/cache/cache-scope.ts +167 -307
- package/src/cache/cf/cf-cache-store.ts +573 -21
- package/src/cache/cf/index.ts +13 -3
- package/src/cache/document-cache.ts +116 -77
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +1 -15
- package/src/cache/memory-segment-store.ts +191 -13
- package/src/cache/profile-registry.ts +73 -0
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +256 -0
- package/src/cache/taint.ts +153 -0
- package/src/cache/types.ts +72 -122
- package/src/client.rsc.tsx +6 -1
- package/src/client.tsx +118 -302
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +5 -1
- package/src/context-var.ts +156 -0
- package/src/debug.ts +19 -9
- package/src/errors.ts +77 -7
- package/src/handle.ts +55 -10
- package/src/handles/MetaTags.tsx +73 -20
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/handles/meta.ts +30 -13
- package/src/host/cookie-handler.ts +21 -15
- package/src/host/errors.ts +8 -8
- package/src/host/index.ts +4 -7
- package/src/host/pattern-matcher.ts +27 -27
- package/src/host/router.ts +61 -39
- package/src/host/testing.ts +8 -8
- package/src/host/types.ts +15 -7
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +65 -45
- package/src/index.rsc.ts +138 -21
- package/src/index.ts +206 -51
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +25 -143
- package/src/loader.ts +27 -10
- package/src/network-error-thrower.tsx +3 -1
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +4 -2
- package/src/prerender/store.ts +159 -13
- package/src/prerender.ts +397 -29
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +231 -121
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +7 -4
- package/src/route-definition/dsl-helpers.ts +1134 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +483 -0
- package/src/route-definition/index.ts +55 -0
- package/src/route-definition/redirect.ts +101 -0
- package/src/route-definition/resolve-handler-use.ts +155 -0
- package/src/route-definition.ts +1 -1431
- package/src/route-map-builder.ts +162 -123
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +66 -9
- package/src/router/content-negotiation.ts +215 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +9 -9
- package/src/router/find-match.ts +160 -0
- package/src/router/handler-context.ts +418 -86
- package/src/router/intercept-resolution.ts +35 -20
- package/src/router/lazy-includes.ts +237 -0
- package/src/router/loader-resolution.ts +359 -128
- package/src/router/logging.ts +251 -0
- package/src/router/manifest.ts +98 -32
- package/src/router/match-api.ts +196 -261
- package/src/router/match-context.ts +4 -2
- package/src/router/match-handlers.ts +441 -0
- package/src/router/match-middleware/background-revalidation.ts +108 -93
- package/src/router/match-middleware/cache-lookup.ts +415 -86
- package/src/router/match-middleware/cache-store.ts +91 -29
- package/src/router/match-middleware/intercept-resolution.ts +48 -21
- package/src/router/match-middleware/segment-resolution.ts +73 -9
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +154 -35
- package/src/router/metrics.ts +240 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +209 -0
- package/src/router/middleware.ts +373 -371
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +292 -52
- package/src/router/prerender-match.ts +502 -0
- package/src/router/preview-match.ts +98 -0
- package/src/router/request-classification.ts +310 -0
- package/src/router/revalidation.ts +152 -39
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +41 -21
- package/src/router/router-interfaces.ts +484 -0
- package/src/router/router-options.ts +618 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +756 -0
- package/src/router/segment-resolution/helpers.ts +268 -0
- package/src/router/segment-resolution/loader-cache.ts +199 -0
- package/src/router/segment-resolution/revalidation.ts +1407 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -1315
- package/src/router/segment-wrappers.ts +291 -0
- package/src/router/substitute-pattern-params.ts +56 -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 +111 -39
- package/src/router/types.ts +17 -9
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +642 -2011
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +864 -1114
- package/src/rsc/helpers.ts +181 -19
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +229 -0
- package/src/rsc/manifest-init.ts +90 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +395 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +360 -0
- package/src/rsc/rsc-rendering.ts +256 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +360 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +52 -11
- package/src/search-params.ts +230 -0
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +187 -38
- package/src/server/context.ts +333 -59
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +113 -15
- package/src/server/loader-registry.ts +24 -64
- package/src/server/request-context.ts +603 -109
- package/src/server.ts +35 -155
- package/src/ssr/index.tsx +107 -30
- package/src/static-handler.ts +126 -0
- package/src/theme/ThemeProvider.tsx +21 -15
- package/src/theme/ThemeScript.tsx +5 -5
- package/src/theme/constants.ts +5 -2
- package/src/theme/index.ts +4 -14
- package/src/theme/theme-context.ts +4 -30
- package/src/theme/theme-script.ts +21 -18
- package/src/types/boundaries.ts +158 -0
- package/src/types/cache-types.ts +198 -0
- package/src/types/error-types.ts +192 -0
- package/src/types/global-namespace.ts +100 -0
- package/src/types/handler-context.ts +764 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +209 -0
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +120 -0
- package/src/types/segments.ts +167 -0
- package/src/types.ts +1 -1757
- package/src/urls/include-helper.ts +207 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +372 -0
- package/src/urls/path-helper.ts +364 -0
- package/src/urls/pattern-types.ts +107 -0
- package/src/urls/response-types.ts +108 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -1282
- package/src/use-loader.tsx +161 -81
- package/src/vite/debug.ts +184 -0
- package/src/vite/discovery/bundle-postprocess.ts +181 -0
- package/src/vite/discovery/discover-routers.ts +376 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +486 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +73 -0
- package/src/vite/discovery/state.ts +117 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +15 -2063
- package/src/vite/plugin-types.ts +103 -0
- package/src/vite/plugins/cjs-to-esm.ts +98 -0
- package/src/vite/plugins/client-ref-dedup.ts +131 -0
- package/src/vite/plugins/client-ref-hashing.ts +117 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +107 -64
- package/src/vite/plugins/expose-id-utils.ts +299 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +127 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +816 -0
- package/src/vite/plugins/performance-tracks.ts +96 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/use-cache-transform.ts +336 -0
- package/src/vite/plugins/version-injector.ts +109 -0
- package/src/vite/plugins/version-plugin.ts +266 -0
- package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +497 -0
- package/src/vite/router-discovery.ts +1423 -0
- package/src/vite/utils/ast-handler-extract.ts +517 -0
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +137 -0
- package/src/vite/utils/manifest-utils.ts +70 -0
- package/src/vite/utils/package-resolution.ts +161 -0
- package/src/vite/utils/prerender-utils.ts +222 -0
- package/src/vite/utils/shared-utils.ts +170 -0
- package/CLAUDE.md +0 -43
- package/src/browser/lru-cache.ts +0 -69
- 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/router.gen.ts +0 -6
- package/src/urls.gen.ts +0 -8
- package/src/vite/expose-handle-id.ts +0 -209
- package/src/vite/expose-loader-id.ts +0 -426
- package/src/vite/expose-location-state-id.ts +0 -177
- package/src/vite/expose-prerender-handler-id.ts +0 -429
- package/src/vite/package-resolution.ts +0 -125
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
|
@@ -10,53 +10,98 @@ export {
|
|
|
10
10
|
resolveLocationStateEntries,
|
|
11
11
|
type LocationStateEntry,
|
|
12
12
|
type LocationStateDefinition,
|
|
13
|
+
type LocationStateOptions,
|
|
13
14
|
} from "./location-state-shared.js";
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Hook to read location state from history.state
|
|
17
18
|
*
|
|
19
|
+
* Behavior depends on the definition:
|
|
20
|
+
* - Normal state: persists across navigations, reactive to popstate
|
|
21
|
+
* - Flash state (created with { flash: true }): read once, cleared after paint
|
|
22
|
+
*
|
|
18
23
|
* Overloaded:
|
|
19
24
|
* - With definition: Returns typed state from the specific key
|
|
20
|
-
* - With type param only: Returns
|
|
25
|
+
* - With type param only: Returns plain state from history.state.state
|
|
21
26
|
*
|
|
22
27
|
* @example
|
|
23
28
|
* ```typescript
|
|
24
|
-
* //
|
|
25
|
-
* const ProductState = createLocationState<{ name: string }>(
|
|
29
|
+
* // Persistent state
|
|
30
|
+
* const ProductState = createLocationState<{ name: string }>();
|
|
26
31
|
* const state = useLocationState(ProductState);
|
|
27
|
-
* // state: { name: string } | undefined
|
|
28
32
|
*
|
|
29
|
-
* //
|
|
30
|
-
* const
|
|
33
|
+
* // Flash state (auto-clears after paint)
|
|
34
|
+
* const FlashMsg = createLocationState<{ text: string }>({ flash: true });
|
|
35
|
+
* const flash = useLocationState(FlashMsg);
|
|
36
|
+
*
|
|
37
|
+
* // Plain state access (reads from history.state.state)
|
|
38
|
+
* const state = useLocationState<{ from?: string }>();
|
|
31
39
|
* ```
|
|
32
40
|
*/
|
|
33
41
|
export function useLocationState<TArgs extends unknown[], TState>(
|
|
34
|
-
definition: LocationStateDefinition<TArgs, TState
|
|
42
|
+
definition: LocationStateDefinition<TArgs, TState>,
|
|
35
43
|
): TState | undefined;
|
|
36
44
|
export function useLocationState<T = unknown>(): T | undefined;
|
|
37
45
|
export function useLocationState<TArgs extends unknown[], TState>(
|
|
38
|
-
definition?: LocationStateDefinition<TArgs, TState
|
|
46
|
+
definition?: LocationStateDefinition<TArgs, TState>,
|
|
39
47
|
): TState | undefined {
|
|
48
|
+
const key = definition?.__rsc_ls_key;
|
|
49
|
+
const isFlash = definition?.__rsc_ls_flash ?? false;
|
|
50
|
+
|
|
40
51
|
const [state, setState] = useState<TState | undefined>(() => {
|
|
41
52
|
if (typeof window === "undefined") return undefined;
|
|
42
|
-
if (
|
|
43
|
-
return window.history.state?.[
|
|
53
|
+
if (key) {
|
|
54
|
+
return window.history.state?.[key] as TState | undefined;
|
|
44
55
|
}
|
|
45
|
-
//
|
|
56
|
+
// Plain state: stored under history.state.state
|
|
46
57
|
return window.history.state?.state as TState | undefined;
|
|
47
58
|
});
|
|
48
59
|
|
|
60
|
+
// Subscribe to popstate and programmatic state changes
|
|
49
61
|
useEffect(() => {
|
|
50
62
|
const handlePopstate = () => {
|
|
51
|
-
if (
|
|
52
|
-
setState(window.history.state?.[
|
|
63
|
+
if (key) {
|
|
64
|
+
setState(window.history.state?.[key] as TState | undefined);
|
|
53
65
|
} else {
|
|
54
66
|
setState(window.history.state?.state as TState | undefined);
|
|
55
67
|
}
|
|
56
68
|
};
|
|
69
|
+
|
|
70
|
+
// Handle programmatic state changes (same-page navigation with
|
|
71
|
+
// ctx.setLocationState where components don't remount)
|
|
72
|
+
const handleLocationState = () => {
|
|
73
|
+
if (key) {
|
|
74
|
+
const val = window.history.state?.[key] as TState | undefined;
|
|
75
|
+
if (isFlash) {
|
|
76
|
+
// For flash state, only update if there's a new value
|
|
77
|
+
if (val !== undefined) {
|
|
78
|
+
setState(val);
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
setState(val);
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
setState(window.history.state?.state as TState | undefined);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
57
88
|
window.addEventListener("popstate", handlePopstate);
|
|
58
|
-
|
|
59
|
-
|
|
89
|
+
window.addEventListener("__rsc_locationstate", handleLocationState);
|
|
90
|
+
return () => {
|
|
91
|
+
window.removeEventListener("popstate", handlePopstate);
|
|
92
|
+
window.removeEventListener("__rsc_locationstate", handleLocationState);
|
|
93
|
+
};
|
|
94
|
+
}, [key, isFlash]);
|
|
95
|
+
|
|
96
|
+
// Flash: clear from history.state after paint so subsequent navigations don't see it.
|
|
97
|
+
// Depends on `state` so it re-runs when state is set via the event listener.
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (isFlash && key && state !== undefined) {
|
|
100
|
+
const cleaned = { ...window.history.state };
|
|
101
|
+
delete cleaned[key];
|
|
102
|
+
window.history.replaceState(cleaned, "", window.location.href);
|
|
103
|
+
}
|
|
104
|
+
}, [isFlash, key, state]);
|
|
60
105
|
|
|
61
106
|
return state;
|
|
62
107
|
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
createElement,
|
|
6
|
+
type Context,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
} from "react";
|
|
4
9
|
|
|
5
10
|
/**
|
|
6
11
|
* Context for the current include() mount path.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Context for CSP nonce propagation to client components during SSR.
|
|
5
|
+
*
|
|
6
|
+
* The SSR renderer wraps the tree with NonceContext.Provider so that
|
|
7
|
+
* client components (e.g. MetaTags) can apply nonces to inline scripts.
|
|
8
|
+
* On the browser side, no provider is needed — the default undefined
|
|
9
|
+
* is correct since CSP nonces are a server-side HTML concern.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createContext, useContext, type Context } from "react";
|
|
13
|
+
|
|
14
|
+
export const NonceContext: Context<string | undefined> = createContext<
|
|
15
|
+
string | undefined
|
|
16
|
+
>(undefined);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Read the CSP nonce during SSR. Returns undefined on the client.
|
|
20
|
+
*/
|
|
21
|
+
export function useNonce(): string | undefined {
|
|
22
|
+
return useContext(NonceContext);
|
|
23
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shallow equality check for selector results.
|
|
3
|
+
* Uses Object.is for value comparison (handles NaN and +-0 correctly).
|
|
4
|
+
*/
|
|
5
|
+
export function shallowEqual<T>(a: T, b: T): boolean {
|
|
6
|
+
if (Object.is(a, b)) return true;
|
|
7
|
+
if (
|
|
8
|
+
typeof a !== "object" ||
|
|
9
|
+
a === null ||
|
|
10
|
+
typeof b !== "object" ||
|
|
11
|
+
b === null
|
|
12
|
+
) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
const keysA = Object.keys(a);
|
|
16
|
+
const keysB = Object.keys(b);
|
|
17
|
+
if (keysA.length !== keysB.length) return false;
|
|
18
|
+
for (const key of keysA) {
|
|
19
|
+
if (
|
|
20
|
+
!Object.hasOwn(b, key) ||
|
|
21
|
+
!Object.is((a as any)[key], (b as any)[key])
|
|
22
|
+
) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
@@ -9,7 +9,8 @@ import {
|
|
|
9
9
|
startTransition,
|
|
10
10
|
} from "react";
|
|
11
11
|
import { NavigationStoreContext } from "./context.js";
|
|
12
|
-
import
|
|
12
|
+
import { shallowEqual } from "./shallow-equal.js";
|
|
13
|
+
import type { TrackedActionState } from "../types.js";
|
|
13
14
|
import { invariant } from "../../errors.js";
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -44,7 +45,7 @@ function normalizeActionId(actionId: string): string {
|
|
|
44
45
|
export function getActionId(action: ServerActionFunction | string): string {
|
|
45
46
|
invariant(
|
|
46
47
|
typeof action === "function" || typeof action === "string",
|
|
47
|
-
`useAction: action must be a function or string, got ${typeof action}
|
|
48
|
+
`useAction: action must be a function or string, got ${typeof action}`,
|
|
48
49
|
);
|
|
49
50
|
const actionId = (action as any)?.$$id;
|
|
50
51
|
if (actionId) {
|
|
@@ -72,7 +73,7 @@ Solutions:
|
|
|
72
73
|
2. Use the action name as a string:
|
|
73
74
|
const state = useAction("myAction");
|
|
74
75
|
|
|
75
|
-
The string must match the exported function name from your "use server" file
|
|
76
|
+
The string must match the exported function name from your "use server" file.`,
|
|
76
77
|
);
|
|
77
78
|
}
|
|
78
79
|
|
|
@@ -126,19 +127,24 @@ export type ServerActionFunction = ((...args: any[]) => Promise<any>) & {
|
|
|
126
127
|
* const error = useAction(addToCart, state => state.error);
|
|
127
128
|
* ```
|
|
128
129
|
*
|
|
130
|
+
* @note The selector is expected to be stable for a given hook instance.
|
|
131
|
+
* This hook tracks one projection of one action. Changing selector semantics
|
|
132
|
+
* for the same action ID without a new action event is not a supported pattern;
|
|
133
|
+
* use separate useAction() subscriptions if you need different projections.
|
|
134
|
+
*
|
|
129
135
|
* @note Actions passed as props from server components lose their metadata
|
|
130
136
|
* during RSC serialization. Use a string action name or import directly.
|
|
131
137
|
*/
|
|
132
138
|
export function useAction(
|
|
133
|
-
action: ServerActionFunction | string
|
|
139
|
+
action: ServerActionFunction | string,
|
|
134
140
|
): TrackedActionState;
|
|
135
141
|
export function useAction<T>(
|
|
136
142
|
action: ServerActionFunction | string,
|
|
137
|
-
selector: (state: TrackedActionState) => T
|
|
143
|
+
selector: (state: TrackedActionState) => T,
|
|
138
144
|
): T;
|
|
139
145
|
export function useAction<T>(
|
|
140
146
|
action: ServerActionFunction | string,
|
|
141
|
-
selector?: (state: TrackedActionState) => T
|
|
147
|
+
selector?: (state: TrackedActionState) => T,
|
|
142
148
|
): T | TrackedActionState {
|
|
143
149
|
const ctx = useContext(NavigationStoreContext);
|
|
144
150
|
const actionId =
|
|
@@ -161,7 +167,10 @@ export function useAction<T>(
|
|
|
161
167
|
T | TrackedActionState
|
|
162
168
|
>(null!);
|
|
163
169
|
|
|
164
|
-
//
|
|
170
|
+
// Ref keeps the latest selector for subscription callbacks without
|
|
171
|
+
// re-subscribing on every render. Selector changes themselves are not
|
|
172
|
+
// treated as a reactive input; this hook expects a stable selector and
|
|
173
|
+
// represents one subscription/projection for one action.
|
|
165
174
|
const selectorRef = useRef(selector);
|
|
166
175
|
selectorRef.current = selector;
|
|
167
176
|
|
|
@@ -169,6 +178,17 @@ export function useAction<T>(
|
|
|
169
178
|
useEffect(() => {
|
|
170
179
|
if (!ctx) return;
|
|
171
180
|
|
|
181
|
+
// Sync current state for the (possibly new) actionId so that switching
|
|
182
|
+
// actions on an idle page doesn't leave stale data from the old action.
|
|
183
|
+
const currentState = ctx.eventController.getActionState(actionId);
|
|
184
|
+
const currentSelected = selectorRef.current
|
|
185
|
+
? selectorRef.current(currentState)
|
|
186
|
+
: currentState;
|
|
187
|
+
if (!shallowEqual(currentSelected, prevSelected.current)) {
|
|
188
|
+
prevSelected.current = currentSelected;
|
|
189
|
+
setBaseState(currentSelected);
|
|
190
|
+
}
|
|
191
|
+
|
|
172
192
|
// Subscribe to action-specific updates
|
|
173
193
|
const unsubscribe = ctx.eventController.subscribeToAction(
|
|
174
194
|
actionId,
|
|
@@ -177,14 +197,14 @@ export function useAction<T>(
|
|
|
177
197
|
? selectorRef.current(state)
|
|
178
198
|
: state;
|
|
179
199
|
|
|
180
|
-
if (!
|
|
200
|
+
if (!shallowEqual(selectedState, prevSelected.current)) {
|
|
181
201
|
prevSelected.current = selectedState;
|
|
182
202
|
setBaseState(selectedState);
|
|
183
203
|
startTransition(() => {
|
|
184
204
|
setOptimisticState(selectedState);
|
|
185
205
|
});
|
|
186
206
|
}
|
|
187
|
-
}
|
|
207
|
+
},
|
|
188
208
|
);
|
|
189
209
|
|
|
190
210
|
return () => {
|
|
@@ -195,46 +215,4 @@ export function useAction<T>(
|
|
|
195
215
|
return (optimisticState ?? baseState) as T | TrackedActionState;
|
|
196
216
|
}
|
|
197
217
|
|
|
198
|
-
function isShallowEqual<T, U>(selectedState: T, baseState: U): boolean {
|
|
199
|
-
// If references are equal, they're shallow equal
|
|
200
|
-
//@ts-expect-error -- TS doesn't like comparing generics
|
|
201
|
-
if (selectedState === baseState) {
|
|
202
|
-
return true;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// If either is null/undefined and they're not equal, they're not shallow equal
|
|
206
|
-
if (selectedState == null || baseState == null) {
|
|
207
|
-
return false;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// If types are different, they're not shallow equal
|
|
211
|
-
if (typeof selectedState !== typeof baseState) {
|
|
212
|
-
return false;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// For primitives, === comparison is sufficient (already checked above)
|
|
216
|
-
if (typeof selectedState !== "object") {
|
|
217
|
-
return false;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// For objects, compare keys and values shallowly
|
|
221
|
-
const keysA = Object.keys(selectedState as object);
|
|
222
|
-
const keysB = Object.keys(baseState as object);
|
|
223
|
-
|
|
224
|
-
if (keysA.length !== keysB.length) {
|
|
225
|
-
return false;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
for (const key of keysA) {
|
|
229
|
-
if (
|
|
230
|
-
!Object.prototype.hasOwnProperty.call(baseState, key) ||
|
|
231
|
-
(selectedState as any)[key] !== (baseState as any)[key]
|
|
232
|
-
) {
|
|
233
|
-
return false;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
return true;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
218
|
export type { TrackedActionState };
|
|
@@ -46,10 +46,12 @@ export interface ClientCacheControls {
|
|
|
46
46
|
export function useClientCache(): ClientCacheControls {
|
|
47
47
|
const ctx = useContext(NavigationStoreContext);
|
|
48
48
|
|
|
49
|
+
if (!ctx) {
|
|
50
|
+
throw new Error("useClientCache must be used within NavigationProvider");
|
|
51
|
+
}
|
|
52
|
+
|
|
49
53
|
const clear = useCallback(() => {
|
|
50
|
-
|
|
51
|
-
ctx.store.clearHistoryCache();
|
|
52
|
-
}
|
|
54
|
+
ctx.store.clearHistoryCache();
|
|
53
55
|
}, [ctx]);
|
|
54
56
|
|
|
55
57
|
return { clear };
|
|
@@ -9,119 +9,10 @@ import {
|
|
|
9
9
|
startTransition,
|
|
10
10
|
} from "react";
|
|
11
11
|
import type { Handle } from "../../handle.js";
|
|
12
|
-
import {
|
|
12
|
+
import { collectHandleData } from "../../handle.js";
|
|
13
13
|
import type { HandleData } from "../types.js";
|
|
14
14
|
import { NavigationStoreContext } from "./context.js";
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* SSR module-level state.
|
|
18
|
-
* Populated by initHandleDataSync before React renders.
|
|
19
|
-
* Used by useState initializer during SSR.
|
|
20
|
-
*/
|
|
21
|
-
let ssrHandleData: HandleData = {};
|
|
22
|
-
let ssrSegmentOrder: string[] = [];
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Filter segment IDs to only include routes and layouts.
|
|
26
|
-
* Excludes parallels (contain .@) and loaders (contain D followed by digit).
|
|
27
|
-
*/
|
|
28
|
-
function filterSegmentOrder(matched: string[]): string[] {
|
|
29
|
-
return matched.filter((id) => {
|
|
30
|
-
if (id.includes(".@")) return false;
|
|
31
|
-
if (/D\d+\./.test(id)) return false;
|
|
32
|
-
return true;
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Resolve the collect function for a handle.
|
|
38
|
-
* Handle objects are plain { __brand, $$id } - collect is stored in the registry
|
|
39
|
-
* (populated when createHandle runs on the client).
|
|
40
|
-
*/
|
|
41
|
-
function resolveCollect<T, A>(handle: Handle<T, A>): (segments: T[][]) => A {
|
|
42
|
-
// Look up collect from the registry (populated when the handle module is imported).
|
|
43
|
-
const registered = getCollectFn(handle.$$id);
|
|
44
|
-
if (registered) {
|
|
45
|
-
return registered as (segments: T[][]) => A;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Fall back to default flat collect with a dev warning.
|
|
49
|
-
if (process.env.NODE_ENV !== "production") {
|
|
50
|
-
console.warn(
|
|
51
|
-
`[rsc-router] Handle "${handle.$$id}" was passed as a prop but its collect ` +
|
|
52
|
-
`function could not be resolved. Falling back to flat array. ` +
|
|
53
|
-
`Import the handle module in a client component to register its collect function.`
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
return ((segments: unknown[][]) => segments.flat()) as unknown as (segments: T[][]) => A;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Collect handle data from segments and transform to final value.
|
|
61
|
-
*/
|
|
62
|
-
function collectHandle<T, A>(
|
|
63
|
-
handle: Handle<T, A>,
|
|
64
|
-
data: HandleData,
|
|
65
|
-
segmentOrder: string[]
|
|
66
|
-
): A {
|
|
67
|
-
const collect = resolveCollect(handle);
|
|
68
|
-
const segmentData = data[handle.$$id];
|
|
69
|
-
|
|
70
|
-
if (!segmentData) {
|
|
71
|
-
return collect([]);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Build array of segment arrays in parent -> child order
|
|
75
|
-
const segmentArrays: T[][] = [];
|
|
76
|
-
for (const segmentId of segmentOrder) {
|
|
77
|
-
const entries = segmentData[segmentId];
|
|
78
|
-
if (entries && entries.length > 0) {
|
|
79
|
-
segmentArrays.push(entries as T[]);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Call collect once with all segment data
|
|
84
|
-
return collect(segmentArrays);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Shallow equality check for selector results.
|
|
89
|
-
*/
|
|
90
|
-
function shallowEqual<T>(a: T, b: T): boolean {
|
|
91
|
-
if (Object.is(a, b)) return true;
|
|
92
|
-
if (
|
|
93
|
-
typeof a !== "object" ||
|
|
94
|
-
a === null ||
|
|
95
|
-
typeof b !== "object" ||
|
|
96
|
-
b === null
|
|
97
|
-
) {
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
|
-
const keysA = Object.keys(a);
|
|
101
|
-
const keysB = Object.keys(b);
|
|
102
|
-
if (keysA.length !== keysB.length) return false;
|
|
103
|
-
for (const key of keysA) {
|
|
104
|
-
if (
|
|
105
|
-
!Object.hasOwn(b, key) ||
|
|
106
|
-
!Object.is((a as any)[key], (b as any)[key])
|
|
107
|
-
) {
|
|
108
|
-
return false;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return true;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Initialize handle data synchronously for SSR.
|
|
116
|
-
* Called before rendering to populate state for useState initializer.
|
|
117
|
-
*
|
|
118
|
-
* @param data - Handle data from RSC payload
|
|
119
|
-
* @param matched - Segment order for reduction
|
|
120
|
-
*/
|
|
121
|
-
export function initHandleDataSync(data: HandleData, matched?: string[]): void {
|
|
122
|
-
ssrHandleData = data;
|
|
123
|
-
ssrSegmentOrder = filterSegmentOrder(matched ?? []);
|
|
124
|
-
}
|
|
15
|
+
import { shallowEqual } from "./shallow-equal.js";
|
|
125
16
|
|
|
126
17
|
/**
|
|
127
18
|
* Hook to access collected handle data.
|
|
@@ -144,25 +35,24 @@ export function initHandleDataSync(data: HandleData, matched?: string[]): void {
|
|
|
144
35
|
export function useHandle<T, A>(handle: Handle<T, A>): A;
|
|
145
36
|
export function useHandle<T, A, S>(
|
|
146
37
|
handle: Handle<T, A>,
|
|
147
|
-
selector: (data: A) => S
|
|
38
|
+
selector: (data: A) => S,
|
|
148
39
|
): S;
|
|
149
40
|
export function useHandle<T, A, S>(
|
|
150
41
|
handle: Handle<T, A>,
|
|
151
|
-
selector?: (data: A) => S
|
|
42
|
+
selector?: (data: A) => S,
|
|
152
43
|
): A | S {
|
|
153
44
|
const ctx = useContext(NavigationStoreContext);
|
|
154
45
|
|
|
155
|
-
// Initial state from
|
|
46
|
+
// Initial state from context event controller, or empty fallback without provider.
|
|
156
47
|
const [value, setValue] = useState<A | S>(() => {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const collected = collectHandle(handle, ssrHandleData, ssrSegmentOrder);
|
|
48
|
+
if (!ctx) {
|
|
49
|
+
const collected = collectHandleData(handle, {}, []);
|
|
160
50
|
return selector ? selector(collected) : collected;
|
|
161
51
|
}
|
|
162
52
|
|
|
163
53
|
// On client, use event controller state
|
|
164
54
|
const state = ctx.eventController.getHandleState();
|
|
165
|
-
const collected =
|
|
55
|
+
const collected = collectHandleData(handle, state.data, state.segmentOrder);
|
|
166
56
|
return selector ? selector(collected) : collected;
|
|
167
57
|
});
|
|
168
58
|
const [optimisticValue, setOptimisticValue] = useOptimistic(value);
|
|
@@ -171,7 +61,7 @@ export function useHandle<T, A, S>(
|
|
|
171
61
|
const prevValueRef = useRef(value);
|
|
172
62
|
prevValueRef.current = value;
|
|
173
63
|
|
|
174
|
-
//
|
|
64
|
+
// Ref keeps the latest selector without re-subscribing on every render.
|
|
175
65
|
const selectorRef = useRef(selector);
|
|
176
66
|
selectorRef.current = selector;
|
|
177
67
|
|
|
@@ -179,11 +69,31 @@ export function useHandle<T, A, S>(
|
|
|
179
69
|
useEffect(() => {
|
|
180
70
|
if (!ctx) return;
|
|
181
71
|
|
|
72
|
+
// Sync current state for the (possibly new) handle so that switching
|
|
73
|
+
// handles on an idle page doesn't leave stale data from the old handle.
|
|
74
|
+
const currentHandleState = ctx.eventController.getHandleState();
|
|
75
|
+
const currentCollected = collectHandleData(
|
|
76
|
+
handle,
|
|
77
|
+
currentHandleState.data,
|
|
78
|
+
currentHandleState.segmentOrder,
|
|
79
|
+
);
|
|
80
|
+
const currentValue = selectorRef.current
|
|
81
|
+
? selectorRef.current(currentCollected)
|
|
82
|
+
: currentCollected;
|
|
83
|
+
if (!shallowEqual(currentValue, prevValueRef.current)) {
|
|
84
|
+
prevValueRef.current = currentValue;
|
|
85
|
+
setValue(currentValue);
|
|
86
|
+
}
|
|
87
|
+
|
|
182
88
|
return ctx.eventController.subscribeToHandles(() => {
|
|
183
89
|
const state = ctx.eventController.getHandleState();
|
|
184
90
|
const isAction =
|
|
185
91
|
ctx.eventController.getState().inflightActions.length > 0;
|
|
186
|
-
const collected =
|
|
92
|
+
const collected = collectHandleData(
|
|
93
|
+
handle,
|
|
94
|
+
state.data,
|
|
95
|
+
state.segmentOrder,
|
|
96
|
+
);
|
|
187
97
|
const nextValue = selectorRef.current
|
|
188
98
|
? selectorRef.current(collected)
|
|
189
99
|
: collected;
|
|
@@ -16,7 +16,9 @@ import { NavigationStoreContext } from "./context.js";
|
|
|
16
16
|
* Context for Link component to provide its destination URL
|
|
17
17
|
* Used by useLinkStatus to determine if this specific link is pending
|
|
18
18
|
*/
|
|
19
|
-
export const LinkContext: Context<string | null> = createContext<string | null>(
|
|
19
|
+
export const LinkContext: Context<string | null> = createContext<string | null>(
|
|
20
|
+
null,
|
|
21
|
+
);
|
|
20
22
|
|
|
21
23
|
/**
|
|
22
24
|
* Link status returned by useLinkStatus hook
|
|
@@ -46,7 +48,7 @@ function normalizeUrl(url: string, origin: string): string {
|
|
|
46
48
|
function isPendingFor(
|
|
47
49
|
linkTo: string | null,
|
|
48
50
|
pendingUrl: string | null,
|
|
49
|
-
origin: string
|
|
51
|
+
origin: string,
|
|
50
52
|
): boolean {
|
|
51
53
|
if (linkTo === null || pendingUrl === null) {
|
|
52
54
|
return false;
|
|
@@ -81,9 +83,8 @@ export function useLinkStatus(): LinkStatus {
|
|
|
81
83
|
const ctx = useContext(NavigationStoreContext);
|
|
82
84
|
|
|
83
85
|
// Get origin for URL normalization (stable across renders)
|
|
84
|
-
const origin =
|
|
85
|
-
? window.location.origin
|
|
86
|
-
: "http://localhost";
|
|
86
|
+
const origin =
|
|
87
|
+
typeof window !== "undefined" ? window.location.origin : "http://localhost";
|
|
87
88
|
|
|
88
89
|
// Base state for useOptimistic
|
|
89
90
|
const [basePending, setBasePending] = useState<boolean>(() => {
|