hono-preact 0.3.0 → 0.5.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/dist/iso/define-routes.d.ts +0 -1
- package/dist/iso/define-routes.js +10 -3
- package/dist/iso/form.js +6 -0
- package/dist/iso/index.d.ts +6 -0
- package/dist/iso/index.js +9 -0
- package/dist/iso/internal/history-shim.d.ts +20 -0
- package/dist/iso/internal/history-shim.js +110 -0
- package/dist/iso/internal/merge-refs.d.ts +4 -0
- package/dist/iso/internal/merge-refs.js +14 -0
- package/dist/iso/internal/page-middleware-host.js +148 -45
- package/dist/iso/internal/persist-registry.d.ts +10 -0
- package/dist/iso/internal/persist-registry.js +24 -0
- package/dist/iso/internal/route-change.d.ts +25 -2
- package/dist/iso/internal/route-change.js +414 -13
- package/dist/iso/internal/use-render.d.ts +11 -0
- package/dist/iso/internal/use-render.js +47 -0
- package/dist/iso/internal/view-transition-event.d.ts +23 -0
- package/dist/iso/internal/view-transition-event.js +25 -0
- package/dist/iso/internal.d.ts +6 -1
- package/dist/iso/internal.js +6 -1
- package/dist/iso/persist.d.ts +14 -0
- package/dist/iso/persist.js +56 -0
- package/dist/iso/view-transition-lifecycle.d.ts +9 -0
- package/dist/iso/view-transition-lifecycle.js +18 -0
- package/dist/iso/view-transition-name.d.ts +17 -0
- package/dist/iso/view-transition-name.js +79 -0
- package/dist/iso/view-transition-types.d.ts +19 -0
- package/dist/iso/view-transition-types.js +36 -0
- package/dist/server/render.js +95 -52
- package/dist/vite/client-entry.js +16 -9
- package/package.json +2 -2
|
@@ -51,7 +51,6 @@ export type RoutesManifest = {
|
|
|
51
51
|
export declare function defineRoutes(tree: RouteDef[]): RoutesManifest;
|
|
52
52
|
export type RoutesProps = {
|
|
53
53
|
routes: RoutesManifest;
|
|
54
|
-
onRouteChange?: (url: string) => void;
|
|
55
54
|
};
|
|
56
55
|
export declare const Routes: ComponentType<RoutesProps>;
|
|
57
56
|
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { h } from 'preact';
|
|
2
2
|
import { lazy, Route, Router, useLocation } from 'preact-iso';
|
|
3
3
|
import { RouteLocationsProvider } from './internal/route-locations.js';
|
|
4
|
+
import { __noteLoadEnd, __noteLoadStart } from './internal/route-change.js';
|
|
4
5
|
function wrapWithRouteLocations(serverMod, location, node) {
|
|
5
6
|
const moduleKey = serverMod
|
|
6
7
|
?.__moduleKey;
|
|
@@ -158,7 +159,10 @@ function makeLayoutGroupComponent(layoutImport, server, layoutPathPattern, child
|
|
|
158
159
|
const inner = buildInnerRoutes(children, viewCache);
|
|
159
160
|
const Wrapper = (location) => {
|
|
160
161
|
const layoutLocation = deriveLayoutLocation(location, layoutPathPattern);
|
|
161
|
-
const layoutNode = h(Layout, null, h(Router,
|
|
162
|
+
const layoutNode = h(Layout, null, h(asRouteComponent(Router), {
|
|
163
|
+
onLoadStart: __noteLoadStart,
|
|
164
|
+
onLoadEnd: __noteLoadEnd,
|
|
165
|
+
}, ...inner));
|
|
162
166
|
return wrapWithRouteLocations(serverMod, layoutLocation, layoutNode);
|
|
163
167
|
};
|
|
164
168
|
return { default: Wrapper };
|
|
@@ -276,8 +280,11 @@ export function defineRoutes(tree) {
|
|
|
276
280
|
serverRoutes: collectServerRoutes(tree),
|
|
277
281
|
};
|
|
278
282
|
}
|
|
279
|
-
export const Routes = ({ routes
|
|
280
|
-
return h(asRouteComponent(Router),
|
|
283
|
+
export const Routes = ({ routes }) => {
|
|
284
|
+
return h(asRouteComponent(Router), {
|
|
285
|
+
onLoadStart: __noteLoadStart,
|
|
286
|
+
onLoadEnd: __noteLoadEnd,
|
|
287
|
+
}, ...routes.flat.map((r) => h(Route, {
|
|
281
288
|
key: r.key,
|
|
282
289
|
path: r.path,
|
|
283
290
|
component: asRouteComponent(r.component),
|
package/dist/iso/form.js
CHANGED
|
@@ -34,6 +34,12 @@ export function Form({ action, children, ...rest }) {
|
|
|
34
34
|
? window.location.pathname + window.location.search
|
|
35
35
|
: '/';
|
|
36
36
|
const fd = new FormData(formEl);
|
|
37
|
+
// Source the action identity from props, not the DOM hidden inputs. On an
|
|
38
|
+
// initial SSR page those inputs render empty (server-side defineAction
|
|
39
|
+
// carries no name metadata) and Preact's hydrate() does not patch their
|
|
40
|
+
// values, so reading them back would post __module/__action='' and 404.
|
|
41
|
+
fd.set('__module', moduleKey);
|
|
42
|
+
fd.set('__action', actionName);
|
|
37
43
|
const payload = collectFormData(fd);
|
|
38
44
|
let handle;
|
|
39
45
|
if (optimistic)
|
package/dist/iso/index.d.ts
CHANGED
|
@@ -36,3 +36,9 @@ export type { RouteChangeHandler } from './route-change.js';
|
|
|
36
36
|
export { Head } from './head.js';
|
|
37
37
|
export type { HeadProps } from './head.js';
|
|
38
38
|
export { ClientScript } from './client-script.js';
|
|
39
|
+
export { useViewTransitionLifecycle, type ViewTransitionLifecycle, type ViewTransitionPhaseCallback, } from './view-transition-lifecycle.js';
|
|
40
|
+
export type { ViewTransitionEvent, NavDirection, ViewTransitionReason, } from './internal/view-transition-event.js';
|
|
41
|
+
export { useViewTransitionTypes, subscribeViewTransitionTypes, type ViewTransitionTypesInput, type ViewTransitionTypesNav, } from './view-transition-types.js';
|
|
42
|
+
export { getNavDirection as getViewTransitionDirection } from './internal/history-shim.js';
|
|
43
|
+
export { useViewTransitionName, useViewTransitionClass, ViewTransitionName, ViewTransitionGroup, type ViewTransitionNameProps, type ViewTransitionGroupProps, } from './view-transition-name.js';
|
|
44
|
+
export { Persist, PersistHost, type PersistProps } from './persist.js';
|
package/dist/iso/index.js
CHANGED
|
@@ -32,3 +32,12 @@ export { isBrowser, env } from './is-browser.js';
|
|
|
32
32
|
export { useRouteChange } from './route-change.js';
|
|
33
33
|
export { Head } from './head.js';
|
|
34
34
|
export { ClientScript } from './client-script.js';
|
|
35
|
+
// View transition lifecycle hook.
|
|
36
|
+
export { useViewTransitionLifecycle, } from './view-transition-lifecycle.js';
|
|
37
|
+
// View transitions types.
|
|
38
|
+
export { useViewTransitionTypes, subscribeViewTransitionTypes, } from './view-transition-types.js';
|
|
39
|
+
export { getNavDirection as getViewTransitionDirection } from './internal/history-shim.js';
|
|
40
|
+
// View transition name + group hooks and components.
|
|
41
|
+
export { useViewTransitionName, useViewTransitionClass, ViewTransitionName, ViewTransitionGroup, } from './view-transition-name.js';
|
|
42
|
+
// Persist components.
|
|
43
|
+
export { Persist, PersistHost } from './persist.js';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { NavDirection } from './view-transition-event.js';
|
|
2
|
+
export declare function onNavigation(listener: () => void): () => void;
|
|
3
|
+
export declare function installHistoryShim(): void;
|
|
4
|
+
export declare function getNavDirection(): NavDirection;
|
|
5
|
+
/**
|
|
6
|
+
* True once any client-side navigation (push, replace, or popstate) has
|
|
7
|
+
* occurred since the page loaded. Before the first navigation the document is
|
|
8
|
+
* still showing its server-rendered, freshly hydrated route. PageMiddlewareHost
|
|
9
|
+
* uses this to pick its render strategy: on the initial load it renders the
|
|
10
|
+
* server children during hydration and applies the client middleware outcome
|
|
11
|
+
* afterwards (so a Suspense boundary never resolves to non-SSR content
|
|
12
|
+
* mid-hydration, which would orphan the server route DOM); after a navigation
|
|
13
|
+
* it suspends on the chain normally. `lastDirection` only ever moves away from
|
|
14
|
+
* 'initial' (never back), so this is a monotonic "have we navigated yet" flag.
|
|
15
|
+
*/
|
|
16
|
+
export declare function hasClientNavigated(): boolean;
|
|
17
|
+
/** Test-only reset. Do not call from production code. */
|
|
18
|
+
export declare function resetHistoryShimForTesting(): void;
|
|
19
|
+
/** Test-only direction setter. Do not call from production code. */
|
|
20
|
+
export declare function setNavDirectionForTesting(dir: NavDirection): void;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
let installed = false;
|
|
2
|
+
let counter = 0;
|
|
3
|
+
let lastDirection = 'initial';
|
|
4
|
+
// Fires synchronously whenever a navigation occurs (pushState / replaceState /
|
|
5
|
+
// popstate), BEFORE the resulting re-render. Lets the view-transition scheduler
|
|
6
|
+
// observe a navigation at navigation time rather than only at render time (which
|
|
7
|
+
// Preact's render batching can coalesce away). See route-change.ts.
|
|
8
|
+
const navListeners = new Set();
|
|
9
|
+
export function onNavigation(listener) {
|
|
10
|
+
navListeners.add(listener);
|
|
11
|
+
return () => navListeners.delete(listener);
|
|
12
|
+
}
|
|
13
|
+
function notifyNavigation() {
|
|
14
|
+
for (const listener of navListeners)
|
|
15
|
+
listener();
|
|
16
|
+
}
|
|
17
|
+
let originalPush = null;
|
|
18
|
+
let originalReplace = null;
|
|
19
|
+
let popstateListener = null;
|
|
20
|
+
function readCounterFromState() {
|
|
21
|
+
if (typeof history === 'undefined')
|
|
22
|
+
return 0;
|
|
23
|
+
const state = history.state;
|
|
24
|
+
return state?.__hpVtIdx ?? 0;
|
|
25
|
+
}
|
|
26
|
+
export function installHistoryShim() {
|
|
27
|
+
if (installed)
|
|
28
|
+
return;
|
|
29
|
+
if (typeof history === 'undefined' || typeof window === 'undefined')
|
|
30
|
+
return;
|
|
31
|
+
installed = true;
|
|
32
|
+
counter = readCounterFromState();
|
|
33
|
+
lastDirection = 'initial';
|
|
34
|
+
originalPush = history.pushState.bind(history);
|
|
35
|
+
originalReplace = history.replaceState.bind(history);
|
|
36
|
+
history.pushState = function patchedPush(state, title, url) {
|
|
37
|
+
counter += 1;
|
|
38
|
+
const merged = {
|
|
39
|
+
...(state ?? {}),
|
|
40
|
+
__hpVtIdx: counter,
|
|
41
|
+
};
|
|
42
|
+
originalPush(merged, title, url);
|
|
43
|
+
lastDirection = 'push';
|
|
44
|
+
notifyNavigation();
|
|
45
|
+
};
|
|
46
|
+
history.replaceState = function patchedReplace(state, title, url) {
|
|
47
|
+
const merged = {
|
|
48
|
+
...(state ?? {}),
|
|
49
|
+
__hpVtIdx: counter,
|
|
50
|
+
};
|
|
51
|
+
originalReplace(merged, title, url);
|
|
52
|
+
lastDirection = 'replace';
|
|
53
|
+
notifyNavigation();
|
|
54
|
+
};
|
|
55
|
+
popstateListener = (e) => {
|
|
56
|
+
const incoming = e.state?.__hpVtIdx ?? 0;
|
|
57
|
+
lastDirection =
|
|
58
|
+
incoming < counter ? 'back' : incoming > counter ? 'forward' : 'replace';
|
|
59
|
+
counter = incoming;
|
|
60
|
+
notifyNavigation();
|
|
61
|
+
};
|
|
62
|
+
window.addEventListener('popstate', popstateListener, { capture: true });
|
|
63
|
+
// Stamp the current entry so subsequent diffs are well-defined.
|
|
64
|
+
if (history.state?.__hpVtIdx === undefined) {
|
|
65
|
+
originalReplace({ ...(history.state ?? {}), __hpVtIdx: counter }, '');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function getNavDirection() {
|
|
69
|
+
return lastDirection;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* True once any client-side navigation (push, replace, or popstate) has
|
|
73
|
+
* occurred since the page loaded. Before the first navigation the document is
|
|
74
|
+
* still showing its server-rendered, freshly hydrated route. PageMiddlewareHost
|
|
75
|
+
* uses this to pick its render strategy: on the initial load it renders the
|
|
76
|
+
* server children during hydration and applies the client middleware outcome
|
|
77
|
+
* afterwards (so a Suspense boundary never resolves to non-SSR content
|
|
78
|
+
* mid-hydration, which would orphan the server route DOM); after a navigation
|
|
79
|
+
* it suspends on the chain normally. `lastDirection` only ever moves away from
|
|
80
|
+
* 'initial' (never back), so this is a monotonic "have we navigated yet" flag.
|
|
81
|
+
*/
|
|
82
|
+
export function hasClientNavigated() {
|
|
83
|
+
return lastDirection !== 'initial';
|
|
84
|
+
}
|
|
85
|
+
/** Test-only reset. Do not call from production code. */
|
|
86
|
+
export function resetHistoryShimForTesting() {
|
|
87
|
+
if (installed &&
|
|
88
|
+
typeof history !== 'undefined' &&
|
|
89
|
+
originalPush &&
|
|
90
|
+
originalReplace) {
|
|
91
|
+
history.pushState = originalPush;
|
|
92
|
+
history.replaceState = originalReplace;
|
|
93
|
+
}
|
|
94
|
+
if (typeof window !== 'undefined' && popstateListener) {
|
|
95
|
+
window.removeEventListener('popstate', popstateListener, {
|
|
96
|
+
capture: true,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
installed = false;
|
|
100
|
+
counter = 0;
|
|
101
|
+
lastDirection = 'initial';
|
|
102
|
+
originalPush = null;
|
|
103
|
+
originalReplace = null;
|
|
104
|
+
popstateListener = null;
|
|
105
|
+
navListeners.clear();
|
|
106
|
+
}
|
|
107
|
+
/** Test-only direction setter. Do not call from production code. */
|
|
108
|
+
export function setNavDirectionForTesting(dir) {
|
|
109
|
+
lastDirection = dir;
|
|
110
|
+
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx } from "preact/jsx-runtime";
|
|
2
2
|
import { useLocation } from 'preact-iso';
|
|
3
3
|
import { Suspense } from 'preact/compat';
|
|
4
|
-
import { useContext, useEffect, useRef } from 'preact/hooks';
|
|
4
|
+
import { useContext, useEffect, useRef, useState } from 'preact/hooks';
|
|
5
5
|
import { isBrowser } from '../is-browser.js';
|
|
6
6
|
import { isRedirect, isRender } from '../outcomes.js';
|
|
7
7
|
import { dispatchServer, dispatchClient } from './middleware-runner.js';
|
|
8
8
|
import { partitionUse } from './use-partitioner.js';
|
|
9
9
|
import wrapPromise from './wrap-promise.js';
|
|
10
|
+
import { hasClientNavigated } from './history-shim.js';
|
|
10
11
|
import { HonoRequestContext } from './contexts.js';
|
|
11
12
|
function startChain(use, location, honoCtx) {
|
|
12
13
|
const { middleware } = partitionUse(use);
|
|
@@ -44,68 +45,147 @@ function startChain(use, location, honoCtx) {
|
|
|
44
45
|
inner: async () => undefined,
|
|
45
46
|
}).then((r) => r.kind === 'outcome' ? { outcome: r.outcome } : { outcome: undefined });
|
|
46
47
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const { route } = useLocation();
|
|
53
|
-
// Client-side redirect: navigate in an effect rather than during render.
|
|
54
|
-
// Render-time side effects are forbidden by Suspense semantics; doing
|
|
55
|
-
// route() in render would also fire on every Preact re-entry during
|
|
56
|
-
// suspension resume. Keyed on the resolved target so a fresh outcome
|
|
57
|
-
// for the same path doesn't refire (the outcome is cached per chain,
|
|
58
|
-
// so this only changes when the path itself changes and a new chain
|
|
59
|
-
// produces a redirect to a different target).
|
|
60
|
-
const redirectTo = isRedirect(outcome) && isBrowser() ? outcome.to : null;
|
|
61
|
-
useEffect(() => {
|
|
62
|
-
if (redirectTo !== null)
|
|
63
|
-
route(redirectTo);
|
|
64
|
-
// `route` is intentionally omitted from deps: it comes from
|
|
65
|
-
// useLocation() which is stable per LocationProvider mount, and
|
|
66
|
-
// referencing it here would re-fire the effect on every render the
|
|
67
|
-
// provider produces.
|
|
68
|
-
}, [redirectTo]);
|
|
48
|
+
/**
|
|
49
|
+
* Applies a settled host outcome to the rendered tree. Shared rendering logic
|
|
50
|
+
* for both host strategies (Suspense and Deferred).
|
|
51
|
+
*/
|
|
52
|
+
function renderOutcome(outcome, children) {
|
|
69
53
|
if (outcome === undefined) {
|
|
70
54
|
return _jsx(_Fragment, { children: children });
|
|
71
55
|
}
|
|
72
56
|
if (isRedirect(outcome)) {
|
|
73
57
|
if (isBrowser()) {
|
|
74
|
-
//
|
|
75
|
-
//
|
|
58
|
+
// The navigation is scheduled in an effect; render nothing meanwhile so
|
|
59
|
+
// the old tree doesn't briefly flash.
|
|
76
60
|
return null;
|
|
77
61
|
}
|
|
78
|
-
// Server: rethrow so renderPage's outer handler can translate to HTTP redirect.
|
|
62
|
+
// Server: rethrow so renderPage's outer handler can translate to an HTTP redirect.
|
|
79
63
|
throw outcome;
|
|
80
64
|
}
|
|
81
65
|
if (isRender(outcome)) {
|
|
82
66
|
const Alt = outcome.Component;
|
|
83
|
-
// Equality-by-reference semantics: each `render(Component)` call
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
// can wrap the returned component or vary props.
|
|
67
|
+
// Equality-by-reference semantics: each `render(Component)` call returns a
|
|
68
|
+
// fresh outcome object, but the wrapped chain caches its result for the
|
|
69
|
+
// lifetime of a path. Within the same path render outcomes are stable.
|
|
70
|
+
// Across paths the chain is re-dispatched, so a fresh chain produces a
|
|
71
|
+
// fresh outcome and Preact remounts naturally when `Alt` differs. If a
|
|
72
|
+
// middleware returns the SAME component reference across paths, Preact
|
|
73
|
+
// treats it as the same element and preserves state. That's the documented
|
|
74
|
+
// semantic; callers needing a forced remount can wrap the component or vary
|
|
75
|
+
// props.
|
|
93
76
|
return _jsx(Alt, {});
|
|
94
77
|
}
|
|
95
78
|
// Deny on the page-render path: rethrow so the outer error boundary or
|
|
96
79
|
// handler can translate to the right response.
|
|
97
80
|
throw outcome;
|
|
98
81
|
}
|
|
99
|
-
|
|
100
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Suspense strategy: suspend on the middleware chain and render its outcome.
|
|
84
|
+
* Used for SSR (prerender awaits the suspension) and for post-navigation client
|
|
85
|
+
* renders (no hydration to mismatch, a fallback is fine while the chain runs).
|
|
86
|
+
*/
|
|
87
|
+
function HostConsumer({ resultRef, children, }) {
|
|
88
|
+
// resultRef.current is populated by the parent before this consumer
|
|
89
|
+
// renders; the null branch is just a type-narrow guard.
|
|
90
|
+
const wrapped = resultRef.current;
|
|
91
|
+
const { outcome } = wrapped ? wrapped.read() : { outcome: undefined };
|
|
92
|
+
const { route } = useLocation();
|
|
93
|
+
// Client-side redirect: navigate in an effect rather than during render
|
|
94
|
+
// (render-time side effects are forbidden by Suspense semantics, and route()
|
|
95
|
+
// in render would also re-fire on every Preact re-entry during suspension
|
|
96
|
+
// resume). Keyed on the resolved target so a fresh outcome for the same path
|
|
97
|
+
// doesn't refire.
|
|
98
|
+
const redirectTo = isRedirect(outcome) && isBrowser() ? outcome.to : null;
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (redirectTo === null)
|
|
101
|
+
return;
|
|
102
|
+
// A plain SPA route() is correct here. This consumer only runs on the
|
|
103
|
+
// server (where the redirect outcome is thrown above, not routed) and for
|
|
104
|
+
// post-navigation client renders; initial-load redirects are handled by
|
|
105
|
+
// DeferredHost, which renders the SSR content during hydration so there is
|
|
106
|
+
// no server-committed tree for the Router to orphan. (The
|
|
107
|
+
// orphan-on-hydration-mismatch is expected Preact behavior, see
|
|
108
|
+
// preactjs/preact#4442.)
|
|
109
|
+
route(redirectTo);
|
|
110
|
+
// `route` is intentionally omitted from deps: it comes from useLocation()
|
|
111
|
+
// which is stable per LocationProvider mount, and referencing it here would
|
|
112
|
+
// re-fire the effect on every render the provider produces.
|
|
113
|
+
}, [redirectTo]);
|
|
114
|
+
return renderOutcome(outcome, children);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Deferred strategy: used on the INITIAL document load (browser, before any
|
|
118
|
+
* client navigation). Renders the server-rendered children during hydration so
|
|
119
|
+
* the hydrated DOM matches SSR, then runs the client chain post-hydration and
|
|
120
|
+
* applies its outcome as a normal update. This avoids a Suspense boundary
|
|
121
|
+
* resolving to non-SSR content mid-hydration, which orphans the server route
|
|
122
|
+
* DOM (expected Preact behavior, preactjs/preact#4442) and stacks the redirect
|
|
123
|
+
* target on top (the "client redirect double-mount").
|
|
124
|
+
*
|
|
125
|
+
* Timing contract: on the initial load the client middleware runs AFTER
|
|
126
|
+
* hydration (in the effect), not before first paint. The server already
|
|
127
|
+
* authorized and rendered this content, so a client guard is advisory here and
|
|
128
|
+
* applies once hydrated; anything it would redirect away from is already on
|
|
129
|
+
* screen from SSR, so deferring exposes nothing new. Post-navigation renders
|
|
130
|
+
* take SuspenseHost, where the chain blocks the render as before.
|
|
131
|
+
*
|
|
132
|
+
* Known limitation: this matches the COMMON case where the server passed and
|
|
133
|
+
* rendered `children`. If a SERVER middleware instead rendered an alternative
|
|
134
|
+
* (so SSR markup != children) and the client produces no matching outcome, the
|
|
135
|
+
* client still renders `children` and the SSR/client mismatch is on the user -
|
|
136
|
+
* the framework does not transmit the server outcome to the client. This is
|
|
137
|
+
* pre-existing (it predates the deferred strategy) and out of scope here.
|
|
138
|
+
*/
|
|
139
|
+
function DeferredHost({ use, location, honoCtx, children, }) {
|
|
140
|
+
const { route } = useLocation();
|
|
141
|
+
// null = chain not yet settled, or settled to a redirect/pass: keep rendering
|
|
142
|
+
// the server children. A settled render()/deny outcome is stored so it can be
|
|
143
|
+
// swapped in via a normal post-hydration update.
|
|
144
|
+
const [applied, setApplied] = useState(null);
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
let cancelled = false;
|
|
147
|
+
// Reset to the server children before re-dispatching, so a stale render()
|
|
148
|
+
// outcome from a previous path cannot linger if this host is ever reused
|
|
149
|
+
// across a path change (in practice it only handles the initial load, but
|
|
150
|
+
// a persisted layout host could in principle see location.path change).
|
|
151
|
+
setApplied(null);
|
|
152
|
+
startChain(use, location, honoCtx).then((result) => {
|
|
153
|
+
if (cancelled)
|
|
154
|
+
return;
|
|
155
|
+
if (isRedirect(result.outcome)) {
|
|
156
|
+
// SPA navigate from the fully hydrated tree. Because the server's
|
|
157
|
+
// content (not null) was rendered during hydration, there is no
|
|
158
|
+
// orphaned route DOM for the Router to stack the target on top of.
|
|
159
|
+
route(result.outcome.to);
|
|
160
|
+
}
|
|
161
|
+
else if (result.outcome !== undefined) {
|
|
162
|
+
// render() / deny: surface after hydration via a normal update.
|
|
163
|
+
setApplied(result);
|
|
164
|
+
}
|
|
165
|
+
// undefined (chain passed): keep the already-rendered children.
|
|
166
|
+
});
|
|
167
|
+
return () => {
|
|
168
|
+
cancelled = true;
|
|
169
|
+
};
|
|
170
|
+
// Re-dispatch if the path changes (mirrors SuspenseHost's per-path dispatch).
|
|
171
|
+
}, [location.path]);
|
|
172
|
+
if (applied === null)
|
|
173
|
+
return _jsx(_Fragment, { children: children });
|
|
174
|
+
return renderOutcome(applied.outcome, children);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Suspense strategy wrapper. Lazily dispatches the chain once per path (see the
|
|
178
|
+
* lazy-ref note below) and renders the outcome through HostConsumer.
|
|
179
|
+
*/
|
|
180
|
+
function SuspenseHost({ use, location, honoCtx, fallback, children, }) {
|
|
101
181
|
// Lazy ref pattern. `useRef(null)` serves a dual purpose: it's the
|
|
102
|
-
// "not-yet-computed" sentinel AND the persistent slot for the wrapped
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
182
|
+
// "not-yet-computed" sentinel AND the persistent slot for the wrapped chain
|
|
183
|
+
// result. We compute on first render and on subsequent renders ONLY when the
|
|
184
|
+
// path changed. `useRef(wrapPromise(startChain(...)))` would evaluate
|
|
185
|
+
// `startChain` every render before useRef decided whether to keep it, which
|
|
186
|
+
// synchronously fires `dispatchServer`/`dispatchClient` every render. That's
|
|
187
|
+
// O(renders) middleware invocations instead of O(navigations); auth checks,
|
|
188
|
+
// analytics, redirects would all repeat.
|
|
109
189
|
const resultRef = useRef(null);
|
|
110
190
|
const prevPath = useRef(location.path);
|
|
111
191
|
if (resultRef.current === null) {
|
|
@@ -116,4 +196,27 @@ export const PageMiddlewareHost = ({ use = [], location, fallback, children }) =
|
|
|
116
196
|
resultRef.current = wrapPromise(startChain(use, location, honoCtx));
|
|
117
197
|
}
|
|
118
198
|
return (_jsx(Suspense, { fallback: fallback, children: _jsx(HostConsumer, { resultRef: resultRef, children: children }) }));
|
|
199
|
+
}
|
|
200
|
+
export const PageMiddlewareHost = ({ use = [], location, fallback, children }) => {
|
|
201
|
+
const honoCtx = useContext(HonoRequestContext).context;
|
|
202
|
+
// Choose the render strategy ONCE per mount so the hook order stays stable
|
|
203
|
+
// across renders (hasClientNavigated() flips to true after the first
|
|
204
|
+
// navigation, which would otherwise change which child - and its hooks - we
|
|
205
|
+
// render).
|
|
206
|
+
//
|
|
207
|
+
// Initial document load (browser, no navigation yet) -> DeferredHost: a
|
|
208
|
+
// Suspense boundary that suspends during hydration and resolves to non-SSR
|
|
209
|
+
// content (e.g. null for a redirect) orphans the server-rendered route DOM
|
|
210
|
+
// (expected Preact behavior, preactjs/preact#4442), which the Router then
|
|
211
|
+
// stacks the redirect target on top of. Rendering the server children during
|
|
212
|
+
// hydration and applying the client outcome afterwards removes that mismatch.
|
|
213
|
+
//
|
|
214
|
+
// Server render and post-navigation client renders have no hydration to
|
|
215
|
+
// mismatch, so the Suspense path (suspend on the chain, render the outcome)
|
|
216
|
+
// is correct there.
|
|
217
|
+
const deferRef = useRef(isBrowser() && !hasClientNavigated());
|
|
218
|
+
if (deferRef.current) {
|
|
219
|
+
return (_jsx(DeferredHost, { use: use, location: location, honoCtx: honoCtx, children: children }));
|
|
220
|
+
}
|
|
221
|
+
return (_jsx(SuspenseHost, { use: use, location: location, honoCtx: honoCtx, fallback: fallback, children: children }));
|
|
119
222
|
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ComponentChildren } from 'preact';
|
|
2
|
+
export interface PersistEntry {
|
|
3
|
+
children: ComponentChildren;
|
|
4
|
+
viewTransitionName: string | undefined;
|
|
5
|
+
}
|
|
6
|
+
export declare function __persistRegistryWrite(id: string, entry: PersistEntry): void;
|
|
7
|
+
export declare function __persistRegistryRead(): ReadonlyMap<string, PersistEntry>;
|
|
8
|
+
export declare function __persistRegistrySubscribe(sub: () => void): () => void;
|
|
9
|
+
/** Test-only reset. Do not call from production code. */
|
|
10
|
+
export declare function __persistRegistryResetForTesting(): void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
let map = new Map();
|
|
2
|
+
const subs = new Set();
|
|
3
|
+
export function __persistRegistryWrite(id, entry) {
|
|
4
|
+
// Replace the map reference so consumers using identity-checks can detect.
|
|
5
|
+
const next = new Map(map);
|
|
6
|
+
next.set(id, entry);
|
|
7
|
+
map = next;
|
|
8
|
+
for (const sub of subs)
|
|
9
|
+
sub();
|
|
10
|
+
}
|
|
11
|
+
export function __persistRegistryRead() {
|
|
12
|
+
return map;
|
|
13
|
+
}
|
|
14
|
+
export function __persistRegistrySubscribe(sub) {
|
|
15
|
+
subs.add(sub);
|
|
16
|
+
return () => {
|
|
17
|
+
subs.delete(sub);
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** Test-only reset. Do not call from production code. */
|
|
21
|
+
export function __persistRegistryResetForTesting() {
|
|
22
|
+
map = new Map();
|
|
23
|
+
subs.clear();
|
|
24
|
+
}
|
|
@@ -1,4 +1,27 @@
|
|
|
1
|
-
|
|
1
|
+
import { ViewTransitionEvent } from './view-transition-event.js';
|
|
2
|
+
export type PhaseName = 'beforeTransition' | 'beforeSwap' | 'afterSwap' | 'afterTransition';
|
|
3
|
+
type PhaseSub = (event: ViewTransitionEvent) => void | Promise<void>;
|
|
4
|
+
type LegacySub = (to: string, from: string | undefined) => void;
|
|
5
|
+
export declare function __subscribePhase(phase: PhaseName, sub: PhaseSub): () => void;
|
|
6
|
+
export declare function __subscribeRouteChange(sub: LegacySub): () => void;
|
|
7
|
+
export declare function __noteLoadStart(): void;
|
|
8
|
+
export declare function __noteLoadEnd(): void;
|
|
9
|
+
/** @internal Test-only reset for coordinator state. */
|
|
10
|
+
export declare function __resetTransitionStateForTesting(): void;
|
|
11
|
+
/**
|
|
12
|
+
* @internal Install the view-transition render scheduler (client only).
|
|
13
|
+
*
|
|
14
|
+
* Takes ownership of `options.debounceRendering`: it captures the previous value
|
|
15
|
+
* as `prevDebounce` (delegated to for every non-navigation flush) and installs
|
|
16
|
+
* `scheduleRender` in its place. This assumes nothing else permanently overrides
|
|
17
|
+
* `options.debounceRendering` afterward. `preact/compat`'s `flushSync` swaps it
|
|
18
|
+
* temporarily and restores it, so that composes fine; a second permanent
|
|
19
|
+
* override would shadow this scheduler (the install is idempotent, so calling it
|
|
20
|
+
* twice is a no-op, not a double-install). Reversed by
|
|
21
|
+
* `__resetTransitionStateForTesting`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function installNavTransitionScheduler(): void;
|
|
2
24
|
export declare function __dispatchRouteChange(to: string, from: string | undefined): void;
|
|
3
|
-
|
|
25
|
+
/** @internal Test-only reset for default-types installer. */
|
|
26
|
+
export declare function resetDefaultTypesForTesting(): void;
|
|
4
27
|
export {};
|