@kasperrt/smolter 0.4.3 → 0.4.4
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/.generated/client/hash-history.ts +27 -0
- package/.generated/client/hydration.ts +98 -0
- package/.generated/client/root-data.ts +52 -0
- package/.generated/core/prepare-routes.ts +50 -0
- package/.generated/core/presentation.ts +1 -1
- package/.generated/preact/client.tsx +1 -26
- package/.generated/preact/deferred-hydration.ts +1 -98
- package/.generated/preact/index.ts +6 -1
- package/.generated/preact/navigation.tsx +13 -5
- package/.generated/preact/root-data.ts +1 -45
- package/.generated/preact/router.tsx +2 -48
- package/.generated/react/client.tsx +130 -0
- package/.generated/react/document.tsx +235 -0
- package/.generated/react/fetcher.ts +29 -7
- package/.generated/react/hooks.ts +20 -18
- package/.generated/react/index.ts +17 -1
- package/.generated/react/lazy-route.ts +116 -0
- package/.generated/react/link.ts +1 -38
- package/.generated/react/navigation-state.ts +13 -0
- package/.generated/react/navigation.ts +145 -0
- package/.generated/react/root-context.ts +22 -0
- package/.generated/react/route-hooks.ts +159 -0
- package/.generated/react/route-types.ts +91 -0
- package/.generated/react/router.ts +50 -0
- package/.generated/react/runtime.ts +1 -0
- package/.generated/server/application.tsx +18 -253
- package/.generated/server/dispatch.ts +87 -0
- package/.generated/server/index.ts +2 -0
- package/.generated/server/react.ts +34 -0
- package/.generated/server/render-application.ts +203 -0
- package/.generated/vite/application/lazy-files.ts +2 -1
- package/.generated/vite/application/vite.ts +12 -4
- package/.generated/vite/react.ts +11 -0
- package/CHANGELOG.md +10 -0
- package/README.md +41 -41
- package/dist/client/hash-history.d.ts +2 -0
- package/dist/client/hash-history.js +26 -0
- package/dist/client/hydration.d.ts +10 -0
- package/dist/{preact/deferred-hydration.js → client/hydration.js} +1 -1
- package/dist/client/root-data.d.ts +14 -0
- package/dist/{preact → client}/root-data.js +1 -1
- package/dist/core/prepare-routes.d.ts +7 -0
- package/dist/core/prepare-routes.js +28 -0
- package/dist/core/presentation.js +1 -1
- package/dist/preact/client.js +5 -26
- package/dist/preact/deferred-hydration.d.ts +1 -10
- package/dist/preact/document.js +2 -2
- package/dist/preact/index.d.ts +1 -1
- package/dist/preact/index.js +1 -1
- package/dist/preact/navigation-state.js +1 -1
- package/dist/preact/navigation.js +7 -5
- package/dist/preact/root-data.d.ts +1 -8
- package/dist/preact/route-hooks.js +1 -1
- package/dist/preact/router.d.ts +2 -2
- package/dist/preact/router.js +2 -26
- package/dist/react/client.d.ts +20 -0
- package/dist/react/client.js +86 -0
- package/dist/react/document.d.ts +25 -0
- package/dist/react/document.js +169 -0
- package/dist/react/fetcher.d.ts +8 -4
- package/dist/react/fetcher.js +12 -1
- package/dist/react/form.js +1 -1
- package/dist/react/hooks.d.ts +7 -6
- package/dist/react/index.d.ts +7 -1
- package/dist/react/index.js +6 -2
- package/dist/react/lazy-route.d.ts +22 -0
- package/dist/react/lazy-route.js +57 -0
- package/dist/react/link.d.ts +1 -12
- package/dist/react/navigation-state.d.ts +2 -0
- package/dist/react/navigation-state.js +9 -0
- package/dist/react/navigation.d.ts +33 -0
- package/dist/react/navigation.js +64 -0
- package/dist/react/root-context.d.ts +14 -0
- package/dist/react/root-context.js +10 -0
- package/dist/react/route-hooks.d.ts +34 -0
- package/dist/react/route-hooks.js +89 -0
- package/dist/react/route-types.d.ts +69 -0
- package/dist/react/router.d.ts +10 -0
- package/dist/react/router.js +22 -0
- package/dist/react/runtime.d.ts +1 -1
- package/dist/react/runtime.js +1 -1
- package/dist/server/application.d.ts +4 -8
- package/dist/server/application.js +9 -156
- package/dist/server/dispatch.d.ts +5 -0
- package/dist/server/dispatch.js +50 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +2 -1
- package/dist/server/preact.js +2 -1
- package/dist/server/react.d.ts +10 -0
- package/dist/server/react.js +22 -0
- package/dist/server/render-application.d.ts +31 -0
- package/dist/server/render-application.js +118 -0
- package/dist/vite/application/lazy-files.d.ts +1 -1
- package/dist/vite/application/lazy-files.js +2 -2
- package/dist/vite/application/vite.d.ts +2 -1
- package/dist/vite/application/vite.js +12 -5
- package/dist/vite/react.d.ts +4 -0
- package/dist/vite/react.js +9 -1
- package/jsr.json +10 -6
- package/package.json +18 -10
- package/.generated/core/resource-types.ts +0 -48
- package/.generated/core/resources.ts +0 -446
- package/.generated/react/data.ts +0 -29
- package/.generated/react/resources/components.tsx +0 -416
- package/dist/core/resource-types.d.ts +0 -41
- package/dist/core/resources.d.ts +0 -21
- package/dist/core/resources.js +0 -347
- package/dist/react/data.d.ts +0 -9
- package/dist/react/data.js +0 -3
- package/dist/react/link.js +0 -27
- package/dist/react/resources/components.d.ts +0 -76
- package/dist/react/resources/components.js +0 -250
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React document metadata, resource links and hydration scripts.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
/** @jsxImportSource react */
|
|
7
|
+
|
|
8
|
+
import { applicationEnvironment } from "../core/environment.ts";
|
|
9
|
+
import { scrollPositions } from "../core/navigation.ts";
|
|
10
|
+
import { serializeData } from "../core/serialize.ts";
|
|
11
|
+
import type { ComponentProps } from "react";
|
|
12
|
+
import {
|
|
13
|
+
createElement,
|
|
14
|
+
Fragment,
|
|
15
|
+
useEffect,
|
|
16
|
+
useLayoutEffect,
|
|
17
|
+
useRef,
|
|
18
|
+
useState,
|
|
19
|
+
} from "react";
|
|
20
|
+
import { useNavigationSnapshot } from "./navigation-state.ts";
|
|
21
|
+
import { useApplication } from "./root-context.ts";
|
|
22
|
+
import { useSnapshot } from "./route-hooks.ts";
|
|
23
|
+
import type { MetaDescriptor } from "./route-types.ts";
|
|
24
|
+
|
|
25
|
+
function renderMeta(item: MetaDescriptor, index: number) {
|
|
26
|
+
if (typeof item.title === "string")
|
|
27
|
+
return <title key={index}>{item.title}</title>;
|
|
28
|
+
|
|
29
|
+
if ("script:ld+json" in item)
|
|
30
|
+
return (
|
|
31
|
+
<script
|
|
32
|
+
key={index}
|
|
33
|
+
type="application/ld+json"
|
|
34
|
+
dangerouslySetInnerHTML={{
|
|
35
|
+
__html: serializeData(item["script:ld+json"]),
|
|
36
|
+
}}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const { tagName, ...props } = item;
|
|
41
|
+
|
|
42
|
+
return createElement(tagName === "link" ? "link" : "meta", {
|
|
43
|
+
...props,
|
|
44
|
+
key: index,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Render root or active-route metadata in the document head. */
|
|
49
|
+
export function Meta(): import("react").ReactElement {
|
|
50
|
+
const { routes, rootModule } = useApplication();
|
|
51
|
+
const snapshot = useSnapshot();
|
|
52
|
+
const route = routes.find((item) => item.path === snapshot.path);
|
|
53
|
+
const args = {
|
|
54
|
+
loaderData: snapshot.data,
|
|
55
|
+
data: snapshot.data,
|
|
56
|
+
params: snapshot.params,
|
|
57
|
+
error: snapshot.error,
|
|
58
|
+
location: new URL(snapshot.url),
|
|
59
|
+
};
|
|
60
|
+
const values = route?.meta?.(args) ?? rootModule?.meta?.(args) ?? [];
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
const title = values.find((item) => typeof item.title === "string")?.title;
|
|
63
|
+
|
|
64
|
+
if (typeof title === "string") document.title = title;
|
|
65
|
+
}, [values]);
|
|
66
|
+
|
|
67
|
+
return <Fragment>{values.map(renderMeta)}</Fragment>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Render stylesheet and other link descriptors from the application root. */
|
|
71
|
+
export function Links(): import("react").ReactElement {
|
|
72
|
+
const { rootModule } = useApplication();
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<Fragment>
|
|
76
|
+
{rootModule?.links?.().map((props, index) => (
|
|
77
|
+
<link key={index} {...props} />
|
|
78
|
+
))}
|
|
79
|
+
</Fragment>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Nonce and browser entry options for the document hydration scripts. */
|
|
84
|
+
export interface ScriptProps {
|
|
85
|
+
nonce?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Render the hydration snapshot and browser entry script with an optional CSP nonce. */
|
|
89
|
+
export function Scripts({ nonce }: ScriptProps): import("react").ReactElement {
|
|
90
|
+
const { root, rootError, router, entry, streaming, initialData } =
|
|
91
|
+
useApplication();
|
|
92
|
+
// The bootstrap payload describes this document's initial SSR snapshot.
|
|
93
|
+
// Navigation and revalidation update the router, not the hydration script.
|
|
94
|
+
const [json] = useState(() => {
|
|
95
|
+
if (initialData !== undefined) return initialData;
|
|
96
|
+
if (streaming) return "";
|
|
97
|
+
|
|
98
|
+
const current = router.getSnapshot();
|
|
99
|
+
const snapshot = {
|
|
100
|
+
...current,
|
|
101
|
+
error: current.error && {
|
|
102
|
+
...current.error,
|
|
103
|
+
name: current.error.name,
|
|
104
|
+
message: current.error.message,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return serializeData({ root, snapshot, ...(rootError && { rootError }) });
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<Fragment>
|
|
113
|
+
<script
|
|
114
|
+
{...(nonce && { nonce })}
|
|
115
|
+
dangerouslySetInnerHTML={{
|
|
116
|
+
__html: applicationEnvironment?.entryScript ?? "",
|
|
117
|
+
}}
|
|
118
|
+
/>
|
|
119
|
+
<script
|
|
120
|
+
id="smolter-data"
|
|
121
|
+
type="application/json"
|
|
122
|
+
{...(nonce && { nonce })}
|
|
123
|
+
dangerouslySetInnerHTML={{ __html: json }}
|
|
124
|
+
/>
|
|
125
|
+
{entry && (
|
|
126
|
+
<script
|
|
127
|
+
type="module"
|
|
128
|
+
async={initialData !== undefined}
|
|
129
|
+
src={entry}
|
|
130
|
+
{...(nonce && { nonce })}
|
|
131
|
+
/>
|
|
132
|
+
)}
|
|
133
|
+
</Fragment>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Reset scroll after navigation unless the navigation requests preservation. */
|
|
138
|
+
export function ScrollRestoration(_props: ScriptProps): null {
|
|
139
|
+
const { router } = useApplication();
|
|
140
|
+
const { url, state } = useSnapshot();
|
|
141
|
+
const navigation = useNavigationSnapshot(router);
|
|
142
|
+
const positions = scrollPositions(router);
|
|
143
|
+
const restoredLocation = useRef("");
|
|
144
|
+
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
const previous = history.scrollRestoration;
|
|
147
|
+
history.scrollRestoration = "manual";
|
|
148
|
+
|
|
149
|
+
return () => {
|
|
150
|
+
history.scrollRestoration = previous;
|
|
151
|
+
};
|
|
152
|
+
}, []);
|
|
153
|
+
|
|
154
|
+
useLayoutEffect(() => {
|
|
155
|
+
if (state !== "idle") return;
|
|
156
|
+
|
|
157
|
+
if ((history.state?.key ?? "initial") !== navigation.key) return;
|
|
158
|
+
|
|
159
|
+
const locationKey = `${navigation.key}:${url}`;
|
|
160
|
+
|
|
161
|
+
if (restoredLocation.current === locationKey) return;
|
|
162
|
+
|
|
163
|
+
// Lazy views subscribe independently; restore after their DOM is committed.
|
|
164
|
+
const frame = requestAnimationFrame(() => {
|
|
165
|
+
restoredLocation.current = locationKey;
|
|
166
|
+
const target = new URL(location.href);
|
|
167
|
+
const saved = navigation.type === "POP" && positions.get(navigation.key);
|
|
168
|
+
|
|
169
|
+
if (!saved && target.hash) {
|
|
170
|
+
document.getElementById(target.hash.slice(1))?.scrollIntoView();
|
|
171
|
+
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!saved && navigation.preventScrollReset) return;
|
|
176
|
+
|
|
177
|
+
const [left, top] = saved || [0, 0];
|
|
178
|
+
window.scrollTo({ left, top, behavior: "instant" });
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
return () => cancelAnimationFrame(frame);
|
|
182
|
+
}, [
|
|
183
|
+
url,
|
|
184
|
+
state,
|
|
185
|
+
positions,
|
|
186
|
+
navigation.key,
|
|
187
|
+
navigation.type,
|
|
188
|
+
navigation.preventScrollReset,
|
|
189
|
+
]);
|
|
190
|
+
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** HTML attributes for server markup and client document reconciliation. */
|
|
195
|
+
export interface DocumentProps extends ComponentProps<"html"> {
|
|
196
|
+
"data-debug"?: boolean;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Render the server document and preserve client-managed theme classes on hydration. */
|
|
200
|
+
export function Document(props: DocumentProps): import("react").ReactElement {
|
|
201
|
+
const { children, ...attributes } = props;
|
|
202
|
+
const className = String(props.className ?? "");
|
|
203
|
+
const previousClassName = useRef(className);
|
|
204
|
+
const lang = String(props.lang ?? "en");
|
|
205
|
+
const debug = String(props["data-debug"] ?? false);
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
// Hydration must preserve classes already applied by the browser theme store.
|
|
208
|
+
// Only reconcile classes when the document's own props actually change.
|
|
209
|
+
if (previousClassName.current !== className) {
|
|
210
|
+
const previous = previousClassName.current.split(/\s+/).filter(Boolean);
|
|
211
|
+
const next = className.split(/\s+/).filter(Boolean);
|
|
212
|
+
document.documentElement.classList.remove(...previous);
|
|
213
|
+
document.documentElement.classList.add(...next);
|
|
214
|
+
previousClassName.current = className;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
document.documentElement.lang = lang;
|
|
218
|
+
document.documentElement.dataset.debug = debug;
|
|
219
|
+
}, [className, lang, debug]);
|
|
220
|
+
|
|
221
|
+
const classes =
|
|
222
|
+
typeof document !== "undefined" && previousClassName.current !== className
|
|
223
|
+
? [...document.documentElement.classList]
|
|
224
|
+
.filter(
|
|
225
|
+
(value) => !previousClassName.current.split(/\s+/).includes(value),
|
|
226
|
+
)
|
|
227
|
+
.concat(className.split(/\s+/).filter(Boolean))
|
|
228
|
+
.join(" ")
|
|
229
|
+
: props.className;
|
|
230
|
+
return (
|
|
231
|
+
<html {...attributes} className={classes} suppressHydrationWarning>
|
|
232
|
+
{children}
|
|
233
|
+
</html>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
@@ -14,8 +14,15 @@ import {
|
|
|
14
14
|
type Router,
|
|
15
15
|
type SubmitOptions,
|
|
16
16
|
} from "../core/index.ts";
|
|
17
|
+
import {
|
|
18
|
+
type Submission,
|
|
19
|
+
type SubmissionOptions,
|
|
20
|
+
submissionBody,
|
|
21
|
+
submissionSearch,
|
|
22
|
+
} from "../core/submission.ts";
|
|
17
23
|
import { useRouter } from "./context.ts";
|
|
18
24
|
import { Form, type FormProps } from "./form.ts";
|
|
25
|
+
import type { RouteData } from "./route-types.ts";
|
|
19
26
|
|
|
20
27
|
import {
|
|
21
28
|
type ComponentType,
|
|
@@ -26,7 +33,7 @@ import {
|
|
|
26
33
|
} from "./runtime.ts";
|
|
27
34
|
|
|
28
35
|
/** Action method and optional explicit route URL for a fetcher submission. */
|
|
29
|
-
export interface FetcherSubmitOptions extends
|
|
36
|
+
export interface FetcherSubmitOptions extends SubmissionOptions {
|
|
30
37
|
action?: string;
|
|
31
38
|
}
|
|
32
39
|
|
|
@@ -35,7 +42,7 @@ export interface TypedFetcher<T> extends Omit<FetcherSnapshot, "data"> {
|
|
|
35
42
|
data?: T;
|
|
36
43
|
Form: ComponentType<FormProps>;
|
|
37
44
|
load: (url?: string) => Promise<Result>;
|
|
38
|
-
submit: (body:
|
|
45
|
+
submit: (body: Submission, options?: FetcherSubmitOptions) => Promise<Result>;
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
/** Create a fetcher for independent route reads and mutations; its data stays local. */
|
|
@@ -47,9 +54,7 @@ export function useFetcher<T extends RouteModule>(
|
|
|
47
54
|
>;
|
|
48
55
|
|
|
49
56
|
/** Create a fetcher for independent route reads and mutations; its data stays local. */
|
|
50
|
-
export function useFetcher<
|
|
51
|
-
T extends AnyDataFunction = DataFunction,
|
|
52
|
-
>(): TypedFetcher<DataOf<T>>;
|
|
57
|
+
export function useFetcher<T = DataFunction>(): TypedFetcher<RouteData<T>>;
|
|
53
58
|
|
|
54
59
|
/** Create a fetcher for independent route reads and mutations; its data stays local. */
|
|
55
60
|
export function useFetcher(route?: RouteDefinition): TypedFetcher<unknown> {
|
|
@@ -68,8 +73,22 @@ export function useFetcher(route?: RouteDefinition): TypedFetcher<unknown> {
|
|
|
68
73
|
Form: (props: FormProps) =>
|
|
69
74
|
createElement(Form, { action: target(), ...props, fetcher: store }),
|
|
70
75
|
load: (url?: string) => store.load(url ?? target()),
|
|
71
|
-
submit: (body:
|
|
72
|
-
|
|
76
|
+
submit: (body: Submission, options: FetcherSubmitOptions = {}) => {
|
|
77
|
+
const action = options.action ?? target();
|
|
78
|
+
const data = submissionBody(body, options.encType);
|
|
79
|
+
const method = options.method?.toUpperCase();
|
|
80
|
+
|
|
81
|
+
if (method === "GET") {
|
|
82
|
+
const url = new URL(action, router.getSnapshot().url);
|
|
83
|
+
url.search = submissionSearch(data);
|
|
84
|
+
|
|
85
|
+
return store.load(url.href);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return store.submit(action, data, {
|
|
89
|
+
method: method as SubmitOptions["method"],
|
|
90
|
+
});
|
|
91
|
+
},
|
|
73
92
|
};
|
|
74
93
|
}, [router, path]);
|
|
75
94
|
|
|
@@ -83,3 +102,6 @@ export function useFetcher(route?: RouteDefinition): TypedFetcher<unknown> {
|
|
|
83
102
|
|
|
84
103
|
return { ...fetcher, ...state };
|
|
85
104
|
}
|
|
105
|
+
|
|
106
|
+
/** A fetcher with a bound form and inferred result data. */
|
|
107
|
+
export type FetcherWithComponents<T> = TypedFetcher<RouteData<T>>;
|
|
@@ -5,43 +5,43 @@
|
|
|
5
5
|
|
|
6
6
|
import type {
|
|
7
7
|
AnyDataFunction,
|
|
8
|
-
DataOf,
|
|
9
8
|
Route,
|
|
10
9
|
RouteModule,
|
|
11
10
|
Router,
|
|
12
11
|
Snapshot,
|
|
13
12
|
} from "../core/index.ts";
|
|
14
|
-
|
|
15
13
|
import { useRouter, useSnapshot } from "./context.ts";
|
|
14
|
+
import type { RouteData } from "./route-types.ts";
|
|
16
15
|
|
|
17
16
|
/** A route module or loader function used for data inference. */
|
|
18
17
|
export type LoaderSource = RouteModule | AnyDataFunction;
|
|
19
18
|
|
|
20
19
|
/** Infer server and client loader data from a route module or function. */
|
|
21
20
|
export type LoaderData<T> = T extends AnyDataFunction
|
|
22
|
-
?
|
|
21
|
+
? RouteData<T>
|
|
23
22
|
: T extends RouteModule
|
|
24
|
-
?
|
|
25
|
-
:
|
|
23
|
+
? RouteData<Extract<T["loader"] | T["clientLoader"], AnyDataFunction>>
|
|
24
|
+
: RouteData<T>;
|
|
26
25
|
|
|
27
26
|
/** Infer server and client action data from a route module or function. */
|
|
28
27
|
export type ActionData<T> = T extends AnyDataFunction
|
|
29
|
-
?
|
|
28
|
+
? RouteData<T>
|
|
30
29
|
: T extends RouteModule
|
|
31
|
-
?
|
|
32
|
-
:
|
|
30
|
+
? RouteData<Extract<T["action"] | T["clientAction"], AnyDataFunction>>
|
|
31
|
+
: RouteData<T>;
|
|
33
32
|
/** Pass a route for inference, or use useLoaderData<typeof loader>() in its module. */
|
|
34
|
-
export function useLoaderData<T
|
|
35
|
-
route?: T,
|
|
36
|
-
): LoaderData<T>
|
|
37
|
-
return useSnapshot(
|
|
38
|
-
|
|
39
|
-
| undefined
|
|
33
|
+
export function useLoaderData<T = RouteModule>(
|
|
34
|
+
route?: T & LoaderSource,
|
|
35
|
+
): LoaderData<T> {
|
|
36
|
+
return useSnapshot(
|
|
37
|
+
"data",
|
|
38
|
+
(route as Route | undefined)?.path,
|
|
39
|
+
) as LoaderData<T>;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/** Read the last page action result, optionally inferring its type from a route. */
|
|
43
|
-
export function useActionData<T
|
|
44
|
-
route?: T,
|
|
43
|
+
export function useActionData<T = RouteModule>(
|
|
44
|
+
route?: T & LoaderSource,
|
|
45
45
|
): ActionData<T> | undefined {
|
|
46
46
|
return useSnapshot("actionData", (route as Route | undefined)?.path) as
|
|
47
47
|
| ActionData<T>
|
|
@@ -49,8 +49,10 @@ export function useActionData<T extends LoaderSource = RouteModule>(
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/** Read path parameters for the active route. */
|
|
52
|
-
export function useParams
|
|
53
|
-
|
|
52
|
+
export function useParams<
|
|
53
|
+
T extends Snapshot["params"] = Snapshot["params"],
|
|
54
|
+
>(): T {
|
|
55
|
+
return useSnapshot("params") as T;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
/** Read the current path, search, and hash together with the navigate function. */
|
|
@@ -9,6 +9,13 @@ export {
|
|
|
9
9
|
createRouter,
|
|
10
10
|
defineRoute,
|
|
11
11
|
} from "../core/index.ts";
|
|
12
|
+
export type { ApplicationEnvironment } from "../core/environment.ts";
|
|
13
|
+
export * from "../core/responses.ts";
|
|
14
|
+
export {
|
|
15
|
+
readSubmission,
|
|
16
|
+
type Submission,
|
|
17
|
+
type SubmissionOptions,
|
|
18
|
+
} from "../core/submission.ts";
|
|
12
19
|
export {
|
|
13
20
|
RouterProvider,
|
|
14
21
|
type RouterProviderProps,
|
|
@@ -17,11 +24,20 @@ export {
|
|
|
17
24
|
export * from "./fetcher.ts";
|
|
18
25
|
export * from "./form.ts";
|
|
19
26
|
export * from "./hooks.ts";
|
|
20
|
-
export * from "./
|
|
27
|
+
export * from "./navigation.ts";
|
|
21
28
|
export {
|
|
22
29
|
Outlet,
|
|
23
30
|
type OutletProps,
|
|
24
31
|
useMatches,
|
|
25
32
|
useOutletContext,
|
|
26
33
|
} from "./outlet.ts";
|
|
34
|
+
export {
|
|
35
|
+
useNavigation,
|
|
36
|
+
useRootData,
|
|
37
|
+
useRouteError,
|
|
38
|
+
useRouteLoaderData,
|
|
39
|
+
useSubmit,
|
|
40
|
+
} from "./route-hooks.ts";
|
|
41
|
+
export * from "./route-types.ts";
|
|
42
|
+
export { type ApplicationProps, RouterApplication } from "./router.ts";
|
|
27
43
|
export * from "./routes.ts";
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* On-demand React route modules with shared intent prefetching.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import { serverRoute } from "../client/index.ts";
|
|
7
|
+
import type { DataArgs, RouteModule, Router } from "../core/index.ts";
|
|
8
|
+
import { useRouter } from "./context.ts";
|
|
9
|
+
import type { AppRoute } from "./route-types.ts";
|
|
10
|
+
import type { ErrorProps } from "./routes.ts";
|
|
11
|
+
import {
|
|
12
|
+
type ComponentType,
|
|
13
|
+
createElement,
|
|
14
|
+
useSyncExternalStore,
|
|
15
|
+
} from "./runtime.ts";
|
|
16
|
+
|
|
17
|
+
/** Lazy route path, server transport endpoint and available handler capabilities. */
|
|
18
|
+
export interface LazyRouteOptions {
|
|
19
|
+
path: string;
|
|
20
|
+
dataEndpoint?: string;
|
|
21
|
+
id?: string;
|
|
22
|
+
loader?: boolean;
|
|
23
|
+
action?: boolean;
|
|
24
|
+
clientAction?: boolean;
|
|
25
|
+
meta?: boolean;
|
|
26
|
+
shouldRevalidate?: boolean;
|
|
27
|
+
ErrorBoundary?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type LoadedRoute = RouteModule & Pick<AppRoute, "meta">;
|
|
31
|
+
|
|
32
|
+
interface LazyAppRoute extends AppRoute {
|
|
33
|
+
resolveComponent: () => ComponentType | undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// A shared wrapper preserves component identity across routes using the same view.
|
|
37
|
+
function LazyView() {
|
|
38
|
+
const router = useRouter<Router>();
|
|
39
|
+
const { path } = useSyncExternalStore(
|
|
40
|
+
router.subscribe,
|
|
41
|
+
router.getSnapshot,
|
|
42
|
+
router.getSnapshot,
|
|
43
|
+
);
|
|
44
|
+
const route = router.routes.find((item) => item.path === path) as
|
|
45
|
+
| LazyAppRoute
|
|
46
|
+
| undefined;
|
|
47
|
+
const Component = route?.resolveComponent();
|
|
48
|
+
|
|
49
|
+
return Component && createElement(Component, {});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Create a route that imports its module on demand and shares the import with intent prefetching. */
|
|
53
|
+
export function lazyRoute(
|
|
54
|
+
options: LazyRouteOptions,
|
|
55
|
+
importer: () => Promise<LoadedRoute>,
|
|
56
|
+
): LazyAppRoute {
|
|
57
|
+
let module: LoadedRoute | undefined;
|
|
58
|
+
let pending: Promise<void> | undefined;
|
|
59
|
+
|
|
60
|
+
function preload() {
|
|
61
|
+
pending ??= importer()
|
|
62
|
+
.then((loaded) => {
|
|
63
|
+
module = serverRoute(options.path, loaded, {
|
|
64
|
+
loader: options.loader,
|
|
65
|
+
action: options.action,
|
|
66
|
+
dataEndpoint: options.dataEndpoint,
|
|
67
|
+
});
|
|
68
|
+
})
|
|
69
|
+
.catch((error: unknown) => {
|
|
70
|
+
pending = undefined;
|
|
71
|
+
throw error;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return pending;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function run(args: DataArgs, action = false) {
|
|
78
|
+
await preload();
|
|
79
|
+
|
|
80
|
+
if (args.signal.aborted) return new Error("Navigation cancelled");
|
|
81
|
+
|
|
82
|
+
const handler = action
|
|
83
|
+
? (module!.clientAction ?? module!.action)
|
|
84
|
+
: (module!.clientLoader ?? module!.loader);
|
|
85
|
+
|
|
86
|
+
// serverRoute supplies serverLoader/serverAction to client handlers.
|
|
87
|
+
return (handler as ((args: DataArgs) => unknown) | undefined)?.(args);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function ErrorBoundary(props: ErrorProps) {
|
|
91
|
+
const Component = module?.ErrorBoundary as
|
|
92
|
+
| ComponentType<ErrorProps>
|
|
93
|
+
| undefined;
|
|
94
|
+
|
|
95
|
+
if (!Component) return props.error.message;
|
|
96
|
+
|
|
97
|
+
return createElement(Component, props);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
path: options.path,
|
|
102
|
+
id: options.id,
|
|
103
|
+
preload,
|
|
104
|
+
default: LazyView,
|
|
105
|
+
resolveComponent: () => module?.default as ComponentType | undefined,
|
|
106
|
+
clientLoader: (args) => run(args),
|
|
107
|
+
...((options.action || options.clientAction) && {
|
|
108
|
+
clientAction: (args: DataArgs) => run(args, true),
|
|
109
|
+
}),
|
|
110
|
+
...(options.meta && { meta: (args) => module?.meta?.(args) ?? [] }),
|
|
111
|
+
...(options.shouldRevalidate && {
|
|
112
|
+
shouldRevalidate: (args) => module?.shouldRevalidate?.(args) ?? true,
|
|
113
|
+
}),
|
|
114
|
+
...(options.ErrorBoundary && { ErrorBoundary }),
|
|
115
|
+
};
|
|
116
|
+
}
|
package/.generated/react/link.ts
CHANGED
|
@@ -1,38 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* React components and typed data hooks. Import from @kasperrt/smolter/react.
|
|
3
|
-
* @module
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { internalURL, navigationClick } from "../core/url.ts";
|
|
7
|
-
import { useRouter } from "./context.ts";
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
type AnchorHTMLAttributes,
|
|
11
|
-
createElement,
|
|
12
|
-
type MouseEvent,
|
|
13
|
-
type ReactNode,
|
|
14
|
-
} from "./runtime.ts";
|
|
15
|
-
|
|
16
|
-
/** Anchor attributes with internal navigation and optional history replacement. */
|
|
17
|
-
export interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
18
|
-
href: string;
|
|
19
|
-
replace?: boolean;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Render an anchor that handles internal navigation and preserves native modified clicks. */
|
|
23
|
-
export function Link(options: LinkProps): ReactNode {
|
|
24
|
-
const { href, replace, onClick, ...props } = options;
|
|
25
|
-
const router = useRouter();
|
|
26
|
-
const click = (event: MouseEvent<HTMLAnchorElement>) => {
|
|
27
|
-
onClick?.(event);
|
|
28
|
-
|
|
29
|
-
if (!navigationClick(event, event.currentTarget)) return;
|
|
30
|
-
|
|
31
|
-
if (!internalURL(router.getSnapshot().url, href)) return;
|
|
32
|
-
|
|
33
|
-
event.preventDefault();
|
|
34
|
-
void router.navigate(href, options);
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
return createElement("a", { ...props, href, onClick: click });
|
|
38
|
-
}
|
|
1
|
+
export { Link, type LinkProps } from "./navigation.ts";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Router } from "../core/index.ts";
|
|
2
|
+
import { navigationStore } from "../core/navigation.ts";
|
|
3
|
+
import { useSyncExternalStore } from "./runtime.ts";
|
|
4
|
+
|
|
5
|
+
export function useNavigationSnapshot(router: Router) {
|
|
6
|
+
const store = navigationStore(router);
|
|
7
|
+
|
|
8
|
+
return useSyncExternalStore(
|
|
9
|
+
store.subscribe,
|
|
10
|
+
store.getSnapshot,
|
|
11
|
+
store.getSnapshot,
|
|
12
|
+
);
|
|
13
|
+
}
|