houdini-react 2.0.0-next.40 → 2.0.0-next.45
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/package.json +8 -8
- package/postInstall.js +1 -1
- package/runtime/Link.tsx +24 -55
- package/runtime/hooks/useDocumentHandle.ts +9 -4
- package/runtime/hooks/useFragment.ts +169 -1
- package/runtime/hooks/useFragmentHandle.ts +104 -36
- package/runtime/hooks/useSubscription.ts +1 -1
- package/runtime/hooks/useSubscriptionHandle.ts +10 -6
- package/runtime/hydration.tsx +2 -2
- package/runtime/index.tsx +2 -2
- package/runtime/manifest.ts +1 -1
- package/runtime/mock.ts +9 -0
- package/runtime/resolve-href.ts +155 -5
- package/runtime/routes.ts +93 -0
- package/runtime/routing/Router.tsx +113 -45
- package/runtime/testing.tsx +163 -0
- package/vite/index.js +11 -3
- package/vite/strip-headers.d.ts +1 -0
- package/vite/strip-headers.js +35 -0
- package/vite/transform.d.ts +3 -1
- package/vite/transform.js +18 -3
package/runtime/manifest.ts
CHANGED
package/runtime/mock.ts
ADDED
package/runtime/resolve-href.ts
CHANGED
|
@@ -1,14 +1,164 @@
|
|
|
1
|
+
// A marshaler turns a rich runtime value (e.g. a Date) into the transport form that
|
|
2
|
+
// belongs in the URL (e.g. a timestamp). Keyed by param / search-param name.
|
|
3
|
+
type Marshaler = (value: any) => any
|
|
4
|
+
type Marshalers = Record<string, Marshaler>
|
|
5
|
+
|
|
6
|
+
// scalarMarshalers builds a name→marshal map for the route's params or search params by
|
|
7
|
+
// looking each declared GraphQL type up in the config's custom scalars. Names without a
|
|
8
|
+
// custom-scalar marshal function are omitted, so callers fall back to String(value).
|
|
9
|
+
// Kept pure (scalars are passed in) so it can be unit-tested without the runtime config.
|
|
10
|
+
export function scalarMarshalers(
|
|
11
|
+
defs: ReadonlyArray<{ name: string; type?: string }> | undefined,
|
|
12
|
+
scalars: Record<string, { marshal?: Marshaler } | undefined> | undefined
|
|
13
|
+
): Marshalers {
|
|
14
|
+
const out: Marshalers = {}
|
|
15
|
+
for (const def of defs ?? []) {
|
|
16
|
+
const marshal = def.type ? scalars?.[def.type]?.marshal : undefined
|
|
17
|
+
if (marshal) {
|
|
18
|
+
out[def.name] = marshal
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return out
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// marshalValue applies the per-key marshaler (if any) and stringifies the result.
|
|
25
|
+
function marshalValue(value: unknown, marshal?: Marshaler): string {
|
|
26
|
+
return String(marshal ? marshal(value) : value)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// An unmarshaler is the inverse of a Marshaler: it turns the transport form a URL carries
|
|
30
|
+
// back into a rich runtime value (e.g. a timestamp string into a Date).
|
|
31
|
+
type Unmarshaler = (value: any) => any
|
|
32
|
+
type Unmarshalers = Record<string, Unmarshaler>
|
|
33
|
+
|
|
34
|
+
// scalarUnmarshalers is the read-side mirror of scalarMarshalers: a name→unmarshal map for
|
|
35
|
+
// the declared params/search params whose GraphQL type is a custom scalar. Names without a
|
|
36
|
+
// custom-scalar unmarshal function are omitted (built-ins and UI-only keys are left as-is).
|
|
37
|
+
export function scalarUnmarshalers(
|
|
38
|
+
defs: ReadonlyArray<{ name: string; type?: string }> | undefined,
|
|
39
|
+
scalars: Record<string, { unmarshal?: Unmarshaler } | undefined> | undefined
|
|
40
|
+
): Unmarshalers {
|
|
41
|
+
const out: Unmarshalers = {}
|
|
42
|
+
for (const def of defs ?? []) {
|
|
43
|
+
const unmarshal = def.type ? scalars?.[def.type]?.unmarshal : undefined
|
|
44
|
+
if (unmarshal) {
|
|
45
|
+
out[def.name] = unmarshal
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return out
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// decodeScalar recovers the marshaled value from the string the URL carries. marshalValue
|
|
52
|
+
// wrote it with String(), which dropped the type, so we JSON.parse to get numbers, booleans
|
|
53
|
+
// and null back, falling back to the raw string when it isn't valid JSON (e.g. a custom
|
|
54
|
+
// scalar that marshals to a plain string). Consequence worth documenting: a value like
|
|
55
|
+
// "true" or "123" always decodes to a boolean / number before it reaches unmarshal.
|
|
56
|
+
function decodeScalar(value: string): unknown {
|
|
57
|
+
try {
|
|
58
|
+
return JSON.parse(value)
|
|
59
|
+
} catch {
|
|
60
|
+
return value
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// unmarshalScalars turns the transport values a parsed query string carries back into rich
|
|
65
|
+
// runtime values for every key that has a custom-scalar unmarshaler. Keys without one
|
|
66
|
+
// (built-ins, UI-only keys) pass through untouched; List values are unmarshaled
|
|
67
|
+
// element-wise. Used for both useRoute().search and the query variables the router feeds
|
|
68
|
+
// back into marshalInputs, so a custom-scalar search param round-trips correctly. When
|
|
69
|
+
// there's nothing to unmarshal the input object is returned as-is (no allocation).
|
|
70
|
+
export function unmarshalScalars(
|
|
71
|
+
values: Record<string, any>,
|
|
72
|
+
unmarshalers: Unmarshalers
|
|
73
|
+
): Record<string, any> {
|
|
74
|
+
if (Object.keys(unmarshalers).length === 0) {
|
|
75
|
+
return values
|
|
76
|
+
}
|
|
77
|
+
const out: Record<string, any> = { ...values }
|
|
78
|
+
for (const [key, unmarshal] of Object.entries(unmarshalers)) {
|
|
79
|
+
if (!(key in out) || out[key] == null) {
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
const value = out[key]
|
|
83
|
+
out[key] = Array.isArray(value)
|
|
84
|
+
? value.map((entry) => unmarshal(decodeScalar(entry)))
|
|
85
|
+
: unmarshal(decodeScalar(value))
|
|
86
|
+
}
|
|
87
|
+
return out
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// the per-route info buildHref needs to marshal: the declared param and search types.
|
|
91
|
+
export type RouteHrefInfo = {
|
|
92
|
+
params?: ReadonlyArray<{ name: string; type?: string }>
|
|
93
|
+
searchParams?: ReadonlyArray<{ name: string; type?: string }>
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// buildHref assembles a full href for a route: it fills the path params and appends the
|
|
97
|
+
// search string, marshaling custom-scalar values in both so the URL always carries their
|
|
98
|
+
// transport form. This is the one place params + search become a URL, shared by <Link>
|
|
99
|
+
// and goto so they behave identically. `route` provides the type info (undefined for an
|
|
100
|
+
// external href, which is returned untouched).
|
|
101
|
+
export function buildHref(
|
|
102
|
+
to: string,
|
|
103
|
+
route: RouteHrefInfo | undefined,
|
|
104
|
+
scalars: Record<string, { marshal?: Marshaler } | undefined> | undefined,
|
|
105
|
+
params?: Record<string, unknown>,
|
|
106
|
+
search?: Record<string, unknown>
|
|
107
|
+
): string {
|
|
108
|
+
let href =
|
|
109
|
+
params != null ? resolveHref(to, params, scalarMarshalers(route?.params, scalars)) : to
|
|
110
|
+
if (search != null) {
|
|
111
|
+
href += serializeSearch(search, scalarMarshalers(route?.searchParams, scalars))
|
|
112
|
+
}
|
|
113
|
+
return href
|
|
114
|
+
}
|
|
115
|
+
|
|
1
116
|
export function resolveHref(
|
|
2
117
|
href: string,
|
|
3
|
-
params: Record<string,
|
|
118
|
+
params: Record<string, unknown>,
|
|
119
|
+
marshalers?: Marshalers
|
|
4
120
|
): string {
|
|
121
|
+
// renders a present param value through its marshaler, or undefined when absent
|
|
122
|
+
const render = (key: string): string | undefined => {
|
|
123
|
+
const val = params[key]
|
|
124
|
+
if (val === undefined || val === null) {
|
|
125
|
+
return undefined
|
|
126
|
+
}
|
|
127
|
+
return marshalValue(val, marshalers?.[key])
|
|
128
|
+
}
|
|
5
129
|
// optional [[param]] — strip the whole /[[param]] segment when the value is absent
|
|
6
130
|
href = href.replace(/\/\[\[([^\]]+)\]\]/g, (_, key: string) => {
|
|
7
|
-
const val =
|
|
8
|
-
return val !== undefined ? '/' +
|
|
131
|
+
const val = render(key)
|
|
132
|
+
return val !== undefined ? '/' + val : ''
|
|
9
133
|
})
|
|
10
134
|
// rest [...slug] — substitute [..slug] with the value (or empty string when absent)
|
|
11
|
-
href = href.replace(/\[\.\.\.([^\]]+)\]/g, (_, key: string) =>
|
|
135
|
+
href = href.replace(/\[\.\.\.([^\]]+)\]/g, (_, key: string) => render(key) ?? '')
|
|
12
136
|
// regular [param]
|
|
13
|
-
return href.replace(/\[([^\]]+)\]/g, (_, key: string) =>
|
|
137
|
+
return href.replace(/\[([^\]]+)\]/g, (_, key: string) => render(key) ?? key)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// serializeSearch turns a search object into a query string (including the leading
|
|
141
|
+
// "?"), skipping null/undefined values and expanding arrays into repeated keys so
|
|
142
|
+
// they round-trip with List-typed query variables. A custom-scalar value is run
|
|
143
|
+
// through its marshaler so the URL holds the transport form. Returns "" when nothing
|
|
144
|
+
// is set.
|
|
145
|
+
export function serializeSearch(search: Record<string, unknown>, marshalers?: Marshalers): string {
|
|
146
|
+
const params = new URLSearchParams()
|
|
147
|
+
for (const [key, value] of Object.entries(search)) {
|
|
148
|
+
if (value === null || value === undefined) {
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
const marshal = marshalers?.[key]
|
|
152
|
+
if (Array.isArray(value)) {
|
|
153
|
+
for (const entry of value) {
|
|
154
|
+
if (entry !== null && entry !== undefined) {
|
|
155
|
+
params.append(key, marshalValue(entry, marshal))
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
params.append(key, marshalValue(value, marshal))
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const str = params.toString()
|
|
163
|
+
return str ? '?' + str : ''
|
|
14
164
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// this file is part of houdini's generated runtime — do not edit
|
|
2
|
+
//
|
|
3
|
+
// It is the single source of per-route typing: the types here are derived from the
|
|
4
|
+
// generated manifest's shape and drive <Link>, goto(), and createMock so the rules for
|
|
5
|
+
// "which params/search does this route accept" live in exactly one place.
|
|
6
|
+
|
|
7
|
+
// @ts-ignore
|
|
8
|
+
import type rawManifest from './manifest.js'
|
|
9
|
+
// @ts-ignore
|
|
10
|
+
import type { _TSType } from './manifest.js'
|
|
11
|
+
|
|
12
|
+
type _Pages = (typeof rawManifest)['pages']
|
|
13
|
+
|
|
14
|
+
// ---- route params ----
|
|
15
|
+
|
|
16
|
+
type _Param = { readonly name: string; readonly type: string; readonly optional: boolean }
|
|
17
|
+
type _ParamObj<Ps extends readonly _Param[]> = {
|
|
18
|
+
[P in Ps[number] as P['optional'] extends true ? P['name'] : never]?: _TSType<P['type']>
|
|
19
|
+
} & {
|
|
20
|
+
[P in Ps[number] as P['optional'] extends true ? never : P['name']]: _TSType<P['type']>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ---- search params ----
|
|
24
|
+
|
|
25
|
+
// Search params are derived from a query's nullable variables, so every key is
|
|
26
|
+
// optional. A `List`-wrapped variable accepts an array (serialized as repeated keys).
|
|
27
|
+
type _SearchParam = {
|
|
28
|
+
readonly name: string
|
|
29
|
+
readonly type: string
|
|
30
|
+
readonly wrappers: readonly string[]
|
|
31
|
+
}
|
|
32
|
+
type _SearchValue<P extends _SearchParam> = 'List' extends P['wrappers'][number]
|
|
33
|
+
? _TSType<P['type']>[]
|
|
34
|
+
: _TSType<P['type']>
|
|
35
|
+
// The route's nullable query variables get precise types, but search isn't limited to
|
|
36
|
+
// them: extra keys are allowed so the query string can also carry UI-only state that no
|
|
37
|
+
// query reads (a selected tab, an open modal, a client-side sort). The query consumes
|
|
38
|
+
// only the declared keys; the rest just ride along in the URL.
|
|
39
|
+
type _SearchObj<Ps extends readonly _SearchParam[]> = {
|
|
40
|
+
[P in Ps[number] as P['name']]?: _SearchValue<P>
|
|
41
|
+
} & Record<string, unknown>
|
|
42
|
+
|
|
43
|
+
// ---- public route typing ----
|
|
44
|
+
|
|
45
|
+
// All known app route URL strings — useful as a constraint for custom link wrappers.
|
|
46
|
+
export type RouteHrefs = _Pages[keyof _Pages] extends { readonly url: infer U extends string }
|
|
47
|
+
? U
|
|
48
|
+
: never
|
|
49
|
+
|
|
50
|
+
// hrefs that aren't app routes: external links, mailto/tel, fragments, relative paths.
|
|
51
|
+
export type ExternalHref =
|
|
52
|
+
| `http://${string}`
|
|
53
|
+
| `https://${string}`
|
|
54
|
+
| `mailto:${string}`
|
|
55
|
+
| `tel:${string}`
|
|
56
|
+
| `blob:${string}`
|
|
57
|
+
| `data:${string}`
|
|
58
|
+
| `//${string}`
|
|
59
|
+
| `#${string}`
|
|
60
|
+
| `./${string}`
|
|
61
|
+
| `../${string}`
|
|
62
|
+
|
|
63
|
+
type _PageForRoute<H extends string> = Extract<_Pages[keyof _Pages], { readonly url: H }>
|
|
64
|
+
|
|
65
|
+
// params is required (and typed) when the route has dynamic segments, and absent
|
|
66
|
+
// otherwise. Kept a separate intersection so TS evaluates `to` independently — route
|
|
67
|
+
// completions still include parameterized routes even before params is filled in.
|
|
68
|
+
export type ParamsForRoute<H extends string> = [_PageForRoute<H>] extends [never]
|
|
69
|
+
? { params?: never }
|
|
70
|
+
: _PageForRoute<H> extends { readonly params: readonly [] }
|
|
71
|
+
? { params?: never }
|
|
72
|
+
: _PageForRoute<H> extends { readonly params: infer Ps extends readonly _Param[] }
|
|
73
|
+
? { params: _ParamObj<Ps> }
|
|
74
|
+
: { params?: never }
|
|
75
|
+
|
|
76
|
+
// search is always optional and always open (any route can carry UI-only query string
|
|
77
|
+
// state). When the route declares nullable query variables, those keys get precise
|
|
78
|
+
// types; otherwise search is just an open record of serializable values.
|
|
79
|
+
export type SearchForRoute<H extends string> = [_PageForRoute<H>] extends [never]
|
|
80
|
+
? { search?: Record<string, unknown> }
|
|
81
|
+
: _PageForRoute<H> extends { readonly searchParams: infer Ps extends readonly _SearchParam[] }
|
|
82
|
+
? { search?: _SearchObj<Ps> }
|
|
83
|
+
: { search?: Record<string, unknown> }
|
|
84
|
+
|
|
85
|
+
// a typed navigation target: a known route plus its (typed) params and search. Used by
|
|
86
|
+
// goto(); <Link> intersects the same pieces onto its anchor props.
|
|
87
|
+
export type NavTarget<H extends RouteHrefs> = { to: H } & ParamsForRoute<H> & SearchForRoute<H>
|
|
88
|
+
|
|
89
|
+
// goto() accepts either a ready-made url string (escape hatch) or a typed NavTarget.
|
|
90
|
+
export interface Goto {
|
|
91
|
+
(url: string): void
|
|
92
|
+
<H extends RouteHrefs>(target: NavTarget<H>): void
|
|
93
|
+
}
|
|
@@ -12,6 +12,8 @@ import type { RouterManifest, RouterPageManifest } from 'houdini/router/types'
|
|
|
12
12
|
import React from 'react'
|
|
13
13
|
import { useContext, useEffect } from 'react'
|
|
14
14
|
|
|
15
|
+
import { buildHref, scalarUnmarshalers, unmarshalScalars } from '../resolve-href.js'
|
|
16
|
+
import type { Goto } from '../routes.js'
|
|
15
17
|
import { type DocumentHandle, useDocumentHandle } from '../hooks/useDocumentHandle.js'
|
|
16
18
|
import { useDocumentStore } from '../hooks/useDocumentStore.js'
|
|
17
19
|
import { type SuspenseCache, suspense_cache } from './cache.js'
|
|
@@ -51,11 +53,21 @@ export function Router({
|
|
|
51
53
|
}) {
|
|
52
54
|
// the current route is just a string in state.
|
|
53
55
|
const [currentURL, setCurrentURL] = React.useState(() => {
|
|
54
|
-
return initialURL || window.location.pathname
|
|
56
|
+
return initialURL || window.location.pathname + window.location.search
|
|
55
57
|
})
|
|
56
58
|
|
|
57
|
-
// find the matching page for the current route
|
|
58
|
-
|
|
59
|
+
// find the matching page for the current route. find_match also hands back the parsed
|
|
60
|
+
// query string (declared search params coerced, UI-only keys raw). custom-scalar route
|
|
61
|
+
// and search params arrive in their url transport form, so we unmarshal them once here
|
|
62
|
+
// — the rich values feed both the query variables (which marshalInputs re-marshals for
|
|
63
|
+
// the request) and useRoute()'s params/search.
|
|
64
|
+
const [page, rawVariables, rawSearch] = find_match(manifest, currentURL)
|
|
65
|
+
const unmarshalers = scalarUnmarshalers(
|
|
66
|
+
[...(page?.params ?? []), ...(page?.searchParams ?? [])],
|
|
67
|
+
getCurrentConfig()?.scalars
|
|
68
|
+
)
|
|
69
|
+
const variables = unmarshalScalars(rawVariables ?? {}, unmarshalers)
|
|
70
|
+
const search = unmarshalScalars(rawSearch, unmarshalers)
|
|
59
71
|
const is404 = !page
|
|
60
72
|
// When no exact match, find the deepest prefix-matching page to render
|
|
61
73
|
// its layout chain with NotFoundGate throwing inside the appropriate boundary.
|
|
@@ -88,7 +100,7 @@ export function Router({
|
|
|
88
100
|
|
|
89
101
|
// whenever the route changes, we need to make sure the browser's stack is up to date
|
|
90
102
|
React.useEffect(() => {
|
|
91
|
-
if (globalThis.window && window.location.pathname !== currentURL) {
|
|
103
|
+
if (globalThis.window && window.location.pathname + window.location.search !== currentURL) {
|
|
92
104
|
window.history.pushState({}, '', currentURL)
|
|
93
105
|
}
|
|
94
106
|
}, [currentURL])
|
|
@@ -99,7 +111,7 @@ export function Router({
|
|
|
99
111
|
return
|
|
100
112
|
}
|
|
101
113
|
const onChange = (_evt: PopStateEvent) => {
|
|
102
|
-
setCurrentURL(window.location.pathname)
|
|
114
|
+
setCurrentURL(window.location.pathname + window.location.search)
|
|
103
115
|
}
|
|
104
116
|
window.addEventListener('popstate', onChange)
|
|
105
117
|
return () => {
|
|
@@ -107,8 +119,26 @@ export function Router({
|
|
|
107
119
|
}
|
|
108
120
|
}, [])
|
|
109
121
|
|
|
110
|
-
// the function to call to navigate
|
|
111
|
-
|
|
122
|
+
// the function to call to navigate. accepts either a ready-made url string or a
|
|
123
|
+
// typed target { to, params, search } that is assembled (and custom scalars
|
|
124
|
+
// marshaled) exactly the way <Link> builds its href. The typed surface is the
|
|
125
|
+
// shared Goto contract; the implementation takes the loose runtime shape.
|
|
126
|
+
const goto = ((
|
|
127
|
+
target:
|
|
128
|
+
| string
|
|
129
|
+
| { to: string; params?: Record<string, unknown>; search?: Record<string, unknown> }
|
|
130
|
+
) => {
|
|
131
|
+
const url =
|
|
132
|
+
typeof target === 'string'
|
|
133
|
+
? target
|
|
134
|
+
: buildHref(
|
|
135
|
+
target.to,
|
|
136
|
+
manifest.pages[manifest.pagesByUrl[target.to]],
|
|
137
|
+
getCurrentConfig()?.scalars,
|
|
138
|
+
target.params,
|
|
139
|
+
target.search
|
|
140
|
+
)
|
|
141
|
+
|
|
112
142
|
// clear the data cache so that we refetch queries with the new session (will force a cache-lookup)
|
|
113
143
|
data_cache.clear()
|
|
114
144
|
// clear pending signals so the next render starts fresh load_query calls
|
|
@@ -116,7 +146,7 @@ export function Router({
|
|
|
116
146
|
|
|
117
147
|
// perform the navigation
|
|
118
148
|
setCurrentURL(url)
|
|
119
|
-
}
|
|
149
|
+
}) as Goto
|
|
120
150
|
|
|
121
151
|
// links are powered using anchor tags that we intercept and handle ourselves
|
|
122
152
|
useLinkBehavior({
|
|
@@ -125,10 +155,19 @@ export function Router({
|
|
|
125
155
|
// there are 2 things that we could preload: the page component and the data
|
|
126
156
|
|
|
127
157
|
// look for the matching route information
|
|
128
|
-
const [page,
|
|
158
|
+
const [page, rawVariables] = find_match(manifest, url)
|
|
129
159
|
if (!page) {
|
|
130
160
|
return
|
|
131
161
|
}
|
|
162
|
+
// unmarshal any custom-scalar route/search params so the preloaded query
|
|
163
|
+
// marshals them the same way the rendered one does
|
|
164
|
+
const variables = unmarshalScalars(
|
|
165
|
+
rawVariables ?? {},
|
|
166
|
+
scalarUnmarshalers(
|
|
167
|
+
[...page.params, ...page.searchParams],
|
|
168
|
+
getCurrentConfig()?.scalars
|
|
169
|
+
)
|
|
170
|
+
)
|
|
132
171
|
|
|
133
172
|
// load the page component if necessary
|
|
134
173
|
if (['page', 'component'].includes(which)) {
|
|
@@ -146,33 +185,34 @@ export function Router({
|
|
|
146
185
|
// render the component embedded in the necessary context so it can orchestrate
|
|
147
186
|
// its needs
|
|
148
187
|
return (
|
|
149
|
-
<
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
</VariableContext.Provider>
|
|
188
|
+
<LocationContext.Provider
|
|
189
|
+
value={{
|
|
190
|
+
pathname: currentURL,
|
|
191
|
+
goto,
|
|
192
|
+
params: variables ?? {},
|
|
193
|
+
search,
|
|
194
|
+
}}
|
|
195
|
+
>
|
|
196
|
+
<Is404Context.Provider value={is404}>
|
|
197
|
+
{is404 ? (
|
|
198
|
+
<NotFoundLayoutBoundary key={targetPage.id}>
|
|
199
|
+
<PageComponent url={currentURL} key={targetPage.id + '__404'} />
|
|
200
|
+
</NotFoundLayoutBoundary>
|
|
201
|
+
) : (
|
|
202
|
+
<PageComponent url={currentURL} key={targetPage.id} />
|
|
203
|
+
)}
|
|
204
|
+
</Is404Context.Provider>
|
|
205
|
+
</LocationContext.Provider>
|
|
168
206
|
)
|
|
169
207
|
}
|
|
170
208
|
|
|
171
|
-
//
|
|
172
|
-
|
|
209
|
+
// internal accessor for the raw location context. the public surface is useRoute, which
|
|
210
|
+
// layers the per-route param/search types on top of this. not re-exported from the package
|
|
211
|
+
// index, so it isn't part of the public API.
|
|
212
|
+
export const useLocationContext = () => useContext(LocationContext)
|
|
173
213
|
|
|
174
214
|
export const ClientRedirect = ({ to }: { to: string }) => {
|
|
175
|
-
const { goto } =
|
|
215
|
+
const { goto } = useLocationContext()
|
|
176
216
|
useEffect(() => {
|
|
177
217
|
goto(to)
|
|
178
218
|
}, [to])
|
|
@@ -632,20 +672,18 @@ export function useSession(): [App.Session, (newSession: Partial<App.Session>) =
|
|
|
632
672
|
return [ctx.session, updateSession]
|
|
633
673
|
}
|
|
634
674
|
|
|
635
|
-
export function useCurrentVariables(): GraphQLVariables {
|
|
636
|
-
return React.useContext(VariableContext)
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
const VariableContext = React.createContext<GraphQLVariables>(null)
|
|
640
|
-
|
|
641
675
|
const LocationContext = React.createContext<{
|
|
642
676
|
pathname: string
|
|
643
677
|
params: Record<string, any>
|
|
678
|
+
// the parsed query string of the current url (declared search params coerced to
|
|
679
|
+
// their scalar type, other keys raw; repeated keys are arrays).
|
|
680
|
+
search: Record<string, any>
|
|
644
681
|
// a function to imperatively navigate to a url
|
|
645
|
-
goto:
|
|
682
|
+
goto: Goto
|
|
646
683
|
}>({
|
|
647
684
|
pathname: '',
|
|
648
685
|
params: {},
|
|
686
|
+
search: {},
|
|
649
687
|
goto: () => {},
|
|
650
688
|
})
|
|
651
689
|
|
|
@@ -686,7 +724,7 @@ function useLinkBehavior({
|
|
|
686
724
|
goto,
|
|
687
725
|
preload,
|
|
688
726
|
}: {
|
|
689
|
-
goto:
|
|
727
|
+
goto: Goto
|
|
690
728
|
preload: (url: string, which: PreloadWhichValue) => void
|
|
691
729
|
}) {
|
|
692
730
|
// always use the click handler
|
|
@@ -702,7 +740,7 @@ function useLinkBehavior({
|
|
|
702
740
|
}
|
|
703
741
|
}
|
|
704
742
|
|
|
705
|
-
function useLinkNavigation({ goto }: { goto:
|
|
743
|
+
function useLinkNavigation({ goto }: { goto: Goto }) {
|
|
706
744
|
// navigations need to be registered as transitions
|
|
707
745
|
const [_pending, startTransition] = React.useTransition()
|
|
708
746
|
|
|
@@ -925,7 +963,7 @@ export function PageContextProvider({
|
|
|
925
963
|
keys: string[]
|
|
926
964
|
children: React.ReactNode
|
|
927
965
|
}) {
|
|
928
|
-
const location =
|
|
966
|
+
const location = useLocationContext()
|
|
929
967
|
const params = Object.fromEntries(
|
|
930
968
|
Object.entries(location.params).filter(([key]) => keys.includes(key))
|
|
931
969
|
)
|
|
@@ -933,12 +971,42 @@ export function PageContextProvider({
|
|
|
933
971
|
return <PageContext.Provider value={{ params }}>{children}</PageContext.Provider>
|
|
934
972
|
}
|
|
935
973
|
|
|
936
|
-
|
|
937
|
-
|
|
974
|
+
// useRoute is the single hook for reading the current route: the route's params (scoped to
|
|
975
|
+
// this route's path segments) and search, both typed when a generated PageRoute/LayoutRoute
|
|
976
|
+
// is supplied, plus the current pathname and goto. This replaces the old useLocation —
|
|
977
|
+
// params/search live here rather than on the component props. Unlike a conventional
|
|
978
|
+
// useLocation it also carries params and goto, which is why it's named for the route.
|
|
979
|
+
export function useRoute<
|
|
980
|
+
// the default leaves params/search empty (not a loose record) so that reading them
|
|
981
|
+
// without passing the route's generated PageRoute type is a compile error, while
|
|
982
|
+
// pathname and goto stay available for navigation-only code.
|
|
983
|
+
_Route extends { params: any; search: any } = { params: {}; search: {} },
|
|
984
|
+
>(): {
|
|
985
|
+
pathname: string
|
|
986
|
+
params: _Route['params']
|
|
987
|
+
search: _Route['search']
|
|
988
|
+
goto: Goto
|
|
989
|
+
} {
|
|
990
|
+
const location = useLocationContext()
|
|
991
|
+
const route = useContext(PageContext)
|
|
992
|
+
return {
|
|
993
|
+
pathname: location.pathname,
|
|
994
|
+
params: route.params as _Route['params'],
|
|
995
|
+
search: location.search as _Route['search'],
|
|
996
|
+
goto: location.goto,
|
|
997
|
+
}
|
|
938
998
|
}
|
|
939
999
|
|
|
940
|
-
|
|
941
|
-
|
|
1000
|
+
// A route shape for useRoute. A route's generated PageRoute/LayoutRoute already satisfies the
|
|
1001
|
+
// `{ params; search }` shape, so the common case is useRoute<PageRoute>(). For a route-agnostic
|
|
1002
|
+
// component (e.g. a reusable paginator that assumes its route exposes `after`/`first` search
|
|
1003
|
+
// params) there's no single generated type to pass — use GenericRoute to type the axis you
|
|
1004
|
+
// depend on and leave the other `never` (which falls back to a loose record). Search comes
|
|
1005
|
+
// first since that's the usual reason to reach for it, so a search-only component writes
|
|
1006
|
+
// GenericRoute<{ ... }> and a params-only one writes GenericRoute<never, { ... }>.
|
|
1007
|
+
export type GenericRoute<Search = never, Params = never> = {
|
|
1008
|
+
params: [Params] extends [never] ? Record<string, any> : Params
|
|
1009
|
+
search: [Search] extends [never] ? Record<string, any> : Search
|
|
942
1010
|
}
|
|
943
1011
|
|
|
944
1012
|
// a signal promise is a promise is used to send signals by having listeners attach
|