hono-preact 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +2 -1
  2. package/dist/adapter-cloudflare.d.ts +1 -0
  3. package/dist/adapter-cloudflare.d.ts.map +1 -0
  4. package/dist/adapter-cloudflare.js +2 -0
  5. package/dist/adapter-node.d.ts +1 -0
  6. package/dist/adapter-node.d.ts.map +1 -0
  7. package/dist/adapter-node.js +2 -0
  8. package/dist/internal.d.ts +1 -1
  9. package/dist/internal.js +1 -1
  10. package/dist/iso/action-result-context.d.ts +22 -0
  11. package/dist/iso/action-result-context.js +2 -0
  12. package/dist/iso/action.d.ts +60 -25
  13. package/dist/iso/action.js +210 -58
  14. package/dist/iso/cache.d.ts +9 -0
  15. package/dist/iso/cache.js +26 -0
  16. package/dist/iso/define-app.d.ts +14 -0
  17. package/dist/iso/define-app.js +3 -0
  18. package/dist/iso/define-loader.d.ts +31 -0
  19. package/dist/iso/define-loader.js +30 -16
  20. package/dist/iso/define-middleware.d.ts +43 -0
  21. package/dist/iso/define-middleware.js +6 -0
  22. package/dist/iso/define-page.d.ts +7 -2
  23. package/dist/iso/define-page.js +1 -1
  24. package/dist/iso/define-routes.d.ts +24 -1
  25. package/dist/iso/define-routes.js +34 -0
  26. package/dist/iso/define-stream-observer.d.ts +20 -0
  27. package/dist/iso/define-stream-observer.js +3 -0
  28. package/dist/iso/form.d.ts +13 -4
  29. package/dist/iso/form.js +115 -33
  30. package/dist/iso/index.d.ts +15 -7
  31. package/dist/iso/index.js +9 -4
  32. package/dist/iso/internal/action-envelope.d.ts +37 -0
  33. package/dist/iso/internal/action-envelope.js +47 -0
  34. package/dist/iso/internal/action-result-store.d.ts +28 -0
  35. package/dist/iso/internal/action-result-store.js +35 -0
  36. package/dist/iso/internal/contexts.d.ts +0 -2
  37. package/dist/iso/internal/contexts.js +0 -1
  38. package/dist/iso/internal/envelope.js +1 -2
  39. package/dist/iso/internal/form-submit-store.d.ts +9 -0
  40. package/dist/iso/internal/form-submit-store.js +32 -0
  41. package/dist/iso/internal/loader-fetch.js +102 -41
  42. package/dist/iso/internal/loader-runner.js +105 -8
  43. package/dist/iso/internal/loader.d.ts +3 -3
  44. package/dist/iso/internal/middleware-runner.d.ts +22 -0
  45. package/dist/iso/internal/middleware-runner.js +79 -0
  46. package/dist/iso/internal/page-middleware-host.d.ts +13 -0
  47. package/dist/iso/internal/page-middleware-host.js +119 -0
  48. package/dist/iso/internal/route-boundary.d.ts +5 -4
  49. package/dist/iso/internal/route-boundary.js +16 -0
  50. package/dist/iso/internal/safe-redirect.d.ts +7 -0
  51. package/dist/iso/internal/safe-redirect.js +27 -0
  52. package/dist/iso/internal/sse-decoder.d.ts +1 -1
  53. package/dist/iso/internal/sse-decoder.js +40 -26
  54. package/dist/iso/internal/stream-observer-runner.d.ts +13 -0
  55. package/dist/iso/internal/stream-observer-runner.js +48 -0
  56. package/dist/iso/internal/use-partitioner.d.ts +9 -0
  57. package/dist/iso/internal/use-partitioner.js +11 -0
  58. package/dist/iso/internal/use-types.d.ts +7 -0
  59. package/dist/iso/internal/use-types.js +1 -0
  60. package/dist/iso/internal.d.ts +12 -5
  61. package/dist/iso/internal.js +16 -7
  62. package/dist/iso/optimistic-action.d.ts +10 -1
  63. package/dist/iso/optimistic-action.js +11 -3
  64. package/dist/iso/optimistic.d.ts +10 -1
  65. package/dist/iso/optimistic.js +45 -5
  66. package/dist/iso/outcomes.d.ts +50 -0
  67. package/dist/iso/outcomes.js +67 -0
  68. package/dist/iso/page-only.d.ts +5 -0
  69. package/dist/iso/page-only.js +20 -0
  70. package/dist/iso/page.d.ts +3 -3
  71. package/dist/iso/page.js +3 -3
  72. package/dist/iso/use-action-result.d.ts +25 -0
  73. package/dist/iso/use-action-result.js +39 -0
  74. package/dist/iso/use-form-status.d.ts +5 -0
  75. package/dist/iso/use-form-status.js +13 -0
  76. package/dist/page.d.ts +1 -0
  77. package/dist/page.d.ts.map +1 -0
  78. package/dist/page.js +8 -0
  79. package/dist/server/actions-handler.d.ts +27 -6
  80. package/dist/server/actions-handler.js +121 -52
  81. package/dist/server/context.js +1 -1
  82. package/dist/server/index.d.ts +3 -2
  83. package/dist/server/index.js +3 -2
  84. package/dist/server/loaders-handler.d.ts +24 -0
  85. package/dist/server/loaders-handler.js +128 -18
  86. package/dist/server/page-action-handler.d.ts +63 -0
  87. package/dist/server/page-action-handler.js +274 -0
  88. package/dist/server/page-action-resolvers.d.ts +28 -0
  89. package/dist/server/page-action-resolvers.js +147 -0
  90. package/dist/server/render.d.ts +2 -0
  91. package/dist/server/render.js +142 -33
  92. package/dist/server/route-server-modules.d.ts +48 -8
  93. package/dist/server/route-server-modules.js +190 -7
  94. package/dist/server/speculation-rules.d.ts +3 -0
  95. package/dist/server/speculation-rules.js +8 -0
  96. package/dist/server/sse.d.ts +50 -12
  97. package/dist/server/sse.js +130 -53
  98. package/dist/vite/adapter-cloudflare.d.ts +2 -0
  99. package/dist/vite/adapter-cloudflare.js +25 -0
  100. package/dist/vite/adapter-node.d.ts +2 -0
  101. package/dist/vite/adapter-node.js +49 -0
  102. package/dist/vite/adapter.d.ts +29 -0
  103. package/dist/vite/adapter.js +1 -0
  104. package/dist/vite/client-shim.js +5 -4
  105. package/dist/vite/guard-strip.js +52 -27
  106. package/dist/vite/hono-preact.d.ts +6 -6
  107. package/dist/vite/hono-preact.js +48 -77
  108. package/dist/vite/index.d.ts +2 -1
  109. package/dist/vite/index.js +1 -1
  110. package/dist/vite/node-dev-server.d.ts +4 -0
  111. package/dist/vite/node-dev-server.js +121 -0
  112. package/dist/vite/server-entry.d.ts +30 -7
  113. package/dist/vite/server-entry.js +170 -79
  114. package/dist/vite/server-exports-contract.d.ts +6 -0
  115. package/dist/vite/server-exports-contract.js +43 -0
  116. package/dist/vite/server-loader-validation.js +36 -9
  117. package/dist/vite/server-loaders-parser.d.ts +17 -1
  118. package/dist/vite/server-loaders-parser.js +41 -0
  119. package/dist/vite/server-only.js +20 -2
  120. package/package.json +33 -5
@@ -0,0 +1,119 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "preact/jsx-runtime";
2
+ import { useLocation } from 'preact-iso';
3
+ import { Suspense } from 'preact/compat';
4
+ import { useContext, useEffect, useRef } from 'preact/hooks';
5
+ import { isBrowser } from '../is-browser.js';
6
+ import { isRedirect, isRender } from '../outcomes.js';
7
+ import { dispatchServer, dispatchClient } from './middleware-runner.js';
8
+ import { partitionUse } from './use-partitioner.js';
9
+ import wrapPromise from './wrap-promise.js';
10
+ import { HonoRequestContext } from './contexts.js';
11
+ function startChain(use, location, honoCtx) {
12
+ const { middleware } = partitionUse(use);
13
+ if (isBrowser()) {
14
+ const client = middleware.filter((m) => m.runs === 'client');
15
+ if (client.length === 0)
16
+ return Promise.resolve({ outcome: undefined });
17
+ return dispatchClient({
18
+ middleware: client,
19
+ ctx: { scope: 'page', location },
20
+ inner: async () => undefined,
21
+ }).then((r) => r.kind === 'outcome' ? { outcome: r.outcome } : { outcome: undefined });
22
+ }
23
+ const server = middleware.filter((m) => m.runs === 'server');
24
+ if (server.length === 0)
25
+ return Promise.resolve({ outcome: undefined });
26
+ if (!honoCtx) {
27
+ // Reject (don't throw synchronously). `wrapPromise` consumes a Promise;
28
+ // a sync throw here never reaches it, so the Suspense/ErrorBoundary path
29
+ // ends up surfacing a coerced "[object Object]"-style message instead of
30
+ // this explicit one. Returning a rejected promise routes the message
31
+ // through `wrapPromise.read()` -> the boundary's error state correctly.
32
+ return Promise.reject(new Error('<PageMiddlewareHost> rendered server-side without a HonoContext.Provider. ' +
33
+ 'renderPage must wrap the prerendered tree in <HonoContext.Provider value={{ context: c }}>.'));
34
+ }
35
+ return dispatchServer({
36
+ middleware: server,
37
+ ctx: {
38
+ scope: 'page',
39
+ c: honoCtx,
40
+ signal: (honoCtx.req?.raw?.signal ??
41
+ new AbortController().signal),
42
+ location,
43
+ },
44
+ inner: async () => undefined,
45
+ }).then((r) => r.kind === 'outcome' ? { outcome: r.outcome } : { outcome: undefined });
46
+ }
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]);
69
+ if (outcome === undefined) {
70
+ return _jsx(_Fragment, { children: children });
71
+ }
72
+ if (isRedirect(outcome)) {
73
+ if (isBrowser()) {
74
+ // Effect above will schedule the navigation; render nothing in the
75
+ // meantime so the old tree doesn't briefly flash.
76
+ return null;
77
+ }
78
+ // Server: rethrow so renderPage's outer handler can translate to HTTP redirect.
79
+ throw outcome;
80
+ }
81
+ if (isRender(outcome)) {
82
+ 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.
93
+ return _jsx(Alt, {});
94
+ }
95
+ // Deny on the page-render path: rethrow so the outer error boundary or
96
+ // handler can translate to the right response.
97
+ throw outcome;
98
+ }
99
+ export const PageMiddlewareHost = ({ use = [], location, fallback, children }) => {
100
+ const honoCtx = useContext(HonoRequestContext).context;
101
+ // 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.
109
+ const resultRef = useRef(null);
110
+ const prevPath = useRef(location.path);
111
+ if (resultRef.current === null) {
112
+ resultRef.current = wrapPromise(startChain(use, location, honoCtx));
113
+ }
114
+ else if (prevPath.current !== location.path) {
115
+ prevPath.current = location.path;
116
+ resultRef.current = wrapPromise(startChain(use, location, honoCtx));
117
+ }
118
+ return (_jsx(Suspense, { fallback: fallback, children: _jsx(HostConsumer, { resultRef: resultRef, children: children }) }));
119
+ };
@@ -1,6 +1,6 @@
1
1
  import { Component } from 'preact';
2
- import type { ComponentChildren, FunctionComponent, JSX } from 'preact';
3
- type ErrorFallback = JSX.Element | ((error: Error, reset: () => void) => JSX.Element);
2
+ import type { ComponentChildren, FunctionComponent } from 'preact';
3
+ type ErrorFallback = ComponentChildren | ((error: Error, reset: () => void) => ComponentChildren);
4
4
  type ErrorBoundaryProps = {
5
5
  fallback?: ErrorFallback;
6
6
  children: ComponentChildren;
@@ -14,11 +14,12 @@ export declare class ErrorBoundary extends Component<ErrorBoundaryProps, {
14
14
  static getDerivedStateFromError(error: unknown): {
15
15
  error: Error;
16
16
  };
17
+ componentDidCatch(error: unknown): void;
17
18
  reset: () => void;
18
- render(): ComponentChildren;
19
+ render(): any;
19
20
  }
20
21
  export declare const RouteBoundary: FunctionComponent<{
21
- fallback?: JSX.Element;
22
+ fallback?: ComponentChildren;
22
23
  errorFallback?: ErrorFallback;
23
24
  children: ComponentChildren;
24
25
  }>;
@@ -1,11 +1,27 @@
1
1
  import { jsx as _jsx } from "preact/jsx-runtime";
2
2
  import { Component } from 'preact';
3
3
  import { Suspense } from 'preact/compat';
4
+ import { isOutcome } from '../outcomes.js';
4
5
  export class ErrorBoundary extends Component {
5
6
  state = { error: null };
7
+ // Outcomes (redirect/deny/render) are NOT errors; they are control-flow
8
+ // signals that the dispatcher / renderPage outer catch is responsible for
9
+ // translating. If RouteBoundary swallowed them here, every page-scope
10
+ // throw from `PageMiddlewareHost` (HostConsumer rethrowing a deny on SSR,
11
+ // for example) would be coerced into `new Error(String(outcome))` and
12
+ // surfaced as the fallback UI, with no 302 / 403 / etc. reaching the
13
+ // network. Re-throw outcomes so the outer handler sees them. The same
14
+ // guard lives in `componentDidCatch` because Preact invokes both hooks
15
+ // when the boundary catches; whichever fires first must not swallow.
6
16
  static getDerivedStateFromError(error) {
17
+ if (isOutcome(error))
18
+ throw error;
7
19
  return { error: error instanceof Error ? error : new Error(String(error)) };
8
20
  }
21
+ componentDidCatch(error) {
22
+ if (isOutcome(error))
23
+ throw error;
24
+ }
9
25
  reset = () => {
10
26
  this.setState({ error: null });
11
27
  };
@@ -0,0 +1,7 @@
1
+ export declare function isSameOrigin(target: string): boolean;
2
+ /**
3
+ * Navigate to `target` only when it resolves same-origin against the current
4
+ * window. Cross-origin targets log a console error and are not followed.
5
+ * Returns true when the navigation was issued.
6
+ */
7
+ export declare function assignSafeRedirect(target: string): boolean;
@@ -0,0 +1,27 @@
1
+ export function isSameOrigin(target) {
2
+ if (typeof window === 'undefined')
3
+ return true;
4
+ try {
5
+ // Relative URLs resolve against the current origin and are always same-origin.
6
+ const resolved = new URL(target, window.location.href);
7
+ return resolved.origin === window.location.origin;
8
+ }
9
+ catch {
10
+ return false;
11
+ }
12
+ }
13
+ /**
14
+ * Navigate to `target` only when it resolves same-origin against the current
15
+ * window. Cross-origin targets log a console error and are not followed.
16
+ * Returns true when the navigation was issued.
17
+ */
18
+ export function assignSafeRedirect(target) {
19
+ if (typeof window === 'undefined')
20
+ return false;
21
+ if (!isSameOrigin(target)) {
22
+ console.error(`[hono-preact] refusing to navigate to cross-origin redirect target: ${target}`);
23
+ return false;
24
+ }
25
+ window.location.assign(target);
26
+ return true;
27
+ }
@@ -2,4 +2,4 @@ export type SSEEvent = {
2
2
  event: string;
3
3
  data: string;
4
4
  };
5
- export declare function readSSE(stream: ReadableStream<Uint8Array>): AsyncGenerator<SSEEvent, void, unknown>;
5
+ export declare function readSSE(stream: ReadableStream<BufferSource>): AsyncGenerator<SSEEvent, void, unknown>;
@@ -1,39 +1,53 @@
1
- export async function* readSSE(stream) {
2
- const reader = stream.getReader();
3
- const decoder = new TextDecoder();
1
+ function lineSplitTransform() {
4
2
  let buffer = '';
3
+ return new TransformStream({
4
+ transform(chunk, controller) {
5
+ buffer += chunk;
6
+ let nl;
7
+ while ((nl = buffer.indexOf('\n')) !== -1) {
8
+ controller.enqueue(buffer.slice(0, nl).replace(/\r$/, ''));
9
+ buffer = buffer.slice(nl + 1);
10
+ }
11
+ },
12
+ flush(controller) {
13
+ if (buffer.length) {
14
+ controller.enqueue(buffer.replace(/\r$/, ''));
15
+ buffer = '';
16
+ }
17
+ },
18
+ });
19
+ }
20
+ export async function* readSSE(stream) {
21
+ const lines = stream
22
+ .pipeThrough(new TextDecoderStream())
23
+ .pipeThrough(lineSplitTransform());
5
24
  let event = 'message';
6
25
  let dataLines = [];
26
+ const reader = lines.getReader();
7
27
  try {
8
28
  while (true) {
9
- const { done, value } = await reader.read();
29
+ const { done, value: line } = await reader.read();
10
30
  if (done) {
11
- buffer += decoder.decode();
12
- if (dataLines.length)
31
+ if (dataLines.length) {
13
32
  yield { event, data: dataLines.join('\n') };
33
+ }
14
34
  return;
15
35
  }
16
- buffer += decoder.decode(value, { stream: true });
17
- let nl;
18
- while ((nl = buffer.indexOf('\n')) !== -1) {
19
- const line = buffer.slice(0, nl).replace(/\r$/, '');
20
- buffer = buffer.slice(nl + 1);
21
- if (line === '') {
22
- if (dataLines.length) {
23
- yield { event, data: dataLines.join('\n') };
24
- }
25
- event = 'message';
26
- dataLines = [];
27
- }
28
- else if (line.startsWith(':')) {
29
- // SSE comment / keepalive, ignore
30
- }
31
- else if (line.startsWith('event:')) {
32
- event = line.slice(6).trim();
33
- }
34
- else if (line.startsWith('data:')) {
35
- dataLines.push(line.slice(5).replace(/^ /, ''));
36
+ if (line === '') {
37
+ if (dataLines.length) {
38
+ yield { event, data: dataLines.join('\n') };
36
39
  }
40
+ event = 'message';
41
+ dataLines = [];
42
+ }
43
+ else if (line.startsWith(':')) {
44
+ // SSE comment / keepalive, ignore
45
+ }
46
+ else if (line.startsWith('event:')) {
47
+ event = line.slice(6).trim();
48
+ }
49
+ else if (line.startsWith('data:')) {
50
+ dataLines.push(line.slice(5).replace(/^ /, ''));
37
51
  }
38
52
  }
39
53
  }
@@ -0,0 +1,13 @@
1
+ import type { StreamObserver, ServerStreamCtx } from '../define-stream-observer.js';
2
+ export declare function fanStart(observers: ReadonlyArray<StreamObserver<unknown, never>>, ctx: ServerStreamCtx): void;
3
+ export declare function fanChunk(observers: ReadonlyArray<StreamObserver<unknown, never>>, ctx: ServerStreamCtx, chunk: unknown, index: number): void;
4
+ export declare function fanEnd(observers: ReadonlyArray<StreamObserver<unknown, never>>, ctx: ServerStreamCtx, info: {
5
+ chunks: number;
6
+ result: unknown;
7
+ }): void;
8
+ export declare function fanError(observers: ReadonlyArray<StreamObserver<unknown, never>>, ctx: ServerStreamCtx, err: unknown, info: {
9
+ chunks: number;
10
+ }): void;
11
+ export declare function fanAbort(observers: ReadonlyArray<StreamObserver<unknown, never>>, ctx: ServerStreamCtx, info: {
12
+ chunks: number;
13
+ }): void;
@@ -0,0 +1,48 @@
1
+ function safeCall(fn) {
2
+ if (!fn)
3
+ return;
4
+ try {
5
+ fn();
6
+ }
7
+ catch (err) {
8
+ // Observer errors are isolated: surface via console for visibility but do
9
+ // not propagate. The stream is the source of truth; observers are a side
10
+ // channel and cannot corrupt the channel they observe.
11
+ // eslint-disable-next-line no-console
12
+ console.error('[stream-observer] hook threw and was isolated:', err);
13
+ }
14
+ }
15
+ export function fanStart(observers, ctx) {
16
+ for (const o of observers) {
17
+ safeCall(o.onStart ? () => o.onStart(ctx) : undefined);
18
+ }
19
+ }
20
+ export function fanChunk(observers, ctx, chunk, index) {
21
+ for (const o of observers) {
22
+ safeCall(o.onChunk ? () => o.onChunk(ctx, chunk, index) : undefined);
23
+ }
24
+ }
25
+ export function fanEnd(observers, ctx, info) {
26
+ for (const o of observers) {
27
+ safeCall(o.onEnd
28
+ ? () =>
29
+ // The partitioner widens observers to TResult=never (the only
30
+ // shape that admits arbitrary user-declared TResult); the call
31
+ // site provides whatever the underlying stream produced. The
32
+ // cast bridges the invariance gap and is safe because TS
33
+ // can't reason about the contravariant function-parameter
34
+ // shape across the array boundary.
35
+ o.onEnd(ctx, info)
36
+ : undefined);
37
+ }
38
+ }
39
+ export function fanError(observers, ctx, err, info) {
40
+ for (const o of observers) {
41
+ safeCall(o.onError ? () => o.onError(ctx, err, info) : undefined);
42
+ }
43
+ }
44
+ export function fanAbort(observers, ctx, info) {
45
+ for (const o of observers) {
46
+ safeCall(o.onAbort ? () => o.onAbort(ctx, info) : undefined);
47
+ }
48
+ }
@@ -0,0 +1,9 @@
1
+ import type { Middleware } from '../define-middleware.js';
2
+ import type { StreamObserver } from '../define-stream-observer.js';
3
+ type AnyObserver = StreamObserver<unknown, never>;
4
+ type UseEntry = Middleware | AnyObserver;
5
+ export declare function partitionUse(use: ReadonlyArray<UseEntry>): {
6
+ middleware: Middleware[];
7
+ observers: AnyObserver[];
8
+ };
9
+ export {};
@@ -0,0 +1,11 @@
1
+ export function partitionUse(use) {
2
+ const middleware = [];
3
+ const observers = [];
4
+ for (const entry of use) {
5
+ if (entry.__kind === 'middleware')
6
+ middleware.push(entry);
7
+ else
8
+ observers.push(entry);
9
+ }
10
+ return { middleware, observers };
11
+ }
@@ -0,0 +1,7 @@
1
+ import type { ServerMiddleware, ClientMiddleware, Scope } from '../define-middleware.js';
2
+ import type { StreamObserver } from '../define-stream-observer.js';
3
+ export type Use<S extends Scope, Streaming extends boolean, T = unknown, R = void> = ReadonlyArray<ServerMiddleware<S> | (S extends 'page' ? ClientMiddleware : never) | (Streaming extends true ? StreamObserver<T, R> : never)>;
4
+ export type PageUse = ReadonlyArray<ServerMiddleware<'page'> | ClientMiddleware | StreamObserver<unknown, never>>;
5
+ export type AppUse = PageUse;
6
+ export type LoaderUse<T, Streaming extends boolean> = Use<'loader', Streaming, T, void>;
7
+ export type ActionUse<TChunk, TResult, Streaming extends boolean> = Use<'action', Streaming, TChunk, TResult>;
@@ -0,0 +1 @@
1
+ export {};
@@ -1,19 +1,26 @@
1
1
  export { Loader } from './internal/loader.js';
2
2
  export { Envelope } from './internal/envelope.js';
3
3
  export { RouteBoundary } from './internal/route-boundary.js';
4
- export { Guards, GuardGate, useGuardResult } from './internal/guards.js';
5
4
  export { OptimisticOverlay } from './internal/optimistic-overlay.js';
6
- export { LoaderIdContext, LoaderDataContext, GuardResultContext, } from './internal/contexts.js';
5
+ export { serializeActionOutcome, type ActionEnvelope, type ActionResolution, type SerializedEnvelope, } from './internal/action-envelope.js';
6
+ export { LoaderIdContext, LoaderDataContext } from './internal/contexts.js';
7
7
  export { ReloadContext } from './reload-context.js';
8
8
  export { RouteLocationsContext, RouteLocationsProvider, } from './internal/route-locations.js';
9
9
  export { getPreloadedData, deletePreloadedData } from './internal/preload.js';
10
- export { runRequestScope, getRequestStore, captureRequestScope, } from './cache.js';
10
+ export { runRequestScope, getRequestStore, captureRequestScope, getActionResultSlot, setActionResultSlot, type ActionResultSlot, } from './cache.js';
11
11
  export { default as wrapPromise } from './internal/wrap-promise.js';
12
- export { runServerGuards, runClientGuards } from './guard.js';
13
12
  export { HonoRequestContext } from './internal/contexts.js';
13
+ export { PageMiddlewareHost } from './internal/page-middleware-host.js';
14
14
  export { __dispatchRouteChange, __subscribeRouteChange, } from './internal/route-change.js';
15
15
  export { installStreamRegistry, subscribeToLoaderStream, } from './internal/stream-registry.js';
16
16
  export { registerServerStreamingLoader, takeServerStreamingLoaders, } from './internal/streaming-ssr.js';
17
17
  export type { ServerLoaderStream } from './internal/streaming-ssr.js';
18
+ export { beginSubmit, endSubmit, isPending, subscribe as subscribeFormSubmit, } from './internal/form-submit-store.js';
19
+ export { assignSafeRedirect, isSameOrigin } from './internal/safe-redirect.js';
20
+ export { setLastActionResult, clearLastActionResult, getLastActionResult, subscribeActionResults, type StoredActionResult, } from './internal/action-result-store.js';
21
+ export { dispatchServer, dispatchClient, type DispatchResult, } from './internal/middleware-runner.js';
22
+ export { partitionUse } from './internal/use-partitioner.js';
23
+ export { fanStart, fanChunk, fanEnd, fanError, fanAbort, } from './internal/stream-observer-runner.js';
18
24
  export { __$createLoaderStub_hpiso } from './internal/loader-stub.js';
19
- export { __$guardNoop_hpiso } from './internal/guard-noop.js';
25
+ export { readSSE } from './internal/sse-decoder.js';
26
+ export type { SSEEvent } from './internal/sse-decoder.js';
@@ -3,8 +3,8 @@
3
3
  // These primitives compose the default <Page> pipeline. They're kept
4
4
  // behind a subpath so the front door (@hono-preact/iso) stays small.
5
5
  // Use them when definePage bindings or <Page> props don't express
6
- // what you need (e.g. distinct fallbacks for guards vs. loader, custom
7
- // pipeline ordering, advanced SSR work).
6
+ // what you need (e.g. custom middleware composition, distinct fallbacks
7
+ // for the middleware host vs. the loader, advanced SSR work).
8
8
  //
9
9
  // STABILITY: this subpath is intentionally less stable than the package's
10
10
  // main surface. Symbols may be renamed, retyped, or removed in any
@@ -27,23 +27,32 @@
27
27
  export { Loader } from './internal/loader.js';
28
28
  export { Envelope } from './internal/envelope.js';
29
29
  export { RouteBoundary } from './internal/route-boundary.js';
30
- export { Guards, GuardGate, useGuardResult } from './internal/guards.js';
31
30
  export { OptimisticOverlay } from './internal/optimistic-overlay.js';
32
- export { LoaderIdContext, LoaderDataContext, GuardResultContext, } from './internal/contexts.js';
31
+ export { serializeActionOutcome, } from './internal/action-envelope.js';
32
+ export { LoaderIdContext, LoaderDataContext } from './internal/contexts.js';
33
33
  export { ReloadContext } from './reload-context.js';
34
34
  export { RouteLocationsContext, RouteLocationsProvider, } from './internal/route-locations.js';
35
35
  export { getPreloadedData, deletePreloadedData } from './internal/preload.js';
36
- export { runRequestScope, getRequestStore, captureRequestScope, } from './cache.js';
36
+ export { runRequestScope, getRequestStore, captureRequestScope, getActionResultSlot, setActionResultSlot, } from './cache.js';
37
37
  export { default as wrapPromise } from './internal/wrap-promise.js';
38
- export { runServerGuards, runClientGuards } from './guard.js';
39
38
  export { HonoRequestContext } from './internal/contexts.js';
39
+ export { PageMiddlewareHost } from './internal/page-middleware-host.js';
40
40
  export { __dispatchRouteChange, __subscribeRouteChange, } from './internal/route-change.js';
41
41
  export { installStreamRegistry, subscribeToLoaderStream, } from './internal/stream-registry.js';
42
42
  export { registerServerStreamingLoader, takeServerStreamingLoaders, } from './internal/streaming-ssr.js';
43
+ export { beginSubmit, endSubmit, isPending, subscribe as subscribeFormSubmit, } from './internal/form-submit-store.js';
44
+ export { assignSafeRedirect, isSameOrigin } from './internal/safe-redirect.js';
45
+ export { setLastActionResult, clearLastActionResult, getLastActionResult, subscribeActionResults, } from './internal/action-result-store.js';
46
+ // Middleware dispatcher + observer fanout. Internal-stability subpath.
47
+ export { dispatchServer, dispatchClient, } from './internal/middleware-runner.js';
48
+ export { partitionUse } from './internal/use-partitioner.js';
49
+ export { fanStart, fanChunk, fanEnd, fanError, fanAbort, } from './internal/stream-observer-runner.js';
43
50
  // ─── Section 2: framework-emitted (DO NOT IMPORT FROM USER CODE) ─────────
44
51
  // The `__$..._hpiso` naming makes the convention visible at every grep:
45
52
  // these symbols are referenced by code the framework's Vite plugins emit
46
53
  // (serverOnlyPlugin's loader stubs, guardStripPlugin's no-op replacement).
47
54
  // User code that imports them couples to plugin internals.
48
55
  export { __$createLoaderStub_hpiso } from './internal/loader-stub.js';
49
- export { __$guardNoop_hpiso } from './internal/guard-noop.js';
56
+ // SSE decoder; useful in tests and advanced consumers that need to read
57
+ // a streaming loader/action response as a sequence of typed SSE events.
58
+ export { readSSE } from './internal/sse-decoder.js';
@@ -1,14 +1,23 @@
1
1
  import { type UseActionOptions, type UseActionResult, type ActionStub } from './action.js';
2
+ import { type OptimisticHandle } from './optimistic.js';
2
3
  import type { LoaderRef } from './define-loader.js';
4
+ export declare const OPTIMISTIC_BRAND: unique symbol;
5
+ export type OptimisticBinding<TPayload, TBase> = {
6
+ apply: (current: TBase, payload: TPayload) => TBase;
7
+ addOptimistic: (payload: TPayload) => OptimisticHandle;
8
+ };
3
9
  export type UseOptimisticActionOptions<TPayload, TResult, TBase, TChunk = never> = Omit<UseActionOptions<TPayload, TResult, TChunk>, 'invalidate' | 'onMutate' | 'onError' | 'onSuccess'> & {
4
10
  base: TBase;
5
11
  apply: (current: TBase, payload: TPayload) => TBase;
6
12
  invalidate?: 'auto' | ReadonlyArray<LoaderRef<unknown>>;
7
13
  onSuccess?: (data: TResult) => void;
8
14
  onError?: (err: Error) => void;
15
+ /** Forwarded to the internal `useOptimistic` call. */
16
+ transition?: boolean;
9
17
  };
10
- export type UseOptimisticActionResult<TPayload, TResult, TBase> = UseActionResult<TPayload, TResult> & {
18
+ export type UseOptimisticActionResult<TPayload, TResult, TBase> = ActionStub<TPayload, TResult, never> & UseActionResult<TPayload, TResult> & {
11
19
  value: TBase;
20
+ readonly [OPTIMISTIC_BRAND]: OptimisticBinding<TPayload, TBase>;
12
21
  };
13
22
  /**
14
23
  * Like `useAction`, but with an optimistic-update wrapper. `TChunk` defaults
@@ -1,5 +1,6 @@
1
1
  import { useAction, } from './action.js';
2
2
  import { useOptimistic } from './optimistic.js';
3
+ export const OPTIMISTIC_BRAND = Symbol('hono-preact.optimistic');
3
4
  /**
4
5
  * Like `useAction`, but with an optimistic-update wrapper. `TChunk` defaults
5
6
  * to `never` so existing non-streaming call sites are unaffected. Pass the
@@ -7,8 +8,8 @@ import { useOptimistic } from './optimistic.js';
7
8
  * when the action is streaming and you need a typed `onChunk` callback.
8
9
  */
9
10
  export function useOptimisticAction(stub, options) {
10
- const { base, apply, onSuccess, onError, ...actionOpts } = options;
11
- const [value, addOptimistic] = useOptimistic(base, apply);
11
+ const { base, apply, onSuccess, onError, transition, ...actionOpts } = options;
12
+ const [value, addOptimistic] = useOptimistic(base, apply, { transition });
12
13
  const action = useAction(stub, {
13
14
  ...actionOpts,
14
15
  onMutate: (payload) => addOptimistic(payload),
@@ -21,5 +22,12 @@ export function useOptimisticAction(stub, options) {
21
22
  onError?.(err);
22
23
  },
23
24
  });
24
- return { ...action, value };
25
+ return {
26
+ __module: stub.__module,
27
+ __action: stub.__action,
28
+ useAction: stub.useAction,
29
+ ...action,
30
+ value,
31
+ [OPTIMISTIC_BRAND]: { apply, addOptimistic },
32
+ };
25
33
  }
@@ -2,4 +2,13 @@ export type OptimisticHandle = {
2
2
  settle: () => void;
3
3
  revert: () => void;
4
4
  };
5
- export declare function useOptimistic<TBase, TPayload>(base: TBase, reducer: (current: TBase, payload: TPayload) => TBase): [TBase, (payload: TPayload) => OptimisticHandle];
5
+ export type UseOptimisticOptions = {
6
+ /**
7
+ * When true, the settle and revert paths are wrapped in
8
+ * `document.startViewTransition`. The initial optimistic update is never
9
+ * wrapped (it must paint same-frame). Falls back to a synchronous update
10
+ * when `document.startViewTransition` is unavailable.
11
+ */
12
+ transition?: boolean;
13
+ };
14
+ export declare function useOptimistic<TBase, TPayload>(base: TBase, reducer: (current: TBase, payload: TPayload) => TBase, options?: UseOptimisticOptions): [TBase, (payload: TPayload) => OptimisticHandle];
@@ -1,14 +1,50 @@
1
1
  import { useCallback, useReducer, useRef } from 'preact/hooks';
2
- export function useOptimistic(base, reducer) {
2
+ export function useOptimistic(base, reducer, options) {
3
3
  const queueRef = useRef([]);
4
4
  const lastBaseRef = useRef(base);
5
5
  const idRef = useRef(0);
6
6
  const [, forceRender] = useReducer((c) => c + 1, 0);
7
+ const transitionRef = useRef(options?.transition === true);
8
+ transitionRef.current = options?.transition === true;
7
9
  if (!Object.is(lastBaseRef.current, base)) {
8
10
  queueRef.current = queueRef.current.filter((e) => e.status !== 'ready');
9
11
  lastBaseRef.current = base;
10
12
  }
11
13
  const value = queueRef.current.reduce((acc, e) => reducer(acc, e.payload), base);
14
+ // Reads `transitionRef.current` at invocation time, not capture time, so it
15
+ // is safe to close over from the memoized `addOptimistic` (useCallback([]))
16
+ // below. Each render rebinds this function and writes the latest option
17
+ // value into the ref; settle/revert created by the stale memoized callback
18
+ // still see the up-to-date `transition` setting through the ref.
19
+ //
20
+ // The callback returns a promise that resolves on the next animation frame
21
+ // so the browser snapshots Preact's POST-render DOM as "new state". Without
22
+ // the rAF wait, `forceRender()` (an async dispatch through useReducer) has
23
+ // not yet flushed when `startViewTransition` snapshots, and the transition
24
+ // captures identical before/after frames with no visible animation.
25
+ const runWithTransition = (mutator) => {
26
+ if (transitionRef.current &&
27
+ typeof document !== 'undefined' &&
28
+ typeof document.startViewTransition === 'function') {
29
+ document.startViewTransition(async () => {
30
+ mutator();
31
+ await new Promise((resolve) => {
32
+ if (typeof requestAnimationFrame === 'function') {
33
+ requestAnimationFrame(() => resolve());
34
+ }
35
+ else {
36
+ // Non-DOM environment (shouldn't reach this branch given the
37
+ // outer check, but defensive). Resolve on next microtask so
38
+ // Preact's scheduled render runs.
39
+ queueMicrotask(resolve);
40
+ }
41
+ });
42
+ });
43
+ }
44
+ else {
45
+ mutator();
46
+ }
47
+ };
12
48
  const addOptimistic = useCallback((payload) => {
13
49
  const id = ++idRef.current;
14
50
  queueRef.current = [...queueRef.current, { id, payload, status: 'active' }];
@@ -17,13 +53,17 @@ export function useOptimistic(base, reducer) {
17
53
  settle: () => {
18
54
  const entry = queueRef.current.find((e) => e.id === id);
19
55
  if (entry && entry.status === 'active') {
20
- entry.status = 'ready';
21
- forceRender();
56
+ runWithTransition(() => {
57
+ entry.status = 'ready';
58
+ forceRender();
59
+ });
22
60
  }
23
61
  },
24
62
  revert: () => {
25
- queueRef.current = queueRef.current.filter((e) => e.id !== id);
26
- forceRender();
63
+ runWithTransition(() => {
64
+ queueRef.current = queueRef.current.filter((e) => e.id !== id);
65
+ forceRender();
66
+ });
27
67
  },
28
68
  };
29
69
  }, []);