@kasperrt/smolter 0.0.1 → 0.3.0
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/{src/internal → .generated}/client/index.ts +1 -0
- package/.generated/core/environment.ts +43 -0
- package/{src/internal → .generated}/core/errors.ts +1 -1
- package/{src/internal → .generated}/core/index.ts +2 -0
- package/.generated/core/navigation-router.ts +107 -0
- package/.generated/core/navigation.ts +147 -0
- package/.generated/core/presentation.ts +108 -0
- package/.generated/core/resource-types.ts +44 -0
- package/.generated/core/resources.ts +444 -0
- package/.generated/core/responses.ts +86 -0
- package/{src/internal → .generated}/core/router.ts +136 -63
- package/.generated/core/serialize.ts +20 -0
- package/.generated/core/store.ts +25 -0
- package/.generated/core/submission.ts +116 -0
- package/{src/internal → .generated}/core/types.ts +18 -1
- package/.generated/core/url.ts +29 -0
- package/.generated/core/utils.ts +5 -0
- package/.generated/preact/await.tsx +41 -0
- package/.generated/preact/client.tsx +93 -0
- package/.generated/preact/context.ts +59 -0
- package/.generated/preact/document.tsx +198 -0
- package/{src/internal → .generated}/preact/fetcher.ts +43 -23
- package/.generated/preact/form.ts +58 -0
- package/{src/internal → .generated}/preact/hooks.ts +25 -28
- package/.generated/preact/index.ts +34 -0
- package/.generated/preact/lazy-route.ts +100 -0
- package/.generated/preact/navigation-state.ts +9 -0
- package/.generated/preact/navigation.tsx +147 -0
- package/.generated/preact/root-context.ts +21 -0
- package/.generated/preact/root-data.ts +45 -0
- package/.generated/preact/route-hooks.tsx +170 -0
- package/.generated/preact/route-types.ts +76 -0
- package/.generated/preact/router.tsx +107 -0
- package/{src/internal → .generated}/preact/routes.ts +17 -9
- package/.generated/react/context.ts +59 -0
- package/.generated/react/data.ts +25 -0
- package/{src/internal → .generated}/react/fetcher.ts +13 -11
- package/.generated/react/form.ts +58 -0
- package/{src/internal → .generated}/react/hooks.ts +9 -14
- package/{src/internal/preact → .generated/react}/index.ts +5 -1
- package/{src/internal → .generated}/react/link.ts +13 -21
- package/.generated/react/resources/components.tsx +404 -0
- package/{src/internal → .generated}/react/routes.ts +17 -9
- package/.generated/server/application.tsx +185 -0
- package/.generated/server/assets.ts +19 -0
- package/.generated/server/handler.ts +69 -0
- package/.generated/server/http.ts +55 -0
- package/{src/internal → .generated}/server/index.ts +4 -17
- package/.generated/server/node.ts +4 -0
- package/.generated/server/preact.ts +7 -0
- package/.generated/vite/application/browser-boundary.ts +42 -0
- package/.generated/vite/application/lazy-files.ts +136 -0
- package/.generated/vite/application/vite.ts +144 -0
- package/{src/internal → .generated}/vite/index.ts +4 -2
- package/.generated/vite/preact.ts +24 -0
- package/.generated/vite/react.ts +18 -0
- package/{src/internal → .generated}/vite/transport.ts +2 -1
- package/CHANGELOG.md +54 -0
- package/README.md +269 -55
- package/dist/client/deserialize.js +10 -0
- package/dist/{internal/client → client}/index.d.ts +1 -0
- package/dist/client/index.js +46 -0
- package/dist/core/environment.d.ts +12 -0
- package/dist/core/environment.js +23 -0
- package/dist/core/errors.js +7 -0
- package/dist/{internal/core → core}/index.d.ts +2 -0
- package/dist/core/match.js +33 -0
- package/dist/core/navigation-router.d.ts +15 -0
- package/dist/core/navigation-router.js +64 -0
- package/dist/core/navigation.d.ts +15 -0
- package/dist/core/navigation.js +92 -0
- package/dist/core/presentation.d.ts +5 -0
- package/dist/core/presentation.js +69 -0
- package/dist/core/resource-types.d.ts +37 -0
- package/dist/core/resources.d.ts +19 -0
- package/dist/core/resources.js +345 -0
- package/dist/core/responses.d.ts +15 -0
- package/dist/core/responses.js +48 -0
- package/dist/{internal/core → core}/router.d.ts +1 -1
- package/dist/core/router.js +204 -0
- package/dist/core/serialize.d.ts +5 -0
- package/dist/core/serialize.js +17 -0
- package/dist/core/store.d.ts +6 -0
- package/dist/core/store.js +21 -0
- package/dist/core/submission.d.ts +30 -0
- package/dist/core/submission.js +41 -0
- package/dist/{internal/core → core}/types.d.ts +16 -1
- package/dist/core/url.d.ts +7 -0
- package/dist/core/url.js +12 -0
- package/dist/core/utils.d.ts +5 -0
- package/dist/core/utils.js +5 -0
- package/dist/files/index.js +64 -0
- package/dist/preact/await.d.ts +7 -0
- package/dist/preact/await.js +19 -0
- package/dist/preact/client.d.ts +7 -0
- package/dist/preact/client.js +67 -0
- package/dist/preact/context.d.ts +18 -0
- package/dist/preact/context.js +23 -0
- package/dist/preact/document.d.ts +15 -0
- package/dist/preact/document.js +155 -0
- package/dist/{internal/preact → preact}/fetcher.d.ts +9 -5
- package/dist/preact/fetcher.js +45 -0
- package/dist/{internal/preact → preact}/form.d.ts +5 -6
- package/dist/preact/form.js +21 -0
- package/dist/{internal/preact → preact}/hooks.d.ts +7 -6
- package/dist/preact/hooks.js +25 -0
- package/dist/preact/index.d.ts +18 -0
- package/dist/preact/index.js +15 -0
- package/dist/preact/lazy-route.d.ts +19 -0
- package/dist/preact/lazy-route.js +51 -0
- package/dist/preact/navigation-state.d.ts +2 -0
- package/dist/preact/navigation-state.js +9 -0
- package/dist/preact/navigation.d.ts +28 -0
- package/dist/preact/navigation.js +60 -0
- package/dist/preact/root-context.d.ts +13 -0
- package/dist/preact/root-context.js +9 -0
- package/dist/preact/root-data.d.ts +8 -0
- package/dist/preact/root-data.js +28 -0
- package/dist/preact/route-hooks.d.ts +32 -0
- package/dist/preact/route-hooks.js +94 -0
- package/dist/preact/route-types.d.ts +54 -0
- package/dist/preact/router.d.ts +16 -0
- package/dist/preact/router.js +63 -0
- package/dist/{internal/preact → preact}/routes.d.ts +1 -1
- package/dist/preact/routes.js +12 -0
- package/dist/{internal/preact → preact}/runtime.js +1 -1
- package/dist/react/context.d.ts +18 -0
- package/dist/react/context.js +23 -0
- package/dist/react/data.d.ts +5 -0
- package/dist/react/data.js +3 -0
- package/dist/{internal/react → react}/fetcher.d.ts +2 -2
- package/dist/react/fetcher.js +34 -0
- package/dist/{internal/react → react}/form.d.ts +5 -6
- package/dist/react/form.js +21 -0
- package/dist/react/hooks.js +25 -0
- package/dist/{internal/react → react}/index.d.ts +1 -1
- package/dist/react/index.js +10 -0
- package/dist/{internal/react → react}/link.d.ts +2 -2
- package/dist/react/link.js +27 -0
- package/dist/react/resources/components.d.ts +64 -0
- package/dist/react/resources/components.js +239 -0
- package/dist/{internal/react → react}/routes.d.ts +1 -1
- package/dist/react/routes.js +12 -0
- package/dist/react/runtime.js +1 -0
- package/dist/server/application.d.ts +4 -0
- package/dist/server/application.js +119 -0
- package/dist/server/assets.d.ts +5 -0
- package/dist/server/assets.js +7 -0
- package/dist/server/handler.d.ts +19 -0
- package/dist/server/handler.js +22 -0
- package/dist/server/http.d.ts +4 -0
- package/dist/server/http.js +27 -0
- package/dist/{internal/server → server}/index.d.ts +2 -3
- package/dist/server/index.js +76 -0
- package/dist/server/node.d.ts +3 -0
- package/dist/server/node.js +3 -0
- package/dist/server/preact.d.ts +3 -0
- package/dist/server/preact.js +4 -0
- package/dist/vite/application/browser-boundary.d.ts +3 -0
- package/dist/vite/application/browser-boundary.js +35 -0
- package/dist/vite/application/lazy-files.d.ts +2 -0
- package/dist/vite/application/lazy-files.js +87 -0
- package/dist/vite/application/vite.d.ts +9 -0
- package/dist/vite/application/vite.js +101 -0
- package/dist/{internal/vite → vite}/index.d.ts +2 -1
- package/dist/vite/index.js +104 -0
- package/dist/vite/preact.d.ts +11 -0
- package/dist/vite/preact.js +15 -0
- package/dist/vite/react.d.ts +9 -0
- package/dist/vite/react.js +14 -0
- package/dist/{internal/vite → vite}/transport.d.ts +1 -1
- package/dist/vite/transport.js +35 -0
- package/jsr.json +28 -11
- package/package.json +119 -38
- package/dist/client.d.ts +0 -2
- package/dist/client.js +0 -2
- package/dist/core.d.ts +0 -2
- package/dist/core.js +0 -2
- package/dist/files.d.ts +0 -2
- package/dist/files.js +0 -2
- package/dist/internal/client/deserialize.js +0 -11
- package/dist/internal/client/index.js +0 -44
- package/dist/internal/core/errors.js +0 -6
- package/dist/internal/core/index.js +0 -6
- package/dist/internal/core/match.js +0 -27
- package/dist/internal/core/router.js +0 -189
- package/dist/internal/core/types.js +0 -5
- package/dist/internal/core/url.d.ts +0 -2
- package/dist/internal/core/url.js +0 -6
- package/dist/internal/files/index.js +0 -77
- package/dist/internal/preact/context.d.ts +0 -18
- package/dist/internal/preact/context.js +0 -20
- package/dist/internal/preact/fetcher.js +0 -28
- package/dist/internal/preact/form.js +0 -41
- package/dist/internal/preact/hooks.js +0 -35
- package/dist/internal/preact/index.d.ts +0 -9
- package/dist/internal/preact/index.js +0 -7
- package/dist/internal/preact/link.d.ts +0 -12
- package/dist/internal/preact/link.js +0 -28
- package/dist/internal/preact/routes.js +0 -22
- package/dist/internal/react/context.d.ts +0 -18
- package/dist/internal/react/context.js +0 -20
- package/dist/internal/react/fetcher.js +0 -28
- package/dist/internal/react/form.js +0 -41
- package/dist/internal/react/hooks.js +0 -35
- package/dist/internal/react/index.js +0 -7
- package/dist/internal/react/link.js +0 -28
- package/dist/internal/react/routes.js +0 -22
- package/dist/internal/react/runtime.js +0 -2
- package/dist/internal/server/index.js +0 -81
- package/dist/internal/vite/index.js +0 -132
- package/dist/internal/vite/transport.js +0 -42
- package/dist/preact.d.ts +0 -2
- package/dist/preact.js +0 -2
- package/dist/react.d.ts +0 -2
- package/dist/react.js +0 -2
- package/dist/server.d.ts +0 -2
- package/dist/server.js +0 -2
- package/dist/vite.d.ts +0 -2
- package/dist/vite.js +0 -2
- package/src/client.ts +0 -2
- package/src/core.ts +0 -2
- package/src/files.ts +0 -2
- package/src/internal/core/url.ts +0 -6
- package/src/internal/preact/context.ts +0 -46
- package/src/internal/preact/form.ts +0 -79
- package/src/internal/preact/link.ts +0 -46
- package/src/internal/react/context.ts +0 -46
- package/src/internal/react/form.ts +0 -79
- package/src/internal/react/index.ts +0 -13
- package/src/preact.ts +0 -2
- package/src/react.ts +0 -2
- package/src/server.ts +0 -2
- package/src/vite.ts +0 -2
- /package/{src/internal → .generated}/client/deserialize.ts +0 -0
- /package/{src/internal → .generated}/core/match.ts +0 -0
- /package/{src/internal → .generated}/files/index.ts +0 -0
- /package/{src/internal → .generated}/preact/runtime.ts +0 -0
- /package/{src/internal → .generated}/react/runtime.ts +0 -0
- /package/dist/{internal/client → client}/deserialize.d.ts +0 -0
- /package/dist/{internal/core → core}/errors.d.ts +0 -0
- /package/dist/{internal/core → core}/match.d.ts +0 -0
- /package/dist/{internal/files → files}/index.d.ts +0 -0
- /package/dist/{internal/preact → preact}/runtime.d.ts +0 -0
- /package/dist/{internal/react → react}/hooks.d.ts +0 -0
- /package/dist/{internal/react → react}/runtime.d.ts +0 -0
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
DataFunction,
|
|
5
5
|
NavigateOptions,
|
|
6
6
|
Result,
|
|
7
|
+
RevalidationArgs,
|
|
7
8
|
Route,
|
|
8
9
|
Router,
|
|
9
10
|
RouterOptions,
|
|
@@ -14,19 +15,25 @@ import { internalURL } from "./url.ts";
|
|
|
14
15
|
|
|
15
16
|
type ChannelSnapshot = Partial<Snapshot> & Pick<Snapshot, "state">;
|
|
16
17
|
|
|
17
|
-
interface OperationOptions extends SubmitOptions, NavigateOptions {
|
|
18
|
-
|
|
18
|
+
interface OperationOptions extends SubmitOptions, NavigateOptions {}
|
|
19
|
+
|
|
20
|
+
interface RevalidationTask {
|
|
21
|
+
revalidate: (
|
|
22
|
+
args: RevalidationArgs,
|
|
23
|
+
targets?: readonly string[],
|
|
24
|
+
) => Promise<unknown>;
|
|
25
|
+
dispose?: () => void;
|
|
19
26
|
}
|
|
20
27
|
|
|
21
28
|
/** Create an isolated router. Use browser: true for browser history, or loadRouter for SSR. */
|
|
22
|
-
export function
|
|
29
|
+
export function createDataRouter(
|
|
23
30
|
routes: readonly Route[],
|
|
24
31
|
options: RouterOptions = {},
|
|
25
32
|
): Router {
|
|
26
33
|
const browser = options.browser && typeof window !== "undefined";
|
|
27
|
-
|
|
34
|
+
const hydrated = options.hydrationData?.state === "idle";
|
|
28
35
|
const initial: Snapshot = {
|
|
29
|
-
loaded:
|
|
36
|
+
loaded: hydrated,
|
|
30
37
|
...(options.hydrationData ?? {
|
|
31
38
|
url: new URL(
|
|
32
39
|
options.url ?? (browser ? location.href : "/"),
|
|
@@ -36,18 +43,15 @@ export function createRouter(
|
|
|
36
43
|
state: "idle",
|
|
37
44
|
}),
|
|
38
45
|
};
|
|
39
|
-
const
|
|
40
|
-
const channel = <S extends ChannelSnapshot>(initial: S, local
|
|
46
|
+
const revalidators = new Set<RevalidationTask>();
|
|
47
|
+
const channel = <S extends ChannelSnapshot>(initial: S, local?: boolean) => {
|
|
41
48
|
let snapshot = initial;
|
|
49
|
+
let loadedURL: string | undefined;
|
|
50
|
+
let submission: FormData | undefined;
|
|
42
51
|
let controller: AbortController | undefined;
|
|
43
52
|
const listeners = new Set<() => void>();
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
controller?.abort();
|
|
47
|
-
listeners.clear();
|
|
48
|
-
};
|
|
49
|
-
const emit = (next: Partial<Snapshot>) => {
|
|
50
|
-
snapshot = { ...snapshot, ...next };
|
|
53
|
+
const emit = (state: Snapshot["state"], next?: Partial<Snapshot>) => {
|
|
54
|
+
snapshot = { ...snapshot, ...next, state };
|
|
51
55
|
|
|
52
56
|
for (const listener of listeners) listener();
|
|
53
57
|
};
|
|
@@ -55,16 +59,19 @@ export function createRouter(
|
|
|
55
59
|
href: string,
|
|
56
60
|
body?: FormData,
|
|
57
61
|
operation: OperationOptions = {},
|
|
58
|
-
// true pushes history; false handles popstate
|
|
62
|
+
// Only loader navigations set this: true pushes history; false handles popstate.
|
|
59
63
|
navigation?: boolean,
|
|
64
|
+
redirects = 10,
|
|
60
65
|
): Promise<Result> => {
|
|
61
66
|
controller?.abort();
|
|
62
67
|
|
|
63
68
|
controller = new AbortController();
|
|
64
69
|
|
|
65
70
|
const { signal } = controller;
|
|
66
|
-
|
|
71
|
+
const { method = "POST" } = operation;
|
|
72
|
+
revalidators.add(task);
|
|
67
73
|
|
|
74
|
+
submission = body;
|
|
68
75
|
const previous = snapshot;
|
|
69
76
|
const url = internalURL(main.getSnapshot().url, href);
|
|
70
77
|
const match = url && matchRoutes(routes, url.pathname);
|
|
@@ -81,6 +88,7 @@ export function createRouter(
|
|
|
81
88
|
"",
|
|
82
89
|
url,
|
|
83
90
|
);
|
|
91
|
+
|
|
84
92
|
const sameRoute = snapshot.path === match?.route.path;
|
|
85
93
|
|
|
86
94
|
snapshot = {
|
|
@@ -89,20 +97,19 @@ export function createRouter(
|
|
|
89
97
|
path: match?.route.path,
|
|
90
98
|
params: match?.params ?? {},
|
|
91
99
|
actionData: undefined,
|
|
92
|
-
|
|
93
|
-
loaded: sameRoute && snapshot.loaded,
|
|
100
|
+
...(!sameRoute && { data: undefined, loaded: false }),
|
|
94
101
|
};
|
|
95
102
|
};
|
|
96
103
|
|
|
97
104
|
if (!body) commit();
|
|
98
|
-
|
|
105
|
+
|
|
106
|
+
emit(body ? "submitting" : "loading", { error: undefined });
|
|
99
107
|
|
|
100
108
|
const result = await safeWrapAsync(() => {
|
|
101
|
-
if (!match) return
|
|
109
|
+
if (!match) return Error(url ? "No route" : "External URL");
|
|
102
110
|
|
|
103
111
|
if (
|
|
104
112
|
navigation !== undefined &&
|
|
105
|
-
!body &&
|
|
106
113
|
previous.loaded &&
|
|
107
114
|
!previous.error &&
|
|
108
115
|
previous.path === match.route.path &&
|
|
@@ -120,12 +127,12 @@ export function createRouter(
|
|
|
120
127
|
match.route[body ? "clientAction" : "clientLoader"] ||
|
|
121
128
|
match.route[body ? "action" : "loader"];
|
|
122
129
|
|
|
123
|
-
if (!fn) return body &&
|
|
130
|
+
if (!fn) return body && Error("No action");
|
|
124
131
|
|
|
125
132
|
return (fn as DataFunction)({
|
|
126
133
|
request: new Request(url, {
|
|
127
134
|
signal,
|
|
128
|
-
method: body &&
|
|
135
|
+
method: body && method,
|
|
129
136
|
body,
|
|
130
137
|
}),
|
|
131
138
|
params: match.params,
|
|
@@ -138,48 +145,88 @@ export function createRouter(
|
|
|
138
145
|
const redirectTo =
|
|
139
146
|
result.data instanceof Response && result.data.headers.get("Location");
|
|
140
147
|
|
|
141
|
-
if (redirectTo &&
|
|
148
|
+
if (redirectTo && redirects) {
|
|
142
149
|
await main.load(
|
|
143
150
|
new URL(redirectTo, url!).href,
|
|
144
151
|
undefined,
|
|
145
|
-
{
|
|
146
|
-
replace: true,
|
|
147
|
-
hops: (operation.hops ?? 0) + 1,
|
|
148
|
-
},
|
|
152
|
+
{ replace: true },
|
|
149
153
|
true,
|
|
154
|
+
redirects - 1,
|
|
150
155
|
);
|
|
151
156
|
|
|
152
|
-
if (!signal.aborted)
|
|
157
|
+
if (!signal.aborted) {
|
|
158
|
+
submission = undefined;
|
|
159
|
+
emit("idle");
|
|
160
|
+
}
|
|
153
161
|
|
|
154
162
|
return result;
|
|
155
163
|
}
|
|
156
164
|
|
|
157
|
-
if (redirectTo) result.error =
|
|
165
|
+
if (redirectTo) result.error = Error("Redirect loop");
|
|
166
|
+
|
|
167
|
+
if (!body && !result.error) loadedURL = url!.href;
|
|
158
168
|
|
|
159
169
|
if (body && !result.error) commit();
|
|
160
170
|
|
|
161
|
-
emit({
|
|
171
|
+
emit(body && !result.error ? "loading" : "idle", {
|
|
162
172
|
[body && !local ? "actionData" : "data"]: result.data,
|
|
163
173
|
error: result.error,
|
|
164
174
|
loaded: body ? snapshot.loaded : !result.error,
|
|
165
|
-
state: body && !result.error ? "loading" : "idle",
|
|
166
175
|
});
|
|
167
176
|
|
|
168
177
|
if (body && !result.error) {
|
|
169
|
-
await router
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
178
|
+
await router
|
|
179
|
+
.revalidate("action", {
|
|
180
|
+
formMethod: method,
|
|
181
|
+
formAction: url!.href,
|
|
182
|
+
formData: body,
|
|
183
|
+
actionResult: result.data,
|
|
184
|
+
})
|
|
185
|
+
.finally(() => {
|
|
186
|
+
if (!signal.aborted) {
|
|
187
|
+
submission = undefined;
|
|
188
|
+
emit("idle");
|
|
189
|
+
}
|
|
190
|
+
});
|
|
175
191
|
|
|
176
|
-
|
|
192
|
+
return result;
|
|
177
193
|
}
|
|
178
194
|
|
|
195
|
+
if (!signal.aborted) submission = undefined;
|
|
196
|
+
|
|
179
197
|
return result;
|
|
180
198
|
};
|
|
199
|
+
const task = {
|
|
200
|
+
revalidate: async (
|
|
201
|
+
args: RevalidationArgs,
|
|
202
|
+
targets?: readonly string[],
|
|
203
|
+
) => {
|
|
204
|
+
if (local && (!loadedURL || submission)) return;
|
|
205
|
+
|
|
206
|
+
const url = new URL(local ? loadedURL! : main.getSnapshot().url);
|
|
207
|
+
const route = matchRoutes(routes, url.pathname)?.route;
|
|
208
|
+
|
|
209
|
+
// Targets are strings, so an absent route ID cannot match one.
|
|
210
|
+
if (
|
|
211
|
+
!route ||
|
|
212
|
+
(targets &&
|
|
213
|
+
!targets.includes(route.id!) &&
|
|
214
|
+
!targets.includes(route.path))
|
|
215
|
+
)
|
|
216
|
+
return;
|
|
181
217
|
|
|
182
|
-
|
|
218
|
+
if (
|
|
219
|
+
route.shouldRevalidate?.({
|
|
220
|
+
...args,
|
|
221
|
+
url,
|
|
222
|
+
currentUrl: url,
|
|
223
|
+
nextUrl: url,
|
|
224
|
+
}) === false
|
|
225
|
+
)
|
|
226
|
+
return;
|
|
227
|
+
|
|
228
|
+
return run(url.href);
|
|
229
|
+
},
|
|
183
230
|
getSnapshot: () => snapshot,
|
|
184
231
|
subscribe(listener: () => void) {
|
|
185
232
|
listeners.add(listener);
|
|
@@ -190,48 +237,74 @@ export function createRouter(
|
|
|
190
237
|
},
|
|
191
238
|
load: run,
|
|
192
239
|
submit: run,
|
|
193
|
-
dispose
|
|
240
|
+
dispose() {
|
|
241
|
+
controller?.abort();
|
|
242
|
+
listeners.clear();
|
|
243
|
+
revalidators.delete(task);
|
|
244
|
+
},
|
|
194
245
|
};
|
|
246
|
+
revalidators.add(task);
|
|
247
|
+
|
|
248
|
+
return task;
|
|
195
249
|
};
|
|
196
250
|
const main = channel(initial);
|
|
197
251
|
let started = 0;
|
|
198
|
-
const pop = () =>
|
|
199
|
-
void main.load(location.href, undefined, undefined, false);
|
|
200
|
-
};
|
|
252
|
+
const pop = () => main.load(location.href, undefined, undefined, false);
|
|
201
253
|
const router: Router = {
|
|
202
254
|
...main,
|
|
203
255
|
routes,
|
|
204
256
|
start() {
|
|
205
257
|
if (!started++ && browser) addEventListener("popstate", pop);
|
|
206
258
|
|
|
207
|
-
|
|
208
|
-
|
|
259
|
+
// A changed snapshot means a page operation has already started.
|
|
260
|
+
if (!hydrated && main.getSnapshot() === initial)
|
|
209
261
|
void main.load(initial.url);
|
|
210
|
-
}
|
|
211
262
|
|
|
212
263
|
return () => {
|
|
213
264
|
if (!--started && browser) removeEventListener("popstate", pop);
|
|
214
265
|
};
|
|
215
266
|
},
|
|
216
267
|
async navigate(url, operation) {
|
|
217
|
-
initialized = true;
|
|
218
268
|
await main.load(url, undefined, operation, true);
|
|
219
269
|
},
|
|
220
|
-
async revalidate(
|
|
270
|
+
async revalidate(options = {}, context) {
|
|
271
|
+
const {
|
|
272
|
+
route,
|
|
273
|
+
reason = "manual",
|
|
274
|
+
...details
|
|
275
|
+
} = typeof options === "string"
|
|
276
|
+
? { reason: options, ...context }
|
|
277
|
+
: options;
|
|
221
278
|
const url = new URL(main.getSnapshot().url);
|
|
279
|
+
const args = {
|
|
280
|
+
...details,
|
|
281
|
+
url,
|
|
282
|
+
currentUrl: url,
|
|
283
|
+
nextUrl: url,
|
|
284
|
+
defaultShouldRevalidate: true,
|
|
285
|
+
reason,
|
|
286
|
+
};
|
|
287
|
+
const targets = typeof route === "string" ? [route] : route;
|
|
288
|
+
|
|
289
|
+
await Promise.all(
|
|
290
|
+
[...revalidators].map((refresh) => refresh.revalidate(args, targets)),
|
|
291
|
+
);
|
|
292
|
+
},
|
|
222
293
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
|
|
294
|
+
registerRevalidator(id, refresh) {
|
|
295
|
+
const task = {
|
|
296
|
+
revalidate: async (
|
|
297
|
+
args: RevalidationArgs,
|
|
298
|
+
targets?: readonly string[],
|
|
299
|
+
) => {
|
|
300
|
+
if (!targets || targets.includes(id)) return refresh(args);
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
revalidators.add(task);
|
|
304
|
+
|
|
305
|
+
return () => {
|
|
306
|
+
revalidators.delete(task);
|
|
307
|
+
};
|
|
235
308
|
},
|
|
236
309
|
|
|
237
310
|
createFetcher() {
|
|
@@ -239,9 +312,9 @@ export function createRouter(
|
|
|
239
312
|
},
|
|
240
313
|
|
|
241
314
|
dispose() {
|
|
242
|
-
|
|
315
|
+
for (const refresh of revalidators) refresh.dispose?.();
|
|
243
316
|
|
|
244
|
-
|
|
317
|
+
revalidators.clear();
|
|
245
318
|
|
|
246
319
|
if (browser) removeEventListener("popstate", pop);
|
|
247
320
|
},
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Snapshot } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
/** Safe inside a script element. Data must be JSON serializable; error stacks and causes are omitted. */
|
|
4
|
+
export function serialize(snapshot: Snapshot): string {
|
|
5
|
+
return serializeData({
|
|
6
|
+
...snapshot,
|
|
7
|
+
error: snapshot.error && {
|
|
8
|
+
name: snapshot.error.name,
|
|
9
|
+
message: snapshot.error.message,
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Encode JSON for embedding inside an HTML script element. */
|
|
15
|
+
export function serializeData(value: unknown): string {
|
|
16
|
+
return JSON.stringify(value)
|
|
17
|
+
.replace(/</g, "\\u003c")
|
|
18
|
+
.replace(/\u2028/g, "\\u2028")
|
|
19
|
+
.replace(/\u2029/g, "\\u2029");
|
|
20
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Small observable value shared by layout data and navigation state. */
|
|
2
|
+
export function createStore<T>(initial: T): {
|
|
3
|
+
getSnapshot: () => T;
|
|
4
|
+
subscribe(listener: () => void): () => void;
|
|
5
|
+
update(next: T): void;
|
|
6
|
+
} {
|
|
7
|
+
let value = initial;
|
|
8
|
+
const listeners = new Set<() => void>();
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
getSnapshot: () => value,
|
|
12
|
+
subscribe(listener: () => void) {
|
|
13
|
+
listeners.add(listener);
|
|
14
|
+
|
|
15
|
+
return () => {
|
|
16
|
+
listeners.delete(listener);
|
|
17
|
+
};
|
|
18
|
+
},
|
|
19
|
+
update(next: T) {
|
|
20
|
+
value = next;
|
|
21
|
+
|
|
22
|
+
for (const listener of listeners) listener();
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Fetcher,
|
|
3
|
+
NavigateOptions,
|
|
4
|
+
Router,
|
|
5
|
+
SubmitOptions,
|
|
6
|
+
} from "./types.ts";
|
|
7
|
+
import { internalURL } from "./url.ts";
|
|
8
|
+
|
|
9
|
+
/** Values accepted by programmatic route submissions. */
|
|
10
|
+
export type Submission = FormData | HTMLFormElement | Record<string, unknown>;
|
|
11
|
+
|
|
12
|
+
/** Destination and encoding for a programmatic route submission. */
|
|
13
|
+
export interface SubmissionOptions {
|
|
14
|
+
action?: string;
|
|
15
|
+
encType?: string;
|
|
16
|
+
method?: string;
|
|
17
|
+
replace?: boolean;
|
|
18
|
+
preventScrollReset?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function formData(value: Submission): FormData {
|
|
22
|
+
if (value instanceof FormData) return value;
|
|
23
|
+
|
|
24
|
+
if (
|
|
25
|
+
typeof HTMLFormElement !== "undefined" &&
|
|
26
|
+
value instanceof HTMLFormElement
|
|
27
|
+
)
|
|
28
|
+
return new FormData(value);
|
|
29
|
+
|
|
30
|
+
const body = new FormData();
|
|
31
|
+
|
|
32
|
+
for (const [key, field] of Object.entries(value))
|
|
33
|
+
body.append(key, String(field));
|
|
34
|
+
|
|
35
|
+
return body;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function submissionBody(value: Submission, encoding?: string): FormData {
|
|
39
|
+
return formData(
|
|
40
|
+
encoding === "application/json"
|
|
41
|
+
? { _payload: JSON.stringify(value) }
|
|
42
|
+
: value,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Encode successful form controls for a GET route request. */
|
|
47
|
+
export function submissionSearch(body: FormData): string {
|
|
48
|
+
const search = new URLSearchParams();
|
|
49
|
+
|
|
50
|
+
for (const [key, value] of body)
|
|
51
|
+
search.append(key, typeof value === "string" ? value : value.name);
|
|
52
|
+
|
|
53
|
+
return String(search);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Read the JSON payload of a programmatic submission. */
|
|
57
|
+
export async function readSubmission(request: Request): Promise<unknown> {
|
|
58
|
+
if (request.headers.get("Content-Type")?.includes("application/json"))
|
|
59
|
+
return request.json();
|
|
60
|
+
|
|
61
|
+
const body = await request.formData();
|
|
62
|
+
|
|
63
|
+
return JSON.parse(String(body.get("_payload") ?? "{}"));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Browser form behavior shared by the React and Preact adapters. */
|
|
67
|
+
export interface RouteFormOptions extends NavigateOptions {
|
|
68
|
+
action?: string;
|
|
69
|
+
method?: "get" | "post" | "put" | "patch" | "delete";
|
|
70
|
+
reloadDocument?: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface FormSubmissionEvent {
|
|
74
|
+
currentTarget: HTMLFormElement;
|
|
75
|
+
defaultPrevented: boolean;
|
|
76
|
+
preventDefault: () => void;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Submit an internal form while leaving external actions to the browser. */
|
|
80
|
+
export function submitForm(
|
|
81
|
+
router: Router,
|
|
82
|
+
event: FormSubmissionEvent,
|
|
83
|
+
submitter: HTMLElement | null,
|
|
84
|
+
options: RouteFormOptions,
|
|
85
|
+
fetcher?: Fetcher,
|
|
86
|
+
): void {
|
|
87
|
+
if (event.defaultPrevented || options.reloadDocument) return;
|
|
88
|
+
|
|
89
|
+
const target = internalURL(
|
|
90
|
+
router.getSnapshot().url,
|
|
91
|
+
submitter?.getAttribute("formaction") ?? options.action,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
if (!target) return;
|
|
95
|
+
|
|
96
|
+
const data = new FormData(event.currentTarget, submitter);
|
|
97
|
+
const method = (
|
|
98
|
+
submitter?.getAttribute("formmethod") ?? options.method
|
|
99
|
+
)?.toUpperCase();
|
|
100
|
+
|
|
101
|
+
event.preventDefault();
|
|
102
|
+
|
|
103
|
+
if (method === "GET") {
|
|
104
|
+
target.search = submissionSearch(data);
|
|
105
|
+
|
|
106
|
+
void (fetcher
|
|
107
|
+
? fetcher.load(target.href)
|
|
108
|
+
: router.navigate(target.href, options));
|
|
109
|
+
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
void (fetcher ?? router).submit(target.href, data, {
|
|
114
|
+
method: method as SubmitOptions["method"],
|
|
115
|
+
});
|
|
116
|
+
}
|
|
@@ -55,6 +55,8 @@ export interface RevalidationArgs extends RevalidationContext {
|
|
|
55
55
|
|
|
56
56
|
/** A route component and its optional server and client data handlers. */
|
|
57
57
|
export interface RouteModule {
|
|
58
|
+
/** Stable identifier for targeted revalidation; defaults to the path. */
|
|
59
|
+
id?: string;
|
|
58
60
|
default?: unknown;
|
|
59
61
|
HydrateFallback?: unknown;
|
|
60
62
|
ErrorBoundary?: unknown;
|
|
@@ -96,7 +98,11 @@ export interface RouterOptions {
|
|
|
96
98
|
|
|
97
99
|
/** Options for replacing rather than appending a browser history entry. */
|
|
98
100
|
export interface NavigateOptions {
|
|
101
|
+
state?: unknown;
|
|
102
|
+
viewTransition?: boolean;
|
|
99
103
|
replace?: boolean;
|
|
104
|
+
/** Preserve scroll in integrations that manage scroll restoration. */
|
|
105
|
+
preventScrollReset?: boolean;
|
|
100
106
|
}
|
|
101
107
|
|
|
102
108
|
/** HTTP method for a route action submission; defaults to POST. */
|
|
@@ -129,6 +135,15 @@ export interface Fetcher {
|
|
|
129
135
|
dispose: () => void;
|
|
130
136
|
}
|
|
131
137
|
|
|
138
|
+
/** Refresh active route data without navigation, optionally selecting route IDs or paths. */
|
|
139
|
+
export interface RevalidateOptions extends RevalidationContext {
|
|
140
|
+
route?: string | readonly string[];
|
|
141
|
+
reason?: RevalidationArgs["reason"];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** A registered layout loader's revalidation callback. */
|
|
145
|
+
export type Revalidator = (args: RevalidationArgs) => void | Promise<void>;
|
|
146
|
+
|
|
132
147
|
/** Route navigation, subscriptions, fetchers, and loader revalidation. */
|
|
133
148
|
export interface Router {
|
|
134
149
|
routes: readonly Route[];
|
|
@@ -137,9 +152,11 @@ export interface Router {
|
|
|
137
152
|
start: () => () => void;
|
|
138
153
|
navigate: (url: string, options?: NavigateOptions) => Promise<void>;
|
|
139
154
|
revalidate: (
|
|
140
|
-
|
|
155
|
+
options?: RevalidateOptions | RevalidationArgs["reason"],
|
|
141
156
|
context?: RevalidationContext,
|
|
142
157
|
) => Promise<void>;
|
|
158
|
+
/** Register layout data under a route ID. The returned function unregisters it. */
|
|
159
|
+
registerRevalidator: (id: string, revalidate: Revalidator) => () => void;
|
|
143
160
|
submit: (
|
|
144
161
|
url: string,
|
|
145
162
|
body: FormData,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Resolve an internal URL; external links remain the browser's responsibility. */
|
|
2
|
+
export function internalURL(base: string, href = base): URL | undefined {
|
|
3
|
+
const url = new URL(href, base);
|
|
4
|
+
|
|
5
|
+
if (url.origin === new URL(base).origin) return url;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Browser and framework event fields used by navigation interception. */
|
|
9
|
+
type NavigationClickEvent = Pick<
|
|
10
|
+
MouseEvent,
|
|
11
|
+
"defaultPrevented" | "button" | "metaKey" | "ctrlKey" | "altKey" | "shiftKey"
|
|
12
|
+
>;
|
|
13
|
+
|
|
14
|
+
/** Whether an anchor click should use router navigation. */
|
|
15
|
+
export function navigationClick(
|
|
16
|
+
event: NavigationClickEvent,
|
|
17
|
+
anchor: HTMLAnchorElement,
|
|
18
|
+
): boolean {
|
|
19
|
+
return !(
|
|
20
|
+
event.defaultPrevented ||
|
|
21
|
+
event.button ||
|
|
22
|
+
event.metaKey ||
|
|
23
|
+
event.ctrlKey ||
|
|
24
|
+
event.altKey ||
|
|
25
|
+
event.shiftKey ||
|
|
26
|
+
anchor.hasAttribute("download") ||
|
|
27
|
+
(anchor.target && anchor.target !== "_self")
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ComponentChildren } from "preact";
|
|
2
|
+
import { use } from "preact/compat";
|
|
3
|
+
|
|
4
|
+
export interface AwaitProps<T> {
|
|
5
|
+
resolve: T | Promise<T>;
|
|
6
|
+
children: (value: Awaited<T>) => ComponentChildren;
|
|
7
|
+
errorElement?: ComponentChildren;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type Resolution = { value: unknown } | { error: unknown };
|
|
11
|
+
|
|
12
|
+
// Cache only the error-as-data adapter. Preact owns pending and resolved state.
|
|
13
|
+
const resolutions = new WeakMap<Promise<unknown>, Promise<Resolution>>();
|
|
14
|
+
|
|
15
|
+
export function Await<T>({
|
|
16
|
+
resolve,
|
|
17
|
+
children,
|
|
18
|
+
errorElement,
|
|
19
|
+
}: AwaitProps<T>): ComponentChildren {
|
|
20
|
+
if (!(resolve instanceof Promise)) return children(resolve as Awaited<T>);
|
|
21
|
+
|
|
22
|
+
let pending = resolutions.get(resolve);
|
|
23
|
+
|
|
24
|
+
if (!pending) {
|
|
25
|
+
pending = resolve.then(
|
|
26
|
+
(value) => ({ value }),
|
|
27
|
+
(error: unknown) => ({ error }),
|
|
28
|
+
);
|
|
29
|
+
resolutions.set(resolve, pending);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const result = use(pending);
|
|
33
|
+
|
|
34
|
+
if ("error" in result) {
|
|
35
|
+
if (errorElement !== undefined) return errorElement;
|
|
36
|
+
|
|
37
|
+
throw result.error;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return children(result.value as Awaited<T>);
|
|
41
|
+
}
|