houdini-react 2.0.0-next.41 → 2.0.0-next.46
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 +101 -5
- package/runtime/hooks/useSubscription.ts +1 -1
- package/runtime/hooks/useSubscriptionHandle.ts +10 -6
- package/runtime/hydration.tsx +19 -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 +130 -46
- 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/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])
|
|
@@ -274,7 +314,23 @@ function usePageData({
|
|
|
274
314
|
injectToStream?.(`
|
|
275
315
|
<script>
|
|
276
316
|
{
|
|
277
|
-
|
|
317
|
+
// the resolved cache snapshot for this streamed query. when the bootstrap
|
|
318
|
+
// module has already run (data arrived after hydration) we hydrate the live
|
|
319
|
+
// cache directly; otherwise the module is still deferred (an @loading query
|
|
320
|
+
// streams its data while the document is open) so we queue the snapshot for
|
|
321
|
+
// hydrate_page to apply once it creates the cache. without this the snapshot
|
|
322
|
+
// would be dropped and the query would hydrate with null data.
|
|
323
|
+
const __houdini__snapshot__ = ${cache.serialize()}
|
|
324
|
+
if (window.__houdini__cache__) {
|
|
325
|
+
// hydrate into a fresh layer and merge it down, rather than clobbering the
|
|
326
|
+
// shared hydration layer (which would drop everything hydrated before it)
|
|
327
|
+
const __houdini__layer__ = window.__houdini__cache__.hydrate(__houdini__snapshot__)
|
|
328
|
+
if (__houdini__layer__) {
|
|
329
|
+
window.__houdini__cache__._internal_unstable.storage.resolveLayer(__houdini__layer__.id)
|
|
330
|
+
}
|
|
331
|
+
} else {
|
|
332
|
+
(window.__houdini__pending_cache__ = window.__houdini__pending_cache__ || []).push(__houdini__snapshot__)
|
|
333
|
+
}
|
|
278
334
|
|
|
279
335
|
const artifactName = "${artifact.name}"
|
|
280
336
|
const value = ${JSON.stringify(
|
|
@@ -632,20 +688,18 @@ export function useSession(): [App.Session, (newSession: Partial<App.Session>) =
|
|
|
632
688
|
return [ctx.session, updateSession]
|
|
633
689
|
}
|
|
634
690
|
|
|
635
|
-
export function useCurrentVariables(): GraphQLVariables {
|
|
636
|
-
return React.useContext(VariableContext)
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
const VariableContext = React.createContext<GraphQLVariables>(null)
|
|
640
|
-
|
|
641
691
|
const LocationContext = React.createContext<{
|
|
642
692
|
pathname: string
|
|
643
693
|
params: Record<string, any>
|
|
694
|
+
// the parsed query string of the current url (declared search params coerced to
|
|
695
|
+
// their scalar type, other keys raw; repeated keys are arrays).
|
|
696
|
+
search: Record<string, any>
|
|
644
697
|
// a function to imperatively navigate to a url
|
|
645
|
-
goto:
|
|
698
|
+
goto: Goto
|
|
646
699
|
}>({
|
|
647
700
|
pathname: '',
|
|
648
701
|
params: {},
|
|
702
|
+
search: {},
|
|
649
703
|
goto: () => {},
|
|
650
704
|
})
|
|
651
705
|
|
|
@@ -686,7 +740,7 @@ function useLinkBehavior({
|
|
|
686
740
|
goto,
|
|
687
741
|
preload,
|
|
688
742
|
}: {
|
|
689
|
-
goto:
|
|
743
|
+
goto: Goto
|
|
690
744
|
preload: (url: string, which: PreloadWhichValue) => void
|
|
691
745
|
}) {
|
|
692
746
|
// always use the click handler
|
|
@@ -702,7 +756,7 @@ function useLinkBehavior({
|
|
|
702
756
|
}
|
|
703
757
|
}
|
|
704
758
|
|
|
705
|
-
function useLinkNavigation({ goto }: { goto:
|
|
759
|
+
function useLinkNavigation({ goto }: { goto: Goto }) {
|
|
706
760
|
// navigations need to be registered as transitions
|
|
707
761
|
const [_pending, startTransition] = React.useTransition()
|
|
708
762
|
|
|
@@ -925,7 +979,7 @@ export function PageContextProvider({
|
|
|
925
979
|
keys: string[]
|
|
926
980
|
children: React.ReactNode
|
|
927
981
|
}) {
|
|
928
|
-
const location =
|
|
982
|
+
const location = useLocationContext()
|
|
929
983
|
const params = Object.fromEntries(
|
|
930
984
|
Object.entries(location.params).filter(([key]) => keys.includes(key))
|
|
931
985
|
)
|
|
@@ -933,12 +987,42 @@ export function PageContextProvider({
|
|
|
933
987
|
return <PageContext.Provider value={{ params }}>{children}</PageContext.Provider>
|
|
934
988
|
}
|
|
935
989
|
|
|
936
|
-
|
|
937
|
-
|
|
990
|
+
// useRoute is the single hook for reading the current route: the route's params (scoped to
|
|
991
|
+
// this route's path segments) and search, both typed when a generated PageRoute/LayoutRoute
|
|
992
|
+
// is supplied, plus the current pathname and goto. This replaces the old useLocation —
|
|
993
|
+
// params/search live here rather than on the component props. Unlike a conventional
|
|
994
|
+
// useLocation it also carries params and goto, which is why it's named for the route.
|
|
995
|
+
export function useRoute<
|
|
996
|
+
// the default leaves params/search empty (not a loose record) so that reading them
|
|
997
|
+
// without passing the route's generated PageRoute type is a compile error, while
|
|
998
|
+
// pathname and goto stay available for navigation-only code.
|
|
999
|
+
_Route extends { params: any; search: any } = { params: {}; search: {} },
|
|
1000
|
+
>(): {
|
|
1001
|
+
pathname: string
|
|
1002
|
+
params: _Route['params']
|
|
1003
|
+
search: _Route['search']
|
|
1004
|
+
goto: Goto
|
|
1005
|
+
} {
|
|
1006
|
+
const location = useLocationContext()
|
|
1007
|
+
const route = useContext(PageContext)
|
|
1008
|
+
return {
|
|
1009
|
+
pathname: location.pathname,
|
|
1010
|
+
params: route.params as _Route['params'],
|
|
1011
|
+
search: location.search as _Route['search'],
|
|
1012
|
+
goto: location.goto,
|
|
1013
|
+
}
|
|
938
1014
|
}
|
|
939
1015
|
|
|
940
|
-
|
|
941
|
-
|
|
1016
|
+
// A route shape for useRoute. A route's generated PageRoute/LayoutRoute already satisfies the
|
|
1017
|
+
// `{ params; search }` shape, so the common case is useRoute<PageRoute>(). For a route-agnostic
|
|
1018
|
+
// component (e.g. a reusable paginator that assumes its route exposes `after`/`first` search
|
|
1019
|
+
// params) there's no single generated type to pass — use GenericRoute to type the axis you
|
|
1020
|
+
// depend on and leave the other `never` (which falls back to a loose record). Search comes
|
|
1021
|
+
// first since that's the usual reason to reach for it, so a search-only component writes
|
|
1022
|
+
// GenericRoute<{ ... }> and a params-only one writes GenericRoute<never, { ... }>.
|
|
1023
|
+
export type GenericRoute<Search = never, Params = never> = {
|
|
1024
|
+
params: [Params] extends [never] ? Record<string, any> : Params
|
|
1025
|
+
search: [Search] extends [never] ? Record<string, any> : Search
|
|
942
1026
|
}
|
|
943
1027
|
|
|
944
1028
|
// a signal promise is a promise is used to send signals by having listeners attach
|