hono-preact 0.4.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.
@@ -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, null, ...inner));
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, onRouteChange, }) => {
280
- return h(asRouteComponent(Router), onRouteChange ? { onRouteChange } : null, ...routes.flat.map((r) => h(Route, {
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)
@@ -38,7 +38,7 @@ export type { HeadProps } from './head.js';
38
38
  export { ClientScript } from './client-script.js';
39
39
  export { useViewTransitionLifecycle, type ViewTransitionLifecycle, type ViewTransitionPhaseCallback, } from './view-transition-lifecycle.js';
40
40
  export type { ViewTransitionEvent, NavDirection, ViewTransitionReason, } from './internal/view-transition-event.js';
41
- export { useViewTransitionTypes, type ViewTransitionTypesInput, type ViewTransitionTypesNav, } from './view-transition-types.js';
41
+ export { useViewTransitionTypes, subscribeViewTransitionTypes, type ViewTransitionTypesInput, type ViewTransitionTypesNav, } from './view-transition-types.js';
42
42
  export { getNavDirection as getViewTransitionDirection } from './internal/history-shim.js';
43
43
  export { useViewTransitionName, useViewTransitionClass, ViewTransitionName, ViewTransitionGroup, type ViewTransitionNameProps, type ViewTransitionGroupProps, } from './view-transition-name.js';
44
44
  export { Persist, PersistHost, type PersistProps } from './persist.js';
package/dist/iso/index.js CHANGED
@@ -35,7 +35,7 @@ export { ClientScript } from './client-script.js';
35
35
  // View transition lifecycle hook.
36
36
  export { useViewTransitionLifecycle, } from './view-transition-lifecycle.js';
37
37
  // View transitions types.
38
- export { useViewTransitionTypes, } from './view-transition-types.js';
38
+ export { useViewTransitionTypes, subscribeViewTransitionTypes, } from './view-transition-types.js';
39
39
  export { getNavDirection as getViewTransitionDirection } from './internal/history-shim.js';
40
40
  // View transition name + group hooks and components.
41
41
  export { useViewTransitionName, useViewTransitionClass, ViewTransitionName, ViewTransitionGroup, } from './view-transition-name.js';
@@ -1,6 +1,19 @@
1
1
  import type { NavDirection } from './view-transition-event.js';
2
+ export declare function onNavigation(listener: () => void): () => void;
2
3
  export declare function installHistoryShim(): void;
3
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;
4
17
  /** Test-only reset. Do not call from production code. */
5
18
  export declare function resetHistoryShimForTesting(): void;
6
19
  /** Test-only direction setter. Do not call from production code. */
@@ -1,6 +1,19 @@
1
1
  let installed = false;
2
2
  let counter = 0;
3
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
+ }
4
17
  let originalPush = null;
5
18
  let originalReplace = null;
6
19
  let popstateListener = null;
@@ -28,6 +41,7 @@ export function installHistoryShim() {
28
41
  };
29
42
  originalPush(merged, title, url);
30
43
  lastDirection = 'push';
44
+ notifyNavigation();
31
45
  };
32
46
  history.replaceState = function patchedReplace(state, title, url) {
33
47
  const merged = {
@@ -36,12 +50,14 @@ export function installHistoryShim() {
36
50
  };
37
51
  originalReplace(merged, title, url);
38
52
  lastDirection = 'replace';
53
+ notifyNavigation();
39
54
  };
40
55
  popstateListener = (e) => {
41
56
  const incoming = e.state?.__hpVtIdx ?? 0;
42
57
  lastDirection =
43
58
  incoming < counter ? 'back' : incoming > counter ? 'forward' : 'replace';
44
59
  counter = incoming;
60
+ notifyNavigation();
45
61
  };
46
62
  window.addEventListener('popstate', popstateListener, { capture: true });
47
63
  // Stamp the current entry so subsequent diffs are well-defined.
@@ -52,6 +68,20 @@ export function installHistoryShim() {
52
68
  export function getNavDirection() {
53
69
  return lastDirection;
54
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
+ }
55
85
  /** Test-only reset. Do not call from production code. */
56
86
  export function resetHistoryShimForTesting() {
57
87
  if (installed &&
@@ -72,6 +102,7 @@ export function resetHistoryShimForTesting() {
72
102
  originalPush = null;
73
103
  originalReplace = null;
74
104
  popstateListener = null;
105
+ navListeners.clear();
75
106
  }
76
107
  /** Test-only direction setter. Do not call from production code. */
77
108
  export function setNavDirectionForTesting(dir) {
@@ -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
- function HostConsumer({ resultRef, children, }) {
48
- // resultRef.current is populated by the parent before this consumer
49
- // renders; the null branch is just a type-narrow guard.
50
- const wrapped = resultRef.current;
51
- const { outcome } = wrapped ? wrapped.read() : { outcome: undefined };
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
- // Effect above will schedule the navigation; render nothing in the
75
- // meantime so the old tree doesn't briefly flash.
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
- // returns a fresh outcome object, but the wrapped chain caches its
85
- // result for the lifetime of a path. Within the same path render
86
- // outcomes are stable. Across paths, the `resultRef` is rewrapped
87
- // (see PageMiddlewareHost below), so a fresh chain produces a fresh
88
- // outcome and Preact remounts naturally when `Alt` differs. If a
89
- // middleware returns the SAME component reference across paths,
90
- // Preact treats it as the same element and preserves state. That's
91
- // the documented semantic; if callers need a forced remount, they
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
- export const PageMiddlewareHost = ({ use = [], location, fallback, children }) => {
100
- const honoCtx = useContext(HonoRequestContext).context;
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
- // chain result. We compute on first render and on subsequent renders
104
- // ONLY when the path changed. `useRef(wrapPromise(startChain(...)))`
105
- // would evaluate `startChain` every render before useRef decided whether
106
- // to keep it, which synchronously fires `dispatchServer`/`dispatchClient`
107
- // every render. That's O(renders) middleware invocations instead of
108
- // O(navigations); auth checks, analytics, redirects would all repeat.
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
  };
@@ -4,6 +4,23 @@ type PhaseSub = (event: ViewTransitionEvent) => void | Promise<void>;
4
4
  type LegacySub = (to: string, from: string | undefined) => void;
5
5
  export declare function __subscribePhase(phase: PhaseName, sub: PhaseSub): () => void;
6
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;
7
24
  export declare function __dispatchRouteChange(to: string, from: string | undefined): void;
8
25
  /** @internal Test-only reset for default-types installer. */
9
26
  export declare function resetDefaultTypesForTesting(): void;
@@ -1,6 +1,6 @@
1
- import { flushSync } from 'preact/compat';
1
+ import { options } from 'preact';
2
2
  import { ViewTransitionEvent, } from './view-transition-event.js';
3
- import { getNavDirection } from './history-shim.js';
3
+ import { getNavDirection, onNavigation } from './history-shim.js';
4
4
  const phaseSubs = {
5
5
  beforeTransition: new Set(),
6
6
  beforeSwap: new Set(),
@@ -30,71 +30,377 @@ function getStartViewTransition() {
30
30
  const fn = document.startViewTransition;
31
31
  return typeof fn === 'function' ? fn.bind(document) : undefined;
32
32
  }
33
- export function __dispatchRouteChange(to, from) {
33
+ function currentPath() {
34
+ return typeof location !== 'undefined'
35
+ ? location.pathname + location.search
36
+ : '';
37
+ }
38
+ // `loadingDepth`: how many Routers are mid-suspense (via onLoadStart/onLoadEnd).
39
+ // Read right after a navigation commits to tell whether the route suspended (a
40
+ // cold navigation), so the transition can wait for the suspended content.
41
+ let loadingDepth = 0;
42
+ export function __noteLoadStart() {
43
+ loadingDepth++;
44
+ }
45
+ export function __noteLoadEnd() {
46
+ loadingDepth = Math.max(0, loadingDepth - 1);
47
+ }
48
+ // The path the app is currently on (the previous navigation's `to`); seeds
49
+ // `from` for the first navigation. A server render leaves it undefined.
50
+ let lastPath = typeof location !== 'undefined'
51
+ ? location.pathname + location.search
52
+ : undefined;
53
+ // Cold-navigation state: a navigation's transition holds until the suspending
54
+ // route's content flushes have all run (see the scheduler below).
55
+ let coldTimeout = null;
56
+ // Bumped per navigation so a superseded transition's (async) callback bows out.
57
+ let navGen = 0;
58
+ // Cap on how long a navigation holds the old snapshot waiting for a suspending
59
+ // route's content. Past it the navigation completes without finishing the
60
+ // transition rather than freezing the page on a slow/stalled load.
61
+ const COLD_COMMIT_TIMEOUT_MS = 500;
62
+ // Extra grace, after the shell is ready, to let a morph partner that loads with
63
+ // the route's DATA (behind inner Suspense, which doesn't move loadingDepth)
64
+ // appear in the new snapshot before the transition captures it.
65
+ const MORPH_PARTNER_GRACE_MS = 150;
66
+ /** @internal Test-only reset for coordinator state. */
67
+ export function __resetTransitionStateForTesting() {
68
+ loadingDepth = 0;
69
+ navGen = 0;
70
+ lastPath =
71
+ typeof location !== 'undefined'
72
+ ? location.pathname + location.search
73
+ : undefined;
74
+ coldRouteSignal = null;
75
+ if (coldTimeout !== null) {
76
+ clearTimeout(coldTimeout);
77
+ coldTimeout = null;
78
+ }
79
+ transitionActive = false;
80
+ // Uninstall the render scheduler (restoring Preact's debounceRendering) so
81
+ // each test can install it fresh.
82
+ if (schedulerInstalled) {
83
+ options.debounceRendering = prevDebounce;
84
+ schedulerInstalled = false;
85
+ prevDebounce = undefined;
86
+ if (unsubscribeNav) {
87
+ unsubscribeNav();
88
+ unsubscribeNav = null;
89
+ }
90
+ }
91
+ }
92
+ function fireAfterSwap(event) {
93
+ for (const sub of phaseSubs.afterSwap)
94
+ sub(event);
95
+ // Legacy subscribers fire at the afterSwap slot: after the DOM swap, before
96
+ // the browser begins animating the new frame.
97
+ fireLegacy(event.to, event.from);
98
+ }
99
+ function fireAfterTransition(event, reason) {
100
+ if (reason !== undefined)
101
+ event.reason = reason;
102
+ for (const sub of phaseSubs.afterTransition)
103
+ sub(event);
104
+ }
105
+ function applyTypes(transition, types) {
106
+ const vtTypes = transition.types;
107
+ if (vtTypes && typeof vtTypes.add === 'function') {
108
+ for (const t of types)
109
+ vtTypes.add(t);
110
+ }
111
+ }
112
+ function skipTransition(transition) {
113
+ const t = transition;
114
+ if (typeof t.skipTransition === 'function')
115
+ t.skipTransition();
116
+ }
117
+ // Build the event for a navigation that has just committed: `to`/`direction`
118
+ // are only correct after the commit (pushState updates the history shim), so
119
+ // this is always called post-commit. Advances `lastPath`.
120
+ function buildEvent(from) {
34
121
  ensureDefaultTypes();
122
+ const to = currentPath();
123
+ lastPath = to;
35
124
  const direction = getNavDirection();
36
125
  const event = new ViewTransitionEvent({ to, from, direction });
37
126
  for (const sub of phaseSubs.beforeTransition)
38
127
  sub(event);
39
- const fireAfterSwap = () => {
40
- for (const sub of phaseSubs.afterSwap)
41
- sub(event);
42
- // Legacy subscribers fire at the afterSwap slot: after the DOM swap,
43
- // before the browser begins animating the new frame.
44
- fireLegacy(to, from);
45
- };
46
- const fireAfterTransition = (reason) => {
47
- if (reason !== undefined)
48
- event.reason = reason;
49
- for (const sub of phaseSubs.afterTransition)
50
- sub(event);
51
- };
52
- if (event._skipped) {
53
- flushSync(() => { });
54
- fireAfterSwap();
55
- fireAfterTransition('skipped');
128
+ return event;
129
+ }
130
+ let schedulerInstalled = false;
131
+ let lastHref = '';
132
+ let prevDebounce;
133
+ let unsubscribeNav = null;
134
+ // True while a navigation's transition is in flight (its callback is pending or
135
+ // awaiting cold content). Lets the navigation observer abandon it.
136
+ let transitionActive = false;
137
+ // Set while a cold navigation's transition awaits a content flush; the next
138
+ // same-URL flush hands its `process` here (or `null` on supersede/timeout) so
139
+ // the transition can run it inside itself.
140
+ let coldRouteSignal = null;
141
+ function defaultSchedule(process) {
142
+ if (prevDebounce)
143
+ prevDebounce(process);
144
+ else
145
+ Promise.resolve().then(process);
146
+ }
147
+ // Fired (via the history shim) at navigation time, before the re-render. If a
148
+ // transition from a previous navigation is still in flight, abandon it here:
149
+ // Preact may coalesce the new navigation's render into the in-flight one, so
150
+ // scheduleRender never sees it and its own supersede branch can't fire.
151
+ function onNavObserved() {
152
+ if (!transitionActive && !coldRouteSignal)
56
153
  return;
154
+ navGen++; // the in-flight callback bows out at its next navGen check
155
+ transitionActive = false;
156
+ if (coldRouteSignal) {
157
+ const resolve = coldRouteSignal;
158
+ coldRouteSignal = null;
159
+ if (coldTimeout !== null) {
160
+ clearTimeout(coldTimeout);
161
+ coldTimeout = null;
162
+ }
163
+ resolve(null);
57
164
  }
58
- const start = getStartViewTransition();
165
+ }
166
+ /**
167
+ * @internal Install the view-transition render scheduler (client only).
168
+ *
169
+ * Takes ownership of `options.debounceRendering`: it captures the previous value
170
+ * as `prevDebounce` (delegated to for every non-navigation flush) and installs
171
+ * `scheduleRender` in its place. This assumes nothing else permanently overrides
172
+ * `options.debounceRendering` afterward. `preact/compat`'s `flushSync` swaps it
173
+ * temporarily and restores it, so that composes fine; a second permanent
174
+ * override would shadow this scheduler (the install is idempotent, so calling it
175
+ * twice is a no-op, not a double-install). Reversed by
176
+ * `__resetTransitionStateForTesting`.
177
+ */
178
+ export function installNavTransitionScheduler() {
179
+ if (schedulerInstalled)
180
+ return;
181
+ if (typeof document === 'undefined' || typeof location === 'undefined')
182
+ return;
183
+ schedulerInstalled = true;
184
+ lastHref = location.href;
185
+ prevDebounce = options.debounceRendering;
186
+ options.debounceRendering = scheduleRender;
187
+ unsubscribeNav = onNavigation(onNavObserved);
188
+ }
189
+ function scheduleRender(process) {
190
+ const href = location.href;
191
+ const navigated = href !== lastHref;
192
+ // The content flush for an in-flight cold navigation (same URL): hand it back
193
+ // to that transition so it lands in the new snapshot.
194
+ if (coldRouteSignal && !navigated) {
195
+ const resolve = coldRouteSignal;
196
+ coldRouteSignal = null;
197
+ if (coldTimeout !== null) {
198
+ clearTimeout(coldTimeout);
199
+ coldTimeout = null;
200
+ }
201
+ resolve(process); // the transition's callback runs `process()` itself
202
+ return;
203
+ }
204
+ // A new navigation arrived while a cold one was still loading: abandon it.
205
+ if (coldRouteSignal && navigated) {
206
+ navGen++;
207
+ const resolve = coldRouteSignal;
208
+ coldRouteSignal = null;
209
+ if (coldTimeout !== null) {
210
+ clearTimeout(coldTimeout);
211
+ coldTimeout = null;
212
+ }
213
+ resolve(null);
214
+ }
215
+ if (navigated) {
216
+ // Reset the load counter at the start of a navigation. A previous route's
217
+ // loads are abandoned by a new navigation, and preact-iso fires onLoadStart
218
+ // without a matching onLoadEnd when a still-suspended Router unmounts (it
219
+ // emits onLoadEnd only on a committed render, not on unmount). Left alone,
220
+ // that leaked depth would make this nav (and later ones) look perpetually
221
+ // cold and burn the cold-load timeout. This nav re-increments it as its own
222
+ // route suspends.
223
+ loadingDepth = 0;
224
+ }
225
+ lastHref = href;
226
+ const start = navigated ? getStartViewTransition() : undefined;
59
227
  if (!start) {
60
- flushSync(() => { });
61
- fireAfterSwap();
62
- fireAfterTransition('unsupported');
228
+ defaultSchedule(process);
63
229
  return;
64
230
  }
65
- const transition = start(() => {
66
- flushSync(() => { });
231
+ runNavTransition(process, start);
232
+ }
233
+ // Wait for the next content flush of an in-flight cold navigation (routed here
234
+ // by scheduleRender), or null on timeout/supersede.
235
+ function waitForColdFlush(myGen, timeoutMs) {
236
+ return new Promise((resolve) => {
237
+ coldRouteSignal = resolve;
238
+ coldTimeout = setTimeout(() => {
239
+ if (navGen === myGen && coldRouteSignal) {
240
+ coldRouteSignal = null;
241
+ coldTimeout = null;
242
+ resolve(null);
243
+ }
244
+ }, timeoutMs);
245
+ });
246
+ }
247
+ // Elements carrying an inline `view-transition-name`. The attribute selector
248
+ // lets the browser filter natively instead of walking every node in JS — this
249
+ // runs on the frozen hot path (inside the transition callback, possibly once per
250
+ // grace tick), so the candidate set should be as small as possible. The selector
251
+ // is a substring match on the serialized `style` attribute, so we still confirm
252
+ // each match by reading the resolved property below.
253
+ function queryVtNamedElements() {
254
+ if (typeof document === 'undefined' || !document.querySelectorAll)
255
+ return [];
256
+ return Array.from(document.querySelectorAll('[style*="view-transition-name"]'));
257
+ }
258
+ // The view-transition-names currently applied in the document (inline styles).
259
+ function collectVtNames() {
260
+ const names = new Set();
261
+ for (const el of queryVtNamedElements()) {
262
+ const n = el.style?.getPropertyValue?.('view-transition-name');
263
+ if (n)
264
+ names.add(n);
265
+ }
266
+ return names;
267
+ }
268
+ // Whether any currently-applied view-transition-name was also in `oldNames` —
269
+ // i.e. a morph pair (same name old + new) is present.
270
+ function hasMorphPartner(oldNames) {
271
+ if (oldNames.size === 0)
272
+ return false;
273
+ for (const el of queryVtNamedElements()) {
274
+ const n = el.style?.getPropertyValue?.('view-transition-name');
275
+ if (n && oldNames.has(n))
276
+ return true;
277
+ }
278
+ return false;
279
+ }
280
+ function runNavTransition(process, start) {
281
+ const from = lastPath;
282
+ // The names present in the outgoing route — used to know when a morph partner
283
+ // has appeared in the new route (see the grace wait below).
284
+ const oldNames = collectVtNames();
285
+ const myGen = ++navGen;
286
+ transitionActive = true;
287
+ let transition;
288
+ let event;
289
+ try {
290
+ transition = start(async () => {
291
+ // The old snapshot has been captured. Flush the navigation render.
292
+ process();
293
+ if (navGen !== myGen)
294
+ return;
295
+ event = buildEvent(from);
296
+ event.transition = transition;
297
+ applyTypes(transition, event.types);
298
+ if (event._skipped) {
299
+ skipTransition(transition);
300
+ }
301
+ else {
302
+ for (const sub of phaseSubs.beforeSwap)
303
+ sub(event);
304
+ // Cold: the route suspended. Keep routing its content flushes into the
305
+ // transition until every route module has loaded (loadingDepth back to
306
+ // 0) — the page-level shell.
307
+ while (loadingDepth > 0) {
308
+ const contentProcess = await waitForColdFlush(myGen, COLD_COMMIT_TIMEOUT_MS);
309
+ if (navGen !== myGen)
310
+ return;
311
+ if (!contentProcess)
312
+ break; // timed out waiting
313
+ contentProcess();
314
+ }
315
+ // If the outgoing route had named elements but none has a partner in the
316
+ // new shell yet, the partner may load with the route's DATA (behind
317
+ // inner Suspense, which doesn't move loadingDepth — e.g. a list whose
318
+ // items come from a loader). Wait briefly for it so the morph can pair.
319
+ if (oldNames.size > 0 && !hasMorphPartner(oldNames)) {
320
+ while (!hasMorphPartner(oldNames)) {
321
+ const contentProcess = await waitForColdFlush(myGen, MORPH_PARTNER_GRACE_MS);
322
+ if (navGen !== myGen)
323
+ return;
324
+ if (!contentProcess)
325
+ break; // grace expired — capture as-is
326
+ contentProcess();
327
+ }
328
+ }
329
+ }
330
+ if (navGen !== myGen)
331
+ return;
332
+ fireAfterSwap(event);
333
+ transitionActive = false; // reached only when still current
334
+ });
335
+ }
336
+ catch {
337
+ // Non-conformant startViewTransition: just flush and fire the post phases.
338
+ transitionActive = false;
339
+ process();
340
+ const ev = buildEvent(from);
341
+ fireAfterSwap(ev);
342
+ fireAfterTransition(ev, 'unsupported');
343
+ return;
344
+ }
345
+ transition.finished.then(() => {
346
+ if (event)
347
+ fireAfterTransition(event, event._skipped ? 'skipped' : undefined);
348
+ }, () => {
349
+ if (event)
350
+ fireAfterTransition(event, 'aborted');
351
+ });
352
+ }
353
+ // Synchronous route-change dispatch for an explicit `to`/`from`: fires
354
+ // `beforeTransition` and runs a transition that wraps a no-op swap (the route is
355
+ // assumed already on screen), firing the post-swap phases and applying types.
356
+ // Production navigations are driven by the scheduler (installNavTransition
357
+ // scheduler); this drives the same phase/type/lifecycle machinery directly for
358
+ // callers that change the route outside the normal navigation flow (and in unit
359
+ // tests).
360
+ export function __dispatchRouteChange(to, from) {
361
+ ensureDefaultTypes();
362
+ const event = new ViewTransitionEvent({
363
+ to,
364
+ from,
365
+ direction: getNavDirection(),
67
366
  });
68
- // Set event.transition before firing beforeSwap so all subsequent phase
69
- // subscribers see a non-null transition. In real browsers startViewTransition
70
- // invokes the callback asynchronously, meaning transition is set here before
71
- // the browser calls the update function. In synchronous test mocks the
72
- // callback returns before start() does, so we set transition here (after
73
- // start() returns) and then fire the post-swap phases manually.
367
+ for (const sub of phaseSubs.beforeTransition)
368
+ sub(event);
369
+ const start = getStartViewTransition();
370
+ if (!start || event._skipped) {
371
+ // No transition runs, so `beforeSwap` (which precedes a real swap) is
372
+ // skipped; the post-swap phases still fire.
373
+ fireAfterSwap(event);
374
+ fireAfterTransition(event, event._skipped ? 'skipped' : 'unsupported');
375
+ return;
376
+ }
377
+ let transition;
378
+ try {
379
+ transition = start(() => { });
380
+ }
381
+ catch {
382
+ fireAfterSwap(event);
383
+ fireAfterTransition(event, 'unsupported');
384
+ return;
385
+ }
74
386
  event.transition = transition;
387
+ applyTypes(transition, event.types);
75
388
  for (const sub of phaseSubs.beforeSwap)
76
389
  sub(event);
77
- fireAfterSwap();
78
- // Apply types accumulated across all phases. beforeTransition and beforeSwap
79
- // have both run by this point, so the full set of types is available here.
80
- const vtTypes = transition.types;
81
- if (vtTypes && typeof vtTypes.add === 'function') {
82
- for (const t of event.types)
83
- vtTypes.add(t);
84
- }
85
- transition.finished.then(() => fireAfterTransition(), () => fireAfterTransition('aborted'));
390
+ fireAfterSwap(event);
391
+ transition.finished.then(() => fireAfterTransition(event), () => fireAfterTransition(event, 'aborted'));
86
392
  }
87
393
  let defaultTypesInstalled = false;
88
- let firstDispatchSeen = false;
394
+ let firstNavSeen = false;
89
395
  let defaultTypeUnsubscriber = null;
90
396
  function ensureDefaultTypes() {
91
397
  if (defaultTypesInstalled)
92
398
  return;
93
399
  defaultTypesInstalled = true;
94
400
  defaultTypeUnsubscriber = __subscribePhase('beforeTransition', (event) => {
95
- if (!firstDispatchSeen) {
401
+ if (!firstNavSeen) {
96
402
  event.types.push('nav-initial');
97
- firstDispatchSeen = true;
403
+ firstNavSeen = true;
98
404
  }
99
405
  else {
100
406
  event.types.push(`nav-${event.direction}`);
@@ -108,6 +414,6 @@ export function resetDefaultTypesForTesting() {
108
414
  defaultTypeUnsubscriber();
109
415
  }
110
416
  defaultTypesInstalled = false;
111
- firstDispatchSeen = false;
417
+ firstNavSeen = false;
112
418
  defaultTypeUnsubscriber = null;
113
419
  }
@@ -11,7 +11,7 @@ export { runRequestScope, getRequestStore, captureRequestScope, getActionResultS
11
11
  export { default as wrapPromise } from './internal/wrap-promise.js';
12
12
  export { HonoRequestContext } from './internal/contexts.js';
13
13
  export { PageMiddlewareHost } from './internal/page-middleware-host.js';
14
- export { __dispatchRouteChange, __subscribeRouteChange, } from './internal/route-change.js';
14
+ export { installNavTransitionScheduler, __subscribeRouteChange, } from './internal/route-change.js';
15
15
  export { installHistoryShim, getNavDirection, } from './internal/history-shim.js';
16
16
  export { __subscribePhase, type PhaseName } from './internal/route-change.js';
17
17
  export { ViewTransitionEvent, type NavDirection, type ViewTransitionReason, } from './internal/view-transition-event.js';
@@ -37,7 +37,7 @@ export { runRequestScope, getRequestStore, captureRequestScope, getActionResultS
37
37
  export { default as wrapPromise } from './internal/wrap-promise.js';
38
38
  export { HonoRequestContext } from './internal/contexts.js';
39
39
  export { PageMiddlewareHost } from './internal/page-middleware-host.js';
40
- export { __dispatchRouteChange, __subscribeRouteChange, } from './internal/route-change.js';
40
+ export { installNavTransitionScheduler, __subscribeRouteChange, } from './internal/route-change.js';
41
41
  export { installHistoryShim, getNavDirection, } from './internal/history-shim.js';
42
42
  export { __subscribePhase } from './internal/route-change.js';
43
43
  export { ViewTransitionEvent, } from './internal/view-transition-event.js';
@@ -5,4 +5,15 @@ export interface ViewTransitionTypesNav {
5
5
  direction: NavDirection;
6
6
  }
7
7
  export type ViewTransitionTypesInput = string | string[] | ((nav: ViewTransitionTypesNav) => string | string[] | null | undefined);
8
+ /**
9
+ * Register a global, route-aware view-transition type rule. The resolver runs on
10
+ * every navigation with `{ to, from, direction }` and returns the type(s) to add
11
+ * to that navigation's transition (a static string/array adds the same type(s) to
12
+ * every navigation). Returns an unsubscribe.
13
+ *
14
+ * Unlike {@link useViewTransitionTypes}, this is not tied to a mounted component,
15
+ * so it covers entering AND leaving a section (a layout hook is not subscribed yet
16
+ * on enter and is already torn down on leave). No-op on the server (no document).
17
+ */
18
+ export declare function subscribeViewTransitionTypes(input: ViewTransitionTypesInput): () => void;
8
19
  export declare function useViewTransitionTypes(input: ViewTransitionTypesInput): void;
@@ -1,21 +1,36 @@
1
1
  import { useEffect, useRef } from 'preact/hooks';
2
2
  import { __subscribePhase } from './internal/route-change.js';
3
+ /**
4
+ * Register a global, route-aware view-transition type rule. The resolver runs on
5
+ * every navigation with `{ to, from, direction }` and returns the type(s) to add
6
+ * to that navigation's transition (a static string/array adds the same type(s) to
7
+ * every navigation). Returns an unsubscribe.
8
+ *
9
+ * Unlike {@link useViewTransitionTypes}, this is not tied to a mounted component,
10
+ * so it covers entering AND leaving a section (a layout hook is not subscribed yet
11
+ * on enter and is already torn down on leave). No-op on the server (no document).
12
+ */
13
+ export function subscribeViewTransitionTypes(input) {
14
+ if (typeof document === 'undefined')
15
+ return () => { };
16
+ return __subscribePhase('beforeTransition', (event) => {
17
+ const resolved = typeof input === 'function'
18
+ ? input({ to: event.to, from: event.from, direction: event.direction })
19
+ : input;
20
+ if (resolved == null)
21
+ return;
22
+ if (typeof resolved === 'string')
23
+ event.types.push(resolved);
24
+ else
25
+ for (const t of resolved)
26
+ event.types.push(t);
27
+ });
28
+ }
3
29
  export function useViewTransitionTypes(input) {
4
30
  const ref = useRef(input);
5
31
  ref.current = input;
6
- useEffect(() => {
7
- return __subscribePhase('beforeTransition', (event) => {
8
- const v = ref.current;
9
- const resolved = typeof v === 'function'
10
- ? v({ to: event.to, from: event.from, direction: event.direction })
11
- : v;
12
- if (resolved == null)
13
- return;
14
- if (typeof resolved === 'string')
15
- event.types.push(resolved);
16
- else
17
- for (const t of resolved)
18
- event.types.push(t);
19
- });
20
- }, []);
32
+ useEffect(() => subscribeViewTransitionTypes((nav) => {
33
+ const v = ref.current;
34
+ return typeof v === 'function' ? v(nav) : v;
35
+ }), []);
21
36
  }
@@ -5,10 +5,11 @@ export function generateClientEntrySource(opts) {
5
5
  return (`import { h, hydrate, render as renderPreact } from 'preact';\n` +
6
6
  `import { LocationProvider } from 'preact-iso';\n` +
7
7
  `import { Routes, PersistHost } from 'hono-preact';\n` +
8
- `import { __dispatchRouteChange, installStreamRegistry, installHistoryShim } from 'hono-preact/internal';\n` +
8
+ `import { installNavTransitionScheduler, installStreamRegistry, installHistoryShim } from 'hono-preact/internal';\n` +
9
9
  `import routes from '${opts.routesAbsPath}';\n` +
10
10
  `\n` +
11
11
  `installHistoryShim();\n` +
12
+ `installNavTransitionScheduler();\n` +
12
13
  `installStreamRegistry();\n` +
13
14
  `\n` +
14
15
  `let persistHost = document.getElementById('__hp_persist_root');\n` +
@@ -19,16 +20,13 @@ export function generateClientEntrySource(opts) {
19
20
  `}\n` +
20
21
  `renderPreact(h(PersistHost, null), persistHost);\n` +
21
22
  `\n` +
22
- `let lastPath;\n` +
23
- `function onRouteChange(path) {\n` +
24
- ` const from = lastPath;\n` +
25
- ` lastPath = path;\n` +
26
- ` __dispatchRouteChange(path, from);\n` +
27
- `}\n` +
28
- `\n` +
23
+ // View transitions are driven by installNavTransitionScheduler() above: it
24
+ // overrides Preact's render scheduler so a navigation's re-render runs inside
25
+ // document.startViewTransition (capturing the outgoing route as the old
26
+ // snapshot before the new one swaps in). No per-navigation wiring needed.
29
27
  `hydrate(\n` +
30
28
  ` h(LocationProvider, null,\n` +
31
- ` h(Routes, { routes, onRouteChange })\n` +
29
+ ` h(Routes, { routes })\n` +
32
30
  ` ),\n` +
33
31
  ` document.getElementById('app')\n` +
34
32
  `);\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hono-preact",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Hono on the edge, Preact in the browser, manifest driven routes, typed RPC, streaming everywhere.",
5
5
  "keywords": [
6
6
  "hono",
@@ -95,8 +95,8 @@
95
95
  },
96
96
  "devDependencies": {
97
97
  "typescript": "*",
98
- "@hono-preact/server": "0.1.0",
99
98
  "@hono-preact/iso": "0.1.0",
99
+ "@hono-preact/server": "0.1.0",
100
100
  "@hono-preact/vite": "0.1.0"
101
101
  },
102
102
  "scripts": {