@kasperrt/smolter 0.4.2 → 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/environment.ts +1 -0
- package/.generated/core/navigation.ts +1 -0
- package/.generated/core/prepare-routes.ts +50 -0
- package/.generated/core/presentation.ts +1 -1
- package/.generated/core/responses.ts +12 -0
- package/.generated/core/utils.ts +5 -2
- package/.generated/preact/await.tsx +1 -0
- package/.generated/preact/client.tsx +8 -27
- package/.generated/preact/deferred-hydration.ts +1 -98
- package/.generated/preact/document.tsx +10 -0
- package/.generated/preact/index.ts +11 -2
- package/.generated/preact/lazy-route.ts +7 -0
- package/.generated/preact/navigation.tsx +19 -5
- package/.generated/preact/root-data.ts +1 -45
- package/.generated/preact/route-hooks.tsx +12 -2
- package/.generated/preact/route-types.ts +14 -0
- package/.generated/preact/router.tsx +5 -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 +28 -2
- 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/outlet.ts +48 -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 -250
- package/.generated/server/assets.ts +7 -0
- package/.generated/server/dispatch.ts +87 -0
- package/.generated/server/handler.ts +3 -0
- package/.generated/server/http.ts +2 -0
- package/.generated/server/index.ts +2 -0
- package/.generated/server/node.ts +5 -2
- package/.generated/server/preact.ts +5 -2
- 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 +20 -0
- package/README.md +48 -48
- 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/environment.d.ts +1 -0
- package/dist/core/navigation.d.ts +1 -0
- 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/core/responses.d.ts +12 -0
- package/dist/core/responses.js +5 -0
- package/dist/core/utils.d.ts +5 -2
- package/dist/core/utils.js +1 -2
- package/dist/preact/await.d.ts +1 -0
- package/dist/preact/await.js +1 -0
- package/dist/preact/client.d.ts +7 -1
- package/dist/preact/client.js +11 -27
- package/dist/preact/deferred-hydration.d.ts +1 -10
- package/dist/preact/document.d.ts +10 -0
- package/dist/preact/document.js +11 -2
- package/dist/preact/index.d.ts +6 -2
- package/dist/preact/index.js +1 -1
- package/dist/preact/lazy-route.d.ts +2 -0
- package/dist/preact/lazy-route.js +6 -0
- package/dist/preact/navigation-state.js +1 -1
- package/dist/preact/navigation.d.ts +6 -0
- package/dist/preact/navigation.js +9 -5
- package/dist/preact/root-data.d.ts +1 -8
- package/dist/preact/route-hooks.d.ts +10 -0
- package/dist/preact/route-hooks.js +13 -3
- package/dist/preact/route-types.d.ts +14 -0
- package/dist/preact/router.d.ts +5 -2
- package/dist/preact/router.js +4 -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 +13 -2
- package/dist/react/index.js +7 -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/outlet.d.ts +16 -0
- package/dist/react/outlet.js +26 -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 -5
- package/dist/server/application.js +9 -154
- package/dist/server/assets.d.ts +2 -0
- package/dist/server/assets.js +6 -0
- package/dist/server/dispatch.d.ts +5 -0
- package/dist/server/dispatch.js +50 -0
- package/dist/server/handler.d.ts +3 -0
- package/dist/server/http.d.ts +2 -0
- package/dist/server/http.js +2 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +2 -1
- package/dist/server/node.d.ts +5 -1
- package/dist/server/preact.d.ts +5 -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 +21 -12
- package/.generated/core/resource-types.ts +0 -44
- package/.generated/core/resources.ts +0 -444
- package/.generated/react/data.ts +0 -25
- package/.generated/react/resources/components.tsx +0 -404
- package/dist/core/resource-types.d.ts +0 -37
- package/dist/core/resources.d.ts +0 -19
- package/dist/core/resources.js +0 -345
- package/dist/react/data.d.ts +0 -5
- package/dist/react/data.js +0 -3
- package/dist/react/link.js +0 -27
- package/dist/react/resources/components.d.ts +0 -64
- package/dist/react/resources/components.js +0 -239
|
@@ -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
|
+
}
|
|
@@ -7,6 +7,7 @@ import { createStore } from "./store.ts";
|
|
|
7
7
|
import type { NavigateOptions, Router } from "./types.ts";
|
|
8
8
|
import { navigationClick } from "./url.ts";
|
|
9
9
|
|
|
10
|
+
/** Browser history operation: POP, PUSH or REPLACE. */
|
|
10
11
|
export type NavigationType = "POP" | "PUSH" | "REPLACE";
|
|
11
12
|
|
|
12
13
|
export interface NavigationSnapshot {
|
|
@@ -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)
|
|
@@ -1,19 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP response helpers and loader result normalization.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
/** Normalized route failure with an HTTP status, status text and optional response data. */
|
|
1
7
|
export interface RouteError extends Error {
|
|
2
8
|
status: number;
|
|
3
9
|
statusText: string;
|
|
4
10
|
data?: unknown;
|
|
5
11
|
}
|
|
6
12
|
|
|
13
|
+
/** Loader value paired with HTTP response metadata. */
|
|
7
14
|
export interface DataResponse<T> {
|
|
8
15
|
value: T;
|
|
9
16
|
init: ResponseInit;
|
|
10
17
|
__routeData: true;
|
|
11
18
|
}
|
|
12
19
|
|
|
20
|
+
/** Wrap loader data with HTTP status and headers without creating a Response body. */
|
|
13
21
|
export function data<T>(value: T, init: ResponseInit = {}): DataResponse<T> {
|
|
14
22
|
return { value, init, __routeData: true };
|
|
15
23
|
}
|
|
16
24
|
|
|
25
|
+
/** Create an empty redirect Response with a Location header; defaults to HTTP 302. */
|
|
17
26
|
export function redirect(
|
|
18
27
|
location: string,
|
|
19
28
|
init: number | ResponseInit = 302,
|
|
@@ -29,10 +38,12 @@ export function redirect(
|
|
|
29
38
|
});
|
|
30
39
|
}
|
|
31
40
|
|
|
41
|
+
/** Identify normalized Error objects carrying an HTTP status. */
|
|
32
42
|
export function isRouteErrorResponse(value: unknown): value is RouteError {
|
|
33
43
|
return value instanceof Error && "status" in value;
|
|
34
44
|
}
|
|
35
45
|
|
|
46
|
+
/** Normalize transport responses and wrapped data, optionally resolving nested deferred values. */
|
|
36
47
|
export async function normalizeResult(
|
|
37
48
|
value: unknown,
|
|
38
49
|
resolveDeferred = true,
|
|
@@ -68,6 +79,7 @@ export async function normalizeResult(
|
|
|
68
79
|
}
|
|
69
80
|
|
|
70
81
|
// Smolter transfers JSON snapshots. Resolve deferred fields before serialization.
|
|
82
|
+
/** Recursively resolve promises in arrays and plain objects before JSON serialization. */
|
|
71
83
|
export async function resolveData(value: unknown): Promise<unknown> {
|
|
72
84
|
const resolved = await value;
|
|
73
85
|
|
package/.generated/core/utils.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Routing patterns, query strings and serialized data utilities.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
3
6
|
export * from "./match.ts";
|
|
4
7
|
export * from "./serialize.ts";
|
|
5
8
|
export * from "./url.ts";
|
|
@@ -17,6 +17,7 @@ type Resolution = { value: unknown } | { error: unknown };
|
|
|
17
17
|
// Cache only the error-as-data adapter. Preact owns pending and resolved state.
|
|
18
18
|
const resolutions = new WeakMap<Promise<unknown>, Promise<Resolution>>();
|
|
19
19
|
|
|
20
|
+
/** Render a value or promise. An explicit fallback owns pending content during early hydration; errorElement handles rejection. */
|
|
20
21
|
export function Await<T>(props: AwaitProps<T>): ComponentChildren {
|
|
21
22
|
const app = useContext(Application);
|
|
22
23
|
if (!(props.resolve instanceof Promise))
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Preact document hydration, browser history and deployment recovery.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
2
6
|
/** @jsxImportSource preact */
|
|
3
7
|
|
|
4
8
|
import {
|
|
@@ -6,6 +10,7 @@ import {
|
|
|
6
10
|
recoverDeployment,
|
|
7
11
|
watchDeployment,
|
|
8
12
|
} from "../client/index.ts";
|
|
13
|
+
import { connectHashHistory } from "../client/hash-history.ts";
|
|
9
14
|
import { createDataRouter, matchRoutes, type Router } from "../core/index.ts";
|
|
10
15
|
import { connectNavigation } from "../core/navigation.ts";
|
|
11
16
|
import { retainRoute } from "../core/presentation.ts";
|
|
@@ -19,6 +24,7 @@ import { prepareRoutes, RouterApplication } from "./router.tsx";
|
|
|
19
24
|
export * from "../client/index.ts";
|
|
20
25
|
export { configureApplication } from "../core/environment.ts";
|
|
21
26
|
|
|
27
|
+
/** Browser hydration settings for hash history, deployment recovery and deferred-data timeout. */
|
|
22
28
|
export interface MountApplicationOptions {
|
|
23
29
|
hash?: boolean;
|
|
24
30
|
deployment?: DeploymentOptions | null;
|
|
@@ -26,6 +32,7 @@ export interface MountApplicationOptions {
|
|
|
26
32
|
deferredTimeout?: number;
|
|
27
33
|
}
|
|
28
34
|
|
|
35
|
+
/** Create the browser application router and hydrate the document, installing recovery before initial lazy loading. */
|
|
29
36
|
export async function mountApplication(
|
|
30
37
|
routes: AppRoute[],
|
|
31
38
|
rootModule?: RootModule,
|
|
@@ -117,29 +124,3 @@ export async function mountApplication(
|
|
|
117
124
|
|
|
118
125
|
return router;
|
|
119
126
|
}
|
|
120
|
-
|
|
121
|
-
function connectHashHistory(router: Router) {
|
|
122
|
-
const read = router.getSnapshot;
|
|
123
|
-
const navigate = router.navigate;
|
|
124
|
-
router.navigate = async (url, options) => {
|
|
125
|
-
const target = new URL(url, router.getSnapshot().url);
|
|
126
|
-
const hash = `#${target.pathname}${target.search}${target.hash}`;
|
|
127
|
-
history[options?.replace ? "replaceState" : "pushState"](null, "", hash);
|
|
128
|
-
await navigate(url, options);
|
|
129
|
-
};
|
|
130
|
-
const unsubscribe = router.subscribe(() => {
|
|
131
|
-
const target = new URL(read().url);
|
|
132
|
-
const hash = `#${target.pathname}${target.search}${target.hash}`;
|
|
133
|
-
|
|
134
|
-
// Redirects commit inside the router rather than through navigate().
|
|
135
|
-
if (location.hash !== hash) history.replaceState(history.state, "", hash);
|
|
136
|
-
});
|
|
137
|
-
const onHashChange = () => void navigate(location.hash.slice(1) || "/");
|
|
138
|
-
window.addEventListener("hashchange", onHashChange);
|
|
139
|
-
const dispose = router.dispose;
|
|
140
|
-
router.dispose = () => {
|
|
141
|
-
unsubscribe();
|
|
142
|
-
window.removeEventListener("hashchange", onHashChange);
|
|
143
|
-
dispose();
|
|
144
|
-
};
|
|
145
|
-
}
|
|
@@ -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";
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preact document metadata, resource links and hydration scripts.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
1
6
|
/** @jsxImportSource preact */
|
|
2
7
|
|
|
3
8
|
import { applicationEnvironment } from "../core/environment.ts";
|
|
@@ -34,6 +39,7 @@ function renderMeta(item: MetaDescriptor, index: number) {
|
|
|
34
39
|
});
|
|
35
40
|
}
|
|
36
41
|
|
|
42
|
+
/** Render root or active-route metadata in the document head. */
|
|
37
43
|
export function Meta(): import("preact").JSX.Element {
|
|
38
44
|
const { routes, rootModule } = useApplication();
|
|
39
45
|
const snapshot = useSnapshot();
|
|
@@ -55,6 +61,7 @@ export function Meta(): import("preact").JSX.Element {
|
|
|
55
61
|
return <Fragment>{values.map(renderMeta)}</Fragment>;
|
|
56
62
|
}
|
|
57
63
|
|
|
64
|
+
/** Render stylesheet and other link descriptors from the application root. */
|
|
58
65
|
export function Links(): import("preact").JSX.Element {
|
|
59
66
|
const { rootModule } = useApplication();
|
|
60
67
|
|
|
@@ -67,10 +74,12 @@ export function Links(): import("preact").JSX.Element {
|
|
|
67
74
|
);
|
|
68
75
|
}
|
|
69
76
|
|
|
77
|
+
/** Nonce and browser entry options for the document hydration scripts. */
|
|
70
78
|
export interface ScriptProps {
|
|
71
79
|
nonce?: string;
|
|
72
80
|
}
|
|
73
81
|
|
|
82
|
+
/** Render the hydration snapshot and browser entry script with an optional CSP nonce. */
|
|
74
83
|
export function Scripts({ nonce }: ScriptProps): import("preact").JSX.Element {
|
|
75
84
|
const { root, rootError, router, entry, streaming, initialData } =
|
|
76
85
|
useApplication();
|
|
@@ -119,6 +128,7 @@ export function Scripts({ nonce }: ScriptProps): import("preact").JSX.Element {
|
|
|
119
128
|
);
|
|
120
129
|
}
|
|
121
130
|
|
|
131
|
+
/** Reset scroll after navigation unless the navigation requests preservation. */
|
|
122
132
|
export function ScrollRestoration(_props: ScriptProps): null {
|
|
123
133
|
const { router } = useApplication();
|
|
124
134
|
const { url, state } = useSnapshot();
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Preact routing components, typed data hooks and application integration.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
2
6
|
export type * from "../core/index.ts";
|
|
3
7
|
export {
|
|
4
8
|
createDataRouter,
|
|
@@ -30,5 +34,10 @@ export {
|
|
|
30
34
|
useSubmit,
|
|
31
35
|
} from "./route-hooks.tsx";
|
|
32
36
|
export * from "./route-types.ts";
|
|
33
|
-
export {
|
|
37
|
+
export {
|
|
38
|
+
type ApplicationProps,
|
|
39
|
+
Outlet,
|
|
40
|
+
type OutletProps,
|
|
41
|
+
RouterApplication,
|
|
42
|
+
} from "./router.tsx";
|
|
34
43
|
export * from "./routes.ts";
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* On-demand Preact route modules with shared intent prefetching.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
1
6
|
import { serverRoute } from "../client/index.ts";
|
|
2
7
|
import type { DataArgs, RouteModule, Router } from "../core/index.ts";
|
|
3
8
|
import { type ComponentType, createElement } from "preact";
|
|
@@ -6,6 +11,7 @@ import type { AppRoute } from "./route-types.ts";
|
|
|
6
11
|
import type { ErrorProps } from "./routes.ts";
|
|
7
12
|
import { useSyncExternalStore } from "./runtime.ts";
|
|
8
13
|
|
|
14
|
+
/** Lazy route path, server transport endpoint and available handler capabilities. */
|
|
9
15
|
export interface LazyRouteOptions {
|
|
10
16
|
path: string;
|
|
11
17
|
dataEndpoint?: string;
|
|
@@ -36,6 +42,7 @@ function LazyView() {
|
|
|
36
42
|
return Component && createElement(Component, {});
|
|
37
43
|
}
|
|
38
44
|
|
|
45
|
+
/** Create a route that imports its module on demand and shares the import with intent prefetching. */
|
|
39
46
|
export function lazyRoute(
|
|
40
47
|
options: LazyRouteOptions,
|
|
41
48
|
importer: () => Promise<LoadedRoute>,
|
|
@@ -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
|
|
|
@@ -15,11 +15,13 @@ export type { NavigateOptions } from "../core/index.ts";
|
|
|
15
15
|
|
|
16
16
|
export type { NavigationType } from "../core/navigation.ts";
|
|
17
17
|
|
|
18
|
+
/** Navigate to a URL or move through browser history using a numeric offset. */
|
|
18
19
|
export type NavigateFunction = (
|
|
19
20
|
to: string | number | Partial<Pick<URL, "pathname" | "search" | "hash">>,
|
|
20
21
|
options?: NavigateOptions,
|
|
21
22
|
) => void;
|
|
22
23
|
|
|
24
|
+
/** Anchor properties and router navigation options, including document reload and prefetch controls. */
|
|
23
25
|
export interface LinkProps extends Omit<ComponentProps<"a">, "href" | "role"> {
|
|
24
26
|
to?: string;
|
|
25
27
|
href?: string;
|
|
@@ -47,11 +49,13 @@ export interface LinkProps extends Omit<ComponentProps<"a">, "href" | "role"> {
|
|
|
47
49
|
reloadDocument?: boolean;
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
/** Active and pending flags supplied to NavLink render callbacks. */
|
|
50
53
|
export interface NavLinkRenderProps {
|
|
51
54
|
isActive: boolean;
|
|
52
55
|
isPending: boolean;
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
/** Link options with matching rules and state-aware class or children callbacks. */
|
|
55
59
|
export interface NavLinkProps
|
|
56
60
|
extends Omit<LinkProps, "className" | "children"> {
|
|
57
61
|
end?: boolean;
|
|
@@ -59,6 +63,7 @@ export interface NavLinkProps
|
|
|
59
63
|
children?: ReactNode | ((state: NavLinkRenderProps) => ReactNode);
|
|
60
64
|
}
|
|
61
65
|
|
|
66
|
+
/** Render an anchor with same-origin router navigation and optional intent prefetching. */
|
|
62
67
|
export function Link(options: LinkProps): ReactNode {
|
|
63
68
|
const {
|
|
64
69
|
href: hrefProp,
|
|
@@ -115,6 +120,7 @@ export function Link(options: LinkProps): ReactNode {
|
|
|
115
120
|
});
|
|
116
121
|
}
|
|
117
122
|
|
|
123
|
+
/** Render a Link with active-route state and aria-current for the matching destination. */
|
|
118
124
|
export function NavLink({
|
|
119
125
|
href,
|
|
120
126
|
to = href ?? "/",
|
|
@@ -124,11 +130,19 @@ export function NavLink({
|
|
|
124
130
|
...props
|
|
125
131
|
}: NavLinkProps): ReactNode {
|
|
126
132
|
const location = useURLLocation();
|
|
127
|
-
const
|
|
133
|
+
const destination = resolveApplicationURL(new URL(to, location.href));
|
|
134
|
+
const target = destination.pathname;
|
|
128
135
|
const isActive =
|
|
129
|
-
|
|
130
|
-
(
|
|
131
|
-
|
|
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 };
|
|
132
146
|
|
|
133
147
|
return createElement(Link, {
|
|
134
148
|
...props,
|