@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,27 @@
|
|
|
1
|
+
import type { Router } from "../core/index.ts";
|
|
2
|
+
|
|
3
|
+
export function connectHashHistory(router: Router) {
|
|
4
|
+
const read = router.getSnapshot;
|
|
5
|
+
const navigate = router.navigate;
|
|
6
|
+
router.navigate = async (url, options) => {
|
|
7
|
+
const target = new URL(url, router.getSnapshot().url);
|
|
8
|
+
const hash = `#${target.pathname}${target.search}${target.hash}`;
|
|
9
|
+
history[options?.replace ? "replaceState" : "pushState"](null, "", hash);
|
|
10
|
+
await navigate(url, options);
|
|
11
|
+
};
|
|
12
|
+
const unsubscribe = router.subscribe(() => {
|
|
13
|
+
const target = new URL(read().url);
|
|
14
|
+
const hash = `#${target.pathname}${target.search}${target.hash}`;
|
|
15
|
+
|
|
16
|
+
// Redirects commit inside the router rather than through navigate().
|
|
17
|
+
if (location.hash !== hash) history.replaceState(history.state, "", hash);
|
|
18
|
+
});
|
|
19
|
+
const onHashChange = () => void navigate(location.hash.slice(1) || "/");
|
|
20
|
+
window.addEventListener("hashchange", onHashChange);
|
|
21
|
+
const dispose = router.dispose;
|
|
22
|
+
router.dispose = () => {
|
|
23
|
+
unsubscribe();
|
|
24
|
+
window.removeEventListener("hashchange", onHashChange);
|
|
25
|
+
dispose();
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
type Settlement = [number, unknown, string | null];
|
|
2
|
+
|
|
3
|
+
interface PendingValue {
|
|
4
|
+
promise: Promise<unknown>;
|
|
5
|
+
resolve: (value: unknown) => void;
|
|
6
|
+
reject: (reason: Error) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface DeferredWindow extends Window {
|
|
10
|
+
__smolterDeferred?: Settlement[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Restore promise identity before hydrating Suspense boundaries. */
|
|
14
|
+
export function readHydration(json: string, timeout = 30_000) {
|
|
15
|
+
if (!Number.isInteger(timeout) || timeout < 1 || timeout > 2_147_483_647)
|
|
16
|
+
throw new RangeError(
|
|
17
|
+
"deferredTimeout must be an integer between 1 and 2147483647 ms",
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const pending = new Map<number, PendingValue>();
|
|
21
|
+
const queue = ((window as DeferredWindow).__smolterDeferred ??= []);
|
|
22
|
+
|
|
23
|
+
function revive(_key: string, value: unknown): unknown {
|
|
24
|
+
if (!value || typeof value !== "object") return value;
|
|
25
|
+
if ("__smolterUndefined" in value) return undefined;
|
|
26
|
+
|
|
27
|
+
if (
|
|
28
|
+
"__smolterPending" in value &&
|
|
29
|
+
typeof value.__smolterPending === "number"
|
|
30
|
+
) {
|
|
31
|
+
const id = value.__smolterPending;
|
|
32
|
+
const existing = pending.get(id);
|
|
33
|
+
if (existing) return existing.promise;
|
|
34
|
+
|
|
35
|
+
let resolve!: PendingValue["resolve"];
|
|
36
|
+
let reject!: PendingValue["reject"];
|
|
37
|
+
const promise = new Promise((accept, fail) => {
|
|
38
|
+
resolve = accept;
|
|
39
|
+
reject = fail;
|
|
40
|
+
});
|
|
41
|
+
void promise.catch(() => {});
|
|
42
|
+
pending.set(id, { promise, resolve, reject });
|
|
43
|
+
|
|
44
|
+
return promise;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if ("__smolterRejected" in value) {
|
|
48
|
+
const failure = value.__smolterRejected as { message: string };
|
|
49
|
+
const rejected = Promise.reject(
|
|
50
|
+
Object.assign(new Error(failure.message), failure),
|
|
51
|
+
);
|
|
52
|
+
void rejected.catch(() => {});
|
|
53
|
+
|
|
54
|
+
return rejected;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const hydration = JSON.parse(json, revive);
|
|
61
|
+
const dispose = () => {
|
|
62
|
+
clearTimeout(deadline);
|
|
63
|
+
window.removeEventListener("DOMContentLoaded", dispose);
|
|
64
|
+
for (const target of pending.values())
|
|
65
|
+
target.reject(new Error("Hydration stream closed"));
|
|
66
|
+
pending.clear();
|
|
67
|
+
queue.push = () => 0;
|
|
68
|
+
};
|
|
69
|
+
const deadline = pending.size
|
|
70
|
+
? setTimeout(() => {
|
|
71
|
+
for (const target of pending.values())
|
|
72
|
+
target.reject(new Error("Hydration stream timed out"));
|
|
73
|
+
dispose();
|
|
74
|
+
}, timeout)
|
|
75
|
+
: undefined;
|
|
76
|
+
const receive = (packet: Settlement) => {
|
|
77
|
+
const [id, value, error] = packet;
|
|
78
|
+
if (id === -1) return dispose();
|
|
79
|
+
|
|
80
|
+
const target = pending.get(id);
|
|
81
|
+
if (!target) return;
|
|
82
|
+
|
|
83
|
+
if (error !== null) target.reject(new Error(error));
|
|
84
|
+
if (error === null)
|
|
85
|
+
target.resolve(JSON.parse(JSON.stringify(value), revive));
|
|
86
|
+
};
|
|
87
|
+
const buffered = queue.splice(0);
|
|
88
|
+
queue.push = (...packets) => {
|
|
89
|
+
packets.forEach(receive);
|
|
90
|
+
return 0;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
window.addEventListener("DOMContentLoaded", dispose, { once: true });
|
|
94
|
+
buffered.forEach(receive);
|
|
95
|
+
if (document.readyState !== "loading") dispose();
|
|
96
|
+
|
|
97
|
+
return { hydration, dispose };
|
|
98
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { RevalidationArgs, Router } from "../core/index.ts";
|
|
2
|
+
|
|
3
|
+
interface RootModule {
|
|
4
|
+
default?: unknown;
|
|
5
|
+
Layout?: unknown;
|
|
6
|
+
id?: string;
|
|
7
|
+
loaderPath?: string;
|
|
8
|
+
shouldRevalidate?: (args: RevalidationArgs) => boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export { createStore as createRootStore } from "../core/store.ts";
|
|
12
|
+
|
|
13
|
+
import { createStore } from "../core/store.ts";
|
|
14
|
+
|
|
15
|
+
export function attachRootData(
|
|
16
|
+
router: Router,
|
|
17
|
+
initial: unknown,
|
|
18
|
+
rootModule?: RootModule,
|
|
19
|
+
) {
|
|
20
|
+
const store = createStore(initial);
|
|
21
|
+
const loaderPath = rootModule?.loaderPath;
|
|
22
|
+
|
|
23
|
+
if (!rootModule || !loaderPath) return store;
|
|
24
|
+
|
|
25
|
+
let pending: ReturnType<Router["createFetcher"]> | undefined;
|
|
26
|
+
const unregister = router.registerRevalidator(
|
|
27
|
+
rootModule.id ?? "root",
|
|
28
|
+
async (args) => {
|
|
29
|
+
if (rootModule.shouldRevalidate?.(args) === false) return;
|
|
30
|
+
|
|
31
|
+
pending?.dispose();
|
|
32
|
+
const fetcher = router.createFetcher();
|
|
33
|
+
pending = fetcher;
|
|
34
|
+
const result = await fetcher.load(loaderPath);
|
|
35
|
+
|
|
36
|
+
if (pending === fetcher && !result.error) store.update(result.data);
|
|
37
|
+
|
|
38
|
+
fetcher.dispose();
|
|
39
|
+
|
|
40
|
+
if (pending === fetcher) pending = undefined;
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
const dispose = router.dispose;
|
|
44
|
+
router.dispose = () => {
|
|
45
|
+
pending?.dispose();
|
|
46
|
+
pending = undefined;
|
|
47
|
+
unregister();
|
|
48
|
+
dispose();
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return store;
|
|
52
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { normalizeResult } from "./responses.ts";
|
|
2
|
+
import type { DataFunction, Route } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
type AppRoute = Route & { routePath?: string; preload?: () => Promise<void> };
|
|
5
|
+
|
|
6
|
+
export function prepareRoutes(
|
|
7
|
+
routes: AppRoute[],
|
|
8
|
+
preserveDeferred = false,
|
|
9
|
+
): AppRoute[] {
|
|
10
|
+
return routes.map((route) => {
|
|
11
|
+
const wrapped: AppRoute = {
|
|
12
|
+
...route,
|
|
13
|
+
path: route.routePath ?? route.path,
|
|
14
|
+
shouldRevalidate: (args) => {
|
|
15
|
+
const { currentUrl, nextUrl } = args;
|
|
16
|
+
|
|
17
|
+
if (
|
|
18
|
+
args.reason === "navigation" &&
|
|
19
|
+
currentUrl.pathname === nextUrl.pathname &&
|
|
20
|
+
currentUrl.search === nextUrl.search &&
|
|
21
|
+
currentUrl.hash !== nextUrl.hash
|
|
22
|
+
)
|
|
23
|
+
return false;
|
|
24
|
+
|
|
25
|
+
return route.shouldRevalidate?.(args) ?? args.defaultShouldRevalidate;
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
for (const key of [
|
|
30
|
+
"loader",
|
|
31
|
+
"action",
|
|
32
|
+
"clientLoader",
|
|
33
|
+
"clientAction",
|
|
34
|
+
] as const) {
|
|
35
|
+
const handler = route[key] as DataFunction | undefined;
|
|
36
|
+
|
|
37
|
+
if (!handler) continue;
|
|
38
|
+
|
|
39
|
+
wrapped[key] = (args) =>
|
|
40
|
+
Promise.resolve()
|
|
41
|
+
.then(() => handler(args))
|
|
42
|
+
.then(
|
|
43
|
+
(value) => normalizeResult(value, !preserveDeferred),
|
|
44
|
+
(error) => normalizeResult(error, !preserveDeferred),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return wrapped;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -9,7 +9,7 @@ type CommitView = (update: () => void) => void;
|
|
|
9
9
|
const pending = new WeakMap<Router, () => Snapshot>();
|
|
10
10
|
|
|
11
11
|
export function pendingLocation(router: Router) {
|
|
12
|
-
const snapshot = pending.get(router)?.();
|
|
12
|
+
const snapshot = pending.get(router)?.() ?? router.getSnapshot();
|
|
13
13
|
|
|
14
14
|
return snapshot?.state !== "idle" && snapshot?.url
|
|
15
15
|
? new URL(snapshot.url)
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
recoverDeployment,
|
|
11
11
|
watchDeployment,
|
|
12
12
|
} from "../client/index.ts";
|
|
13
|
+
import { connectHashHistory } from "../client/hash-history.ts";
|
|
13
14
|
import { createDataRouter, matchRoutes, type Router } from "../core/index.ts";
|
|
14
15
|
import { connectNavigation } from "../core/navigation.ts";
|
|
15
16
|
import { retainRoute } from "../core/presentation.ts";
|
|
@@ -123,29 +124,3 @@ export async function mountApplication(
|
|
|
123
124
|
|
|
124
125
|
return router;
|
|
125
126
|
}
|
|
126
|
-
|
|
127
|
-
function connectHashHistory(router: Router) {
|
|
128
|
-
const read = router.getSnapshot;
|
|
129
|
-
const navigate = router.navigate;
|
|
130
|
-
router.navigate = async (url, options) => {
|
|
131
|
-
const target = new URL(url, router.getSnapshot().url);
|
|
132
|
-
const hash = `#${target.pathname}${target.search}${target.hash}`;
|
|
133
|
-
history[options?.replace ? "replaceState" : "pushState"](null, "", hash);
|
|
134
|
-
await navigate(url, options);
|
|
135
|
-
};
|
|
136
|
-
const unsubscribe = router.subscribe(() => {
|
|
137
|
-
const target = new URL(read().url);
|
|
138
|
-
const hash = `#${target.pathname}${target.search}${target.hash}`;
|
|
139
|
-
|
|
140
|
-
// Redirects commit inside the router rather than through navigate().
|
|
141
|
-
if (location.hash !== hash) history.replaceState(history.state, "", hash);
|
|
142
|
-
});
|
|
143
|
-
const onHashChange = () => void navigate(location.hash.slice(1) || "/");
|
|
144
|
-
window.addEventListener("hashchange", onHashChange);
|
|
145
|
-
const dispose = router.dispose;
|
|
146
|
-
router.dispose = () => {
|
|
147
|
-
unsubscribe();
|
|
148
|
-
window.removeEventListener("hashchange", onHashChange);
|
|
149
|
-
dispose();
|
|
150
|
-
};
|
|
151
|
-
}
|
|
@@ -1,98 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
interface PendingValue {
|
|
4
|
-
promise: Promise<unknown>;
|
|
5
|
-
resolve: (value: unknown) => void;
|
|
6
|
-
reject: (reason: Error) => void;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface DeferredWindow extends Window {
|
|
10
|
-
__smolterDeferred?: Settlement[];
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/** Restore promise identity before hydrating Suspense boundaries. */
|
|
14
|
-
export function readHydration(json: string, timeout = 30_000) {
|
|
15
|
-
if (!Number.isInteger(timeout) || timeout < 1 || timeout > 2_147_483_647)
|
|
16
|
-
throw new RangeError(
|
|
17
|
-
"deferredTimeout must be an integer between 1 and 2147483647 ms",
|
|
18
|
-
);
|
|
19
|
-
|
|
20
|
-
const pending = new Map<number, PendingValue>();
|
|
21
|
-
const queue = ((window as DeferredWindow).__smolterDeferred ??= []);
|
|
22
|
-
|
|
23
|
-
function revive(_key: string, value: unknown): unknown {
|
|
24
|
-
if (!value || typeof value !== "object") return value;
|
|
25
|
-
if ("__smolterUndefined" in value) return undefined;
|
|
26
|
-
|
|
27
|
-
if (
|
|
28
|
-
"__smolterPending" in value &&
|
|
29
|
-
typeof value.__smolterPending === "number"
|
|
30
|
-
) {
|
|
31
|
-
const id = value.__smolterPending;
|
|
32
|
-
const existing = pending.get(id);
|
|
33
|
-
if (existing) return existing.promise;
|
|
34
|
-
|
|
35
|
-
let resolve!: PendingValue["resolve"];
|
|
36
|
-
let reject!: PendingValue["reject"];
|
|
37
|
-
const promise = new Promise((accept, fail) => {
|
|
38
|
-
resolve = accept;
|
|
39
|
-
reject = fail;
|
|
40
|
-
});
|
|
41
|
-
void promise.catch(() => {});
|
|
42
|
-
pending.set(id, { promise, resolve, reject });
|
|
43
|
-
|
|
44
|
-
return promise;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if ("__smolterRejected" in value) {
|
|
48
|
-
const failure = value.__smolterRejected as { message: string };
|
|
49
|
-
const rejected = Promise.reject(
|
|
50
|
-
Object.assign(new Error(failure.message), failure),
|
|
51
|
-
);
|
|
52
|
-
void rejected.catch(() => {});
|
|
53
|
-
|
|
54
|
-
return rejected;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return value;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const hydration = JSON.parse(json, revive);
|
|
61
|
-
const dispose = () => {
|
|
62
|
-
clearTimeout(deadline);
|
|
63
|
-
window.removeEventListener("DOMContentLoaded", dispose);
|
|
64
|
-
for (const target of pending.values())
|
|
65
|
-
target.reject(new Error("Hydration stream closed"));
|
|
66
|
-
pending.clear();
|
|
67
|
-
queue.push = () => 0;
|
|
68
|
-
};
|
|
69
|
-
const deadline = pending.size
|
|
70
|
-
? setTimeout(() => {
|
|
71
|
-
for (const target of pending.values())
|
|
72
|
-
target.reject(new Error("Hydration stream timed out"));
|
|
73
|
-
dispose();
|
|
74
|
-
}, timeout)
|
|
75
|
-
: undefined;
|
|
76
|
-
const receive = (packet: Settlement) => {
|
|
77
|
-
const [id, value, error] = packet;
|
|
78
|
-
if (id === -1) return dispose();
|
|
79
|
-
|
|
80
|
-
const target = pending.get(id);
|
|
81
|
-
if (!target) return;
|
|
82
|
-
|
|
83
|
-
if (error !== null) target.reject(new Error(error));
|
|
84
|
-
if (error === null)
|
|
85
|
-
target.resolve(JSON.parse(JSON.stringify(value), revive));
|
|
86
|
-
};
|
|
87
|
-
const buffered = queue.splice(0);
|
|
88
|
-
queue.push = (...packets) => {
|
|
89
|
-
packets.forEach(receive);
|
|
90
|
-
return 0;
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
window.addEventListener("DOMContentLoaded", dispose, { once: true });
|
|
94
|
-
buffered.forEach(receive);
|
|
95
|
-
if (document.readyState !== "loading") dispose();
|
|
96
|
-
|
|
97
|
-
return { hydration, dispose };
|
|
98
|
-
}
|
|
1
|
+
export { type DeferredWindow, readHydration } from "../client/hydration.ts";
|
|
@@ -34,5 +34,10 @@ export {
|
|
|
34
34
|
useSubmit,
|
|
35
35
|
} from "./route-hooks.tsx";
|
|
36
36
|
export * from "./route-types.ts";
|
|
37
|
-
export {
|
|
37
|
+
export {
|
|
38
|
+
type ApplicationProps,
|
|
39
|
+
Outlet,
|
|
40
|
+
type OutletProps,
|
|
41
|
+
RouterApplication,
|
|
42
|
+
} from "./router.tsx";
|
|
38
43
|
export * from "./routes.ts";
|
|
@@ -7,7 +7,7 @@ import { navigationClick } from "../core/url.ts";
|
|
|
7
7
|
import type { TargetedMouseEvent } from "preact";
|
|
8
8
|
import type { ComponentProps, ReactNode } from "preact/compat";
|
|
9
9
|
import { useRouter, useSnapshot } from "./context.ts";
|
|
10
|
-
import { useURLLocation } from "./route-hooks.tsx";
|
|
10
|
+
import { useNavigation, useURLLocation } from "./route-hooks.tsx";
|
|
11
11
|
import type { AppRoute } from "./route-types.ts";
|
|
12
12
|
import { createElement } from "./runtime.ts";
|
|
13
13
|
|
|
@@ -130,11 +130,19 @@ export function NavLink({
|
|
|
130
130
|
...props
|
|
131
131
|
}: NavLinkProps): ReactNode {
|
|
132
132
|
const location = useURLLocation();
|
|
133
|
-
const
|
|
133
|
+
const destination = resolveApplicationURL(new URL(to, location.href));
|
|
134
|
+
const target = destination.pathname;
|
|
134
135
|
const isActive =
|
|
135
|
-
|
|
136
|
-
(
|
|
137
|
-
|
|
136
|
+
destination.origin === location.origin &&
|
|
137
|
+
(location.pathname === target ||
|
|
138
|
+
(!end && target !== "/" && location.pathname.startsWith(`${target}/`)));
|
|
139
|
+
const pending = useNavigation().location;
|
|
140
|
+
const isPending =
|
|
141
|
+
!!pending &&
|
|
142
|
+
pending.origin === destination.origin &&
|
|
143
|
+
(pending.pathname === target ||
|
|
144
|
+
(!end && target !== "/" && pending.pathname.startsWith(`${target}/`)));
|
|
145
|
+
const state = { isActive, isPending };
|
|
138
146
|
|
|
139
147
|
return createElement(Link, {
|
|
140
148
|
...props,
|
|
@@ -1,45 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import type { RootModule } from "./route-types.ts";
|
|
3
|
-
|
|
4
|
-
export { createStore as createRootStore } from "../core/store.ts";
|
|
5
|
-
|
|
6
|
-
import { createStore } from "../core/store.ts";
|
|
7
|
-
|
|
8
|
-
export function attachRootData(
|
|
9
|
-
router: Router,
|
|
10
|
-
initial: unknown,
|
|
11
|
-
rootModule?: RootModule,
|
|
12
|
-
) {
|
|
13
|
-
const store = createStore(initial);
|
|
14
|
-
const loaderPath = rootModule?.loaderPath;
|
|
15
|
-
|
|
16
|
-
if (!rootModule || !loaderPath) return store;
|
|
17
|
-
|
|
18
|
-
let pending: ReturnType<Router["createFetcher"]> | undefined;
|
|
19
|
-
const unregister = router.registerRevalidator(
|
|
20
|
-
rootModule.id ?? "root",
|
|
21
|
-
async (args) => {
|
|
22
|
-
if (rootModule.shouldRevalidate?.(args) === false) return;
|
|
23
|
-
|
|
24
|
-
pending?.dispose();
|
|
25
|
-
const fetcher = router.createFetcher();
|
|
26
|
-
pending = fetcher;
|
|
27
|
-
const result = await fetcher.load(loaderPath);
|
|
28
|
-
|
|
29
|
-
if (pending === fetcher && !result.error) store.update(result.data);
|
|
30
|
-
|
|
31
|
-
fetcher.dispose();
|
|
32
|
-
|
|
33
|
-
if (pending === fetcher) pending = undefined;
|
|
34
|
-
},
|
|
35
|
-
);
|
|
36
|
-
const dispose = router.dispose;
|
|
37
|
-
router.dispose = () => {
|
|
38
|
-
pending?.dispose();
|
|
39
|
-
pending = undefined;
|
|
40
|
-
unregister();
|
|
41
|
-
dispose();
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
return store;
|
|
45
|
-
}
|
|
1
|
+
export { attachRootData, createRootStore } from "../client/root-data.ts";
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
/** @jsxImportSource preact */
|
|
2
2
|
|
|
3
|
-
import type { DataFunction } from "../core/index.ts";
|
|
4
|
-
import { normalizeResult } from "../core/responses.ts";
|
|
5
3
|
import type { ReactNode } from "preact/compat";
|
|
6
4
|
import { RouterProvider } from "./context.ts";
|
|
7
5
|
import {
|
|
@@ -10,7 +8,6 @@ import {
|
|
|
10
8
|
OutletContext,
|
|
11
9
|
} from "./root-context.ts";
|
|
12
10
|
import { createRootStore } from "./root-data.ts";
|
|
13
|
-
import type { AppRoute } from "./route-types.ts";
|
|
14
11
|
import { Routes } from "./routes.ts";
|
|
15
12
|
import { useContext, useMemo, useSyncExternalStore } from "./runtime.ts";
|
|
16
13
|
|
|
@@ -34,6 +31,7 @@ export function Outlet({
|
|
|
34
31
|
);
|
|
35
32
|
}
|
|
36
33
|
|
|
34
|
+
/** Root data, document components and request-scoped router for an application. */
|
|
37
35
|
export interface ApplicationProps extends ApplicationContext {
|
|
38
36
|
document?: boolean;
|
|
39
37
|
rootStore?: ReturnType<typeof createRootStore>;
|
|
@@ -69,48 +67,4 @@ export function RouterApplication(
|
|
|
69
67
|
);
|
|
70
68
|
}
|
|
71
69
|
|
|
72
|
-
export
|
|
73
|
-
routes: AppRoute[],
|
|
74
|
-
preserveDeferred = false,
|
|
75
|
-
): AppRoute[] {
|
|
76
|
-
return routes.map((route) => {
|
|
77
|
-
const wrapped: AppRoute = {
|
|
78
|
-
...route,
|
|
79
|
-
path: route.routePath ?? route.path,
|
|
80
|
-
shouldRevalidate: (args) => {
|
|
81
|
-
const { currentUrl, nextUrl } = args;
|
|
82
|
-
|
|
83
|
-
if (
|
|
84
|
-
args.reason === "navigation" &&
|
|
85
|
-
currentUrl.pathname === nextUrl.pathname &&
|
|
86
|
-
currentUrl.search === nextUrl.search &&
|
|
87
|
-
currentUrl.hash !== nextUrl.hash
|
|
88
|
-
)
|
|
89
|
-
return false;
|
|
90
|
-
|
|
91
|
-
return route.shouldRevalidate?.(args) ?? args.defaultShouldRevalidate;
|
|
92
|
-
},
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
for (const key of [
|
|
96
|
-
"loader",
|
|
97
|
-
"action",
|
|
98
|
-
"clientLoader",
|
|
99
|
-
"clientAction",
|
|
100
|
-
] as const) {
|
|
101
|
-
const handler = route[key] as DataFunction | undefined;
|
|
102
|
-
|
|
103
|
-
if (!handler) continue;
|
|
104
|
-
|
|
105
|
-
wrapped[key] = (args) =>
|
|
106
|
-
Promise.resolve()
|
|
107
|
-
.then(() => handler(args))
|
|
108
|
-
.then(
|
|
109
|
-
(value) => normalizeResult(value, !preserveDeferred),
|
|
110
|
-
(error) => normalizeResult(error, !preserveDeferred),
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return wrapped;
|
|
115
|
-
});
|
|
116
|
-
}
|
|
70
|
+
export { prepareRoutes } from "../core/prepare-routes.ts";
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React document hydration, browser history and deployment recovery.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
/** @jsxImportSource react */
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type DeploymentOptions,
|
|
10
|
+
recoverDeployment,
|
|
11
|
+
watchDeployment,
|
|
12
|
+
} from "../client/index.ts";
|
|
13
|
+
import { connectHashHistory } from "../client/hash-history.ts";
|
|
14
|
+
import { readHydration } from "../client/hydration.ts";
|
|
15
|
+
import { attachRootData } from "../client/root-data.ts";
|
|
16
|
+
import { createDataRouter, matchRoutes, type Router } from "../core/index.ts";
|
|
17
|
+
import { connectNavigation } from "../core/navigation.ts";
|
|
18
|
+
import { prepareRoutes } from "../core/prepare-routes.ts";
|
|
19
|
+
import { retainRoute } from "../core/presentation.ts";
|
|
20
|
+
import { flushSync } from "react-dom";
|
|
21
|
+
import { createRoot, hydrateRoot } from "react-dom/client";
|
|
22
|
+
import type { AppRoute, Hydration, RootModule } from "./route-types.ts";
|
|
23
|
+
import { RouterApplication } from "./router.ts";
|
|
24
|
+
|
|
25
|
+
export * from "../client/index.ts";
|
|
26
|
+
export { configureApplication } from "../core/environment.ts";
|
|
27
|
+
|
|
28
|
+
/** Browser hydration settings for hash history, deployment recovery and deferred-data timeout. */
|
|
29
|
+
export interface MountApplicationOptions {
|
|
30
|
+
hash?: boolean;
|
|
31
|
+
deployment?: DeploymentOptions | null;
|
|
32
|
+
/** Total initial deferred-data budget in integer milliseconds; not an inactivity timeout. Defaults to 30,000. */
|
|
33
|
+
deferredTimeout?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Create the browser application router and hydrate the document, installing recovery before initial lazy loading. */
|
|
37
|
+
export async function mountApplication(
|
|
38
|
+
routes: AppRoute[],
|
|
39
|
+
rootModule?: RootModule,
|
|
40
|
+
options: boolean | MountApplicationOptions = false,
|
|
41
|
+
): Promise<Router> {
|
|
42
|
+
const {
|
|
43
|
+
hash = false,
|
|
44
|
+
deployment = null,
|
|
45
|
+
deferredTimeout,
|
|
46
|
+
} = typeof options === "boolean" ? { hash: options } : options;
|
|
47
|
+
const initialURL = new URL(
|
|
48
|
+
hash ? location.hash.slice(1) || "/" : location.href,
|
|
49
|
+
location.origin,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const initialRoute = matchRoutes(routes, initialURL.pathname)?.route as
|
|
53
|
+
| AppRoute
|
|
54
|
+
| undefined;
|
|
55
|
+
|
|
56
|
+
await initialRoute?.preload?.().catch(async (error: unknown) => {
|
|
57
|
+
await recoverDeployment(deployment, initialURL);
|
|
58
|
+
throw error;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
routes = prepareRoutes(routes);
|
|
62
|
+
const element = document.getElementById("smolter-data");
|
|
63
|
+
const initial = element?.textContent
|
|
64
|
+
? readHydration(element.textContent, deferredTimeout)
|
|
65
|
+
: undefined;
|
|
66
|
+
const hydration: Hydration | undefined = initial?.hydration;
|
|
67
|
+
|
|
68
|
+
if (hydration?.snapshot.error)
|
|
69
|
+
hydration.snapshot.error = Object.assign(
|
|
70
|
+
new Error(hydration.snapshot.error.message),
|
|
71
|
+
hydration.snapshot.error,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const router = createDataRouter(routes, {
|
|
75
|
+
browser: !hash,
|
|
76
|
+
url: hash
|
|
77
|
+
? new URL(location.hash.slice(1) || "/", location.origin).href
|
|
78
|
+
: location.href,
|
|
79
|
+
...(hydration && { hydrationData: hydration.snapshot }),
|
|
80
|
+
});
|
|
81
|
+
const dispose = router.dispose;
|
|
82
|
+
router.dispose = () => {
|
|
83
|
+
initial?.dispose();
|
|
84
|
+
dispose();
|
|
85
|
+
};
|
|
86
|
+
watchDeployment(router, deployment);
|
|
87
|
+
if (hash) connectHashHistory(router);
|
|
88
|
+
|
|
89
|
+
retainRoute(router, flushSync);
|
|
90
|
+
|
|
91
|
+
connectNavigation(router);
|
|
92
|
+
const rootStore = attachRootData(router, hydration?.root, rootModule);
|
|
93
|
+
const props = {
|
|
94
|
+
routes,
|
|
95
|
+
router,
|
|
96
|
+
root: hydration?.root,
|
|
97
|
+
rootModule,
|
|
98
|
+
rootStore,
|
|
99
|
+
rootError: hydration?.rootError,
|
|
100
|
+
initialData: element?.textContent ?? undefined,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
if (hydration) {
|
|
104
|
+
const mounted = hydrateRoot(
|
|
105
|
+
document,
|
|
106
|
+
<RouterApplication {...props} document />,
|
|
107
|
+
);
|
|
108
|
+
const cleanup = router.dispose;
|
|
109
|
+
router.dispose = () => {
|
|
110
|
+
mounted.unmount();
|
|
111
|
+
cleanup();
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return router;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const target = document.getElementById("root");
|
|
118
|
+
|
|
119
|
+
if (target) {
|
|
120
|
+
const mounted = createRoot(target);
|
|
121
|
+
mounted.render(<RouterApplication {...props} />);
|
|
122
|
+
const cleanup = router.dispose;
|
|
123
|
+
router.dispose = () => {
|
|
124
|
+
mounted.unmount();
|
|
125
|
+
cleanup();
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return router;
|
|
130
|
+
}
|