hono-preact 0.1.0 → 0.2.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 (88) 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.d.ts +10 -14
  11. package/dist/iso/action.js +57 -21
  12. package/dist/iso/define-app.d.ts +7 -0
  13. package/dist/iso/define-app.js +3 -0
  14. package/dist/iso/define-loader.d.ts +19 -0
  15. package/dist/iso/define-loader.js +4 -0
  16. package/dist/iso/define-middleware.d.ts +43 -0
  17. package/dist/iso/define-middleware.js +6 -0
  18. package/dist/iso/define-page.d.ts +7 -2
  19. package/dist/iso/define-page.js +1 -1
  20. package/dist/iso/define-routes.d.ts +24 -1
  21. package/dist/iso/define-routes.js +34 -0
  22. package/dist/iso/define-stream-observer.d.ts +20 -0
  23. package/dist/iso/define-stream-observer.js +3 -0
  24. package/dist/iso/index.d.ts +10 -5
  25. package/dist/iso/index.js +5 -3
  26. package/dist/iso/internal/contexts.d.ts +0 -2
  27. package/dist/iso/internal/contexts.js +0 -1
  28. package/dist/iso/internal/loader-fetch.js +37 -7
  29. package/dist/iso/internal/loader-runner.js +105 -8
  30. package/dist/iso/internal/middleware-runner.d.ts +22 -0
  31. package/dist/iso/internal/middleware-runner.js +79 -0
  32. package/dist/iso/internal/page-middleware-host.d.ts +13 -0
  33. package/dist/iso/internal/page-middleware-host.js +119 -0
  34. package/dist/iso/internal/route-boundary.d.ts +1 -0
  35. package/dist/iso/internal/route-boundary.js +16 -0
  36. package/dist/iso/internal/stream-observer-runner.d.ts +13 -0
  37. package/dist/iso/internal/stream-observer-runner.js +48 -0
  38. package/dist/iso/internal/use-partitioner.d.ts +9 -0
  39. package/dist/iso/internal/use-partitioner.js +11 -0
  40. package/dist/iso/internal/use-types.d.ts +7 -0
  41. package/dist/iso/internal/use-types.js +1 -0
  42. package/dist/iso/internal.d.ts +5 -4
  43. package/dist/iso/internal.js +8 -6
  44. package/dist/iso/outcomes.d.ts +38 -0
  45. package/dist/iso/outcomes.js +56 -0
  46. package/dist/iso/page-only.d.ts +5 -0
  47. package/dist/iso/page-only.js +20 -0
  48. package/dist/iso/page.d.ts +3 -3
  49. package/dist/iso/page.js +3 -3
  50. package/dist/page.d.ts +1 -0
  51. package/dist/page.d.ts.map +1 -0
  52. package/dist/page.js +8 -0
  53. package/dist/server/actions-handler.d.ts +20 -6
  54. package/dist/server/actions-handler.js +83 -47
  55. package/dist/server/context.js +1 -1
  56. package/dist/server/index.d.ts +1 -1
  57. package/dist/server/index.js +1 -1
  58. package/dist/server/loaders-handler.d.ts +16 -0
  59. package/dist/server/loaders-handler.js +94 -17
  60. package/dist/server/render.d.ts +2 -0
  61. package/dist/server/render.js +104 -33
  62. package/dist/server/route-server-modules.d.ts +42 -1
  63. package/dist/server/route-server-modules.js +184 -0
  64. package/dist/server/sse.d.ts +24 -1
  65. package/dist/server/sse.js +56 -4
  66. package/dist/vite/adapter-cloudflare.d.ts +2 -0
  67. package/dist/vite/adapter-cloudflare.js +25 -0
  68. package/dist/vite/adapter-node.d.ts +2 -0
  69. package/dist/vite/adapter-node.js +49 -0
  70. package/dist/vite/adapter.d.ts +29 -0
  71. package/dist/vite/adapter.js +1 -0
  72. package/dist/vite/client-shim.js +5 -4
  73. package/dist/vite/guard-strip.js +52 -27
  74. package/dist/vite/hono-preact.d.ts +6 -6
  75. package/dist/vite/hono-preact.js +48 -77
  76. package/dist/vite/index.d.ts +2 -1
  77. package/dist/vite/index.js +1 -1
  78. package/dist/vite/node-dev-server.d.ts +4 -0
  79. package/dist/vite/node-dev-server.js +121 -0
  80. package/dist/vite/server-entry.d.ts +30 -7
  81. package/dist/vite/server-entry.js +161 -78
  82. package/dist/vite/server-exports-contract.d.ts +6 -0
  83. package/dist/vite/server-exports-contract.js +43 -0
  84. package/dist/vite/server-loader-validation.js +36 -9
  85. package/dist/vite/server-loaders-parser.d.ts +17 -1
  86. package/dist/vite/server-loaders-parser.js +41 -0
  87. package/dist/vite/server-only.js +20 -2
  88. package/package.json +32 -4
@@ -2,6 +2,9 @@ import { isBrowser } from '../is-browser.js';
2
2
  import { getRequestHonoContext } from '../cache.js';
3
3
  import { fetchLoaderData } from './loader-fetch.js';
4
4
  import { registerServerStreamingLoader } from './streaming-ssr.js';
5
+ import { dispatchServer } from './middleware-runner.js';
6
+ import { partitionUse } from './use-partitioner.js';
7
+ import { fanStart, fanChunk, fanEnd, fanError, } from './stream-observer-runner.js';
5
8
  function isAsyncGenerator(value) {
6
9
  return (value != null &&
7
10
  typeof value === 'object' &&
@@ -45,15 +48,109 @@ export function runLoader(loaderRef, location, id, signal, callbacks) {
45
48
  return c;
46
49
  },
47
50
  };
48
- const result = await loaderRef.fn(ctx);
49
- if (isAsyncGenerator(result)) {
50
- const step = await result.next();
51
- if (step.done) {
52
- return undefined; // generator returned without yielding
51
+ // Partition the loader's `use` array into middleware + observers. The
52
+ // dispatcher only consumes middleware; observers attach to the streaming
53
+ // pump below so chunks emitted during SSR flush observer hooks the same
54
+ // way the RPC/SSE path does.
55
+ const { middleware: allMiddleware, observers } = partitionUse((loaderRef.use ?? []));
56
+ const serverMw = allMiddleware.filter((m) => m.runs === 'server');
57
+ // Pre-build a ServerLoaderCtx so observers and middleware see the same
58
+ // ctx shape they see on the RPC path. `c` proxies through the lazy
59
+ // getter so test paths (no request scope) keep working when no consumer
60
+ // reads it.
61
+ const serverCtx = Object.defineProperties({
62
+ scope: 'loader',
63
+ signal,
64
+ location,
65
+ module: loaderRef.__moduleKey ?? '<unkeyed>',
66
+ loader: loaderName,
67
+ }, {
68
+ c: {
69
+ get: () => ctx.c,
70
+ enumerable: true,
71
+ },
72
+ });
73
+ /**
74
+ * Wrap a generator that has already yielded its first chunk so that
75
+ * subsequent yields fire observer hooks. Index 0 has already fired
76
+ * before this wrapper runs (see runInner below), so we count from 1.
77
+ *
78
+ * Implemented as a real `async function*` (not a hand-rolled object)
79
+ * so the result conforms to AsyncGenerator's full structural contract
80
+ * (including `[Symbol.asyncDispose]` on newer libs).
81
+ */
82
+ function wrapGeneratorWithObservers(gen, startIndex) {
83
+ async function* wrapped() {
84
+ let chunks = startIndex;
85
+ try {
86
+ while (true) {
87
+ const step = await gen.next();
88
+ if (step.done) {
89
+ fanEnd(observers, serverCtx, {
90
+ chunks,
91
+ result: step.value,
92
+ });
93
+ return step.value;
94
+ }
95
+ fanChunk(observers, serverCtx, step.value, chunks);
96
+ chunks += 1;
97
+ yield step.value;
98
+ }
99
+ }
100
+ catch (err) {
101
+ fanError(observers, serverCtx, err, { chunks });
102
+ throw err;
103
+ }
53
104
  }
54
- registerServerStreamingLoader(id, result);
55
- return step.value;
105
+ return wrapped();
106
+ }
107
+ const runInner = async () => {
108
+ const result = await loaderRef.fn(ctx);
109
+ if (isAsyncGenerator(result)) {
110
+ if (observers.length > 0) {
111
+ fanStart(observers, serverCtx);
112
+ }
113
+ const step = await result.next();
114
+ if (step.done) {
115
+ // Generator returned without yielding. Fire onEnd with chunks=0
116
+ // so observers see a clean lifecycle even on empty streams.
117
+ if (observers.length > 0) {
118
+ fanEnd(observers, serverCtx, {
119
+ chunks: 0,
120
+ result: step.value,
121
+ });
122
+ }
123
+ return undefined;
124
+ }
125
+ if (observers.length > 0) {
126
+ fanChunk(observers, serverCtx, step.value, 0);
127
+ // Register the OBSERVED wrapper so renderPage's drain fires
128
+ // onChunk for every subsequent yield and onEnd / onError at
129
+ // termination.
130
+ registerServerStreamingLoader(id, wrapGeneratorWithObservers(result, 1));
131
+ }
132
+ else {
133
+ registerServerStreamingLoader(id, result);
134
+ }
135
+ return step.value;
136
+ }
137
+ return result;
138
+ };
139
+ // Empty middleware path bypasses the dispatcher so existing call sites
140
+ // keep their exact prior behavior. Observer fanout above still fires
141
+ // through runInner; the dispatcher is only about ordered middleware
142
+ // before/after the inner.
143
+ if (serverMw.length === 0) {
144
+ return (await runInner());
145
+ }
146
+ const dispatch = await dispatchServer({
147
+ middleware: serverMw,
148
+ ctx: serverCtx,
149
+ inner: runInner,
150
+ });
151
+ if (dispatch.kind === 'outcome') {
152
+ throw dispatch.outcome;
56
153
  }
57
- return result;
154
+ return dispatch.value;
58
155
  })();
59
156
  }
@@ -0,0 +1,22 @@
1
+ import type { ServerMiddleware, ClientMiddleware, ServerCtx, ClientPageCtx, Scope } from '../define-middleware.js';
2
+ import { type Outcome } from '../outcomes.js';
3
+ export type DispatchResult<T> = {
4
+ kind: 'ok';
5
+ value: T;
6
+ } | {
7
+ kind: 'outcome';
8
+ outcome: Outcome;
9
+ };
10
+ type ServerDispatchArgs<T, S extends Scope> = {
11
+ middleware: ReadonlyArray<ServerMiddleware<S>>;
12
+ ctx: ServerCtx<S>;
13
+ inner: () => Promise<T>;
14
+ };
15
+ export declare function dispatchServer<T, S extends Scope = Scope>(args: ServerDispatchArgs<T, S>): Promise<DispatchResult<T>>;
16
+ type ClientDispatchArgs<T> = {
17
+ middleware: ReadonlyArray<ClientMiddleware>;
18
+ ctx: ClientPageCtx;
19
+ inner: () => Promise<T>;
20
+ };
21
+ export declare function dispatchClient<T>(args: ClientDispatchArgs<T>): Promise<DispatchResult<T>>;
22
+ export {};
@@ -0,0 +1,79 @@
1
+ import { isOutcome } from '../outcomes.js';
2
+ export async function dispatchServer(args) {
3
+ let innerResult;
4
+ const runChain = async (index) => {
5
+ if (index >= args.middleware.length) {
6
+ innerResult = await args.inner();
7
+ return;
8
+ }
9
+ const mw = args.middleware[index];
10
+ let nextCalled = false;
11
+ const next = async () => {
12
+ if (nextCalled) {
13
+ throw new Error(`Middleware at index ${index} called next() more than once. ` +
14
+ `Each middleware must call next() exactly once: a second call ` +
15
+ `would re-run the downstream chain (and the inner function) ` +
16
+ `with the original ctx, producing duplicate side effects.`);
17
+ }
18
+ nextCalled = true;
19
+ await runChain(index + 1);
20
+ return innerResult;
21
+ };
22
+ const ret = await mw.fn(args.ctx, next);
23
+ if (isOutcome(ret)) {
24
+ throw ret;
25
+ }
26
+ if (!nextCalled) {
27
+ throw new Error(`Middleware at index ${index} returned without calling next() or short-circuiting via a thrown outcome. ` +
28
+ `Middleware must either: (a) await/return next() to pass control on, or (b) throw a redirect/deny/render outcome to short-circuit. ` +
29
+ `Returning silently is ambiguous and would let downstream code run.`);
30
+ }
31
+ };
32
+ try {
33
+ await runChain(0);
34
+ }
35
+ catch (thrown) {
36
+ if (isOutcome(thrown)) {
37
+ return { kind: 'outcome', outcome: thrown };
38
+ }
39
+ throw thrown;
40
+ }
41
+ return { kind: 'ok', value: innerResult };
42
+ }
43
+ export async function dispatchClient(args) {
44
+ let innerResult;
45
+ const runChain = async (index) => {
46
+ if (index >= args.middleware.length) {
47
+ innerResult = await args.inner();
48
+ return;
49
+ }
50
+ const mw = args.middleware[index];
51
+ let nextCalled = false;
52
+ const next = async () => {
53
+ if (nextCalled) {
54
+ throw new Error(`Middleware at index ${index} called next() more than once. ` +
55
+ `Each middleware must call next() exactly once: a second call ` +
56
+ `would re-run the downstream chain (and the inner function) ` +
57
+ `with the original ctx, producing duplicate side effects.`);
58
+ }
59
+ nextCalled = true;
60
+ await runChain(index + 1);
61
+ return innerResult;
62
+ };
63
+ const ret = await mw.fn(args.ctx, next);
64
+ if (isOutcome(ret))
65
+ throw ret;
66
+ if (!nextCalled) {
67
+ throw new Error(`Middleware at index ${index} returned without calling next() or short-circuiting via a thrown outcome.`);
68
+ }
69
+ };
70
+ try {
71
+ await runChain(0);
72
+ }
73
+ catch (thrown) {
74
+ if (isOutcome(thrown))
75
+ return { kind: 'outcome', outcome: thrown };
76
+ throw thrown;
77
+ }
78
+ return { kind: 'ok', value: innerResult };
79
+ }
@@ -0,0 +1,13 @@
1
+ import { type ComponentChildren, type FunctionComponent, type JSX } from 'preact';
2
+ import { type RouteHook } from 'preact-iso';
3
+ import type { Middleware } from '../define-middleware.js';
4
+ import type { StreamObserver } from '../define-stream-observer.js';
5
+ type AnyObserver = StreamObserver<unknown, never>;
6
+ type UseEntry = Middleware | AnyObserver;
7
+ export declare const PageMiddlewareHost: FunctionComponent<{
8
+ use?: ReadonlyArray<UseEntry>;
9
+ location: RouteHook;
10
+ fallback?: JSX.Element;
11
+ children: ComponentChildren;
12
+ }>;
13
+ export {};
@@ -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
+ };
@@ -14,6 +14,7 @@ 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
19
  render(): ComponentChildren;
19
20
  }
@@ -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,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,20 @@
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 { LoaderIdContext, LoaderDataContext } from './internal/contexts.js';
7
6
  export { ReloadContext } from './reload-context.js';
8
7
  export { RouteLocationsContext, RouteLocationsProvider, } from './internal/route-locations.js';
9
8
  export { getPreloadedData, deletePreloadedData } from './internal/preload.js';
10
9
  export { runRequestScope, getRequestStore, captureRequestScope, } from './cache.js';
11
10
  export { default as wrapPromise } from './internal/wrap-promise.js';
12
- export { runServerGuards, runClientGuards } from './guard.js';
13
11
  export { HonoRequestContext } from './internal/contexts.js';
12
+ export { PageMiddlewareHost } from './internal/page-middleware-host.js';
14
13
  export { __dispatchRouteChange, __subscribeRouteChange, } from './internal/route-change.js';
15
14
  export { installStreamRegistry, subscribeToLoaderStream, } from './internal/stream-registry.js';
16
15
  export { registerServerStreamingLoader, takeServerStreamingLoaders, } from './internal/streaming-ssr.js';
17
16
  export type { ServerLoaderStream } from './internal/streaming-ssr.js';
17
+ export { dispatchServer, dispatchClient, type DispatchResult, } from './internal/middleware-runner.js';
18
+ export { partitionUse } from './internal/use-partitioner.js';
19
+ export { fanStart, fanChunk, fanEnd, fanError, fanAbort, } from './internal/stream-observer-runner.js';
18
20
  export { __$createLoaderStub_hpiso } from './internal/loader-stub.js';
19
- export { __$guardNoop_hpiso } from './internal/guard-noop.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,25 @@
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 { LoaderIdContext, LoaderDataContext } from './internal/contexts.js';
33
32
  export { ReloadContext } from './reload-context.js';
34
33
  export { RouteLocationsContext, RouteLocationsProvider, } from './internal/route-locations.js';
35
34
  export { getPreloadedData, deletePreloadedData } from './internal/preload.js';
36
35
  export { runRequestScope, getRequestStore, captureRequestScope, } from './cache.js';
37
36
  export { default as wrapPromise } from './internal/wrap-promise.js';
38
- export { runServerGuards, runClientGuards } from './guard.js';
39
37
  export { HonoRequestContext } from './internal/contexts.js';
38
+ export { PageMiddlewareHost } from './internal/page-middleware-host.js';
40
39
  export { __dispatchRouteChange, __subscribeRouteChange, } from './internal/route-change.js';
41
40
  export { installStreamRegistry, subscribeToLoaderStream, } from './internal/stream-registry.js';
42
41
  export { registerServerStreamingLoader, takeServerStreamingLoaders, } from './internal/streaming-ssr.js';
42
+ // Middleware dispatcher + observer fanout. Internal-stability subpath.
43
+ export { dispatchServer, dispatchClient, } from './internal/middleware-runner.js';
44
+ export { partitionUse } from './internal/use-partitioner.js';
45
+ export { fanStart, fanChunk, fanEnd, fanError, fanAbort, } from './internal/stream-observer-runner.js';
43
46
  // ─── Section 2: framework-emitted (DO NOT IMPORT FROM USER CODE) ─────────
44
47
  // The `__$..._hpiso` naming makes the convention visible at every grep:
45
48
  // these symbols are referenced by code the framework's Vite plugins emit
46
49
  // (serverOnlyPlugin's loader stubs, guardStripPlugin's no-op replacement).
47
50
  // User code that imports them couples to plugin internals.
48
51
  export { __$createLoaderStub_hpiso } from './internal/loader-stub.js';
49
- export { __$guardNoop_hpiso } from './internal/guard-noop.js';
@@ -0,0 +1,38 @@
1
+ import type { FunctionComponent } from 'preact';
2
+ import type { RedirectStatusCode, ClientErrorStatusCode, ServerErrorStatusCode } from 'hono/utils/http-status';
3
+ export type ErrorStatusCode = ClientErrorStatusCode | ServerErrorStatusCode;
4
+ export type { RedirectStatusCode };
5
+ export type RedirectOutcome = {
6
+ __outcome: 'redirect';
7
+ to: string;
8
+ status: RedirectStatusCode;
9
+ headers: Record<string, string> | undefined;
10
+ };
11
+ export type DenyOutcome = {
12
+ __outcome: 'deny';
13
+ status: ErrorStatusCode;
14
+ message: string;
15
+ headers: Record<string, string> | undefined;
16
+ };
17
+ export type RenderOutcome = {
18
+ __outcome: 'render';
19
+ Component: FunctionComponent;
20
+ };
21
+ export type Outcome = RedirectOutcome | DenyOutcome | RenderOutcome;
22
+ type RedirectInput = string | {
23
+ to: string;
24
+ status?: RedirectStatusCode;
25
+ headers?: Record<string, string>;
26
+ };
27
+ export declare function redirect(input: RedirectInput): RedirectOutcome;
28
+ type DenyInput = {
29
+ status: ErrorStatusCode;
30
+ message?: string;
31
+ headers?: Record<string, string>;
32
+ };
33
+ export declare function deny(status: ErrorStatusCode, message?: string): DenyOutcome;
34
+ export declare function deny(spec: DenyInput): DenyOutcome;
35
+ export declare function isOutcome(value: unknown): value is Outcome;
36
+ export declare function isRedirect(value: unknown): value is RedirectOutcome;
37
+ export declare function isDeny(value: unknown): value is DenyOutcome;
38
+ export declare function isRender(value: unknown): value is RenderOutcome;