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.
- package/README.md +2 -1
- package/dist/adapter-cloudflare.d.ts +1 -0
- package/dist/adapter-cloudflare.d.ts.map +1 -0
- package/dist/adapter-cloudflare.js +2 -0
- package/dist/adapter-node.d.ts +1 -0
- package/dist/adapter-node.d.ts.map +1 -0
- package/dist/adapter-node.js +2 -0
- package/dist/internal.d.ts +1 -1
- package/dist/internal.js +1 -1
- package/dist/iso/action.d.ts +10 -14
- package/dist/iso/action.js +57 -21
- package/dist/iso/define-app.d.ts +7 -0
- package/dist/iso/define-app.js +3 -0
- package/dist/iso/define-loader.d.ts +19 -0
- package/dist/iso/define-loader.js +4 -0
- package/dist/iso/define-middleware.d.ts +43 -0
- package/dist/iso/define-middleware.js +6 -0
- package/dist/iso/define-page.d.ts +7 -2
- package/dist/iso/define-page.js +1 -1
- package/dist/iso/define-routes.d.ts +24 -1
- package/dist/iso/define-routes.js +34 -0
- package/dist/iso/define-stream-observer.d.ts +20 -0
- package/dist/iso/define-stream-observer.js +3 -0
- package/dist/iso/index.d.ts +10 -5
- package/dist/iso/index.js +5 -3
- package/dist/iso/internal/contexts.d.ts +0 -2
- package/dist/iso/internal/contexts.js +0 -1
- package/dist/iso/internal/loader-fetch.js +37 -7
- package/dist/iso/internal/loader-runner.js +105 -8
- package/dist/iso/internal/middleware-runner.d.ts +22 -0
- package/dist/iso/internal/middleware-runner.js +79 -0
- package/dist/iso/internal/page-middleware-host.d.ts +13 -0
- package/dist/iso/internal/page-middleware-host.js +119 -0
- package/dist/iso/internal/route-boundary.d.ts +1 -0
- package/dist/iso/internal/route-boundary.js +16 -0
- package/dist/iso/internal/stream-observer-runner.d.ts +13 -0
- package/dist/iso/internal/stream-observer-runner.js +48 -0
- package/dist/iso/internal/use-partitioner.d.ts +9 -0
- package/dist/iso/internal/use-partitioner.js +11 -0
- package/dist/iso/internal/use-types.d.ts +7 -0
- package/dist/iso/internal/use-types.js +1 -0
- package/dist/iso/internal.d.ts +5 -4
- package/dist/iso/internal.js +8 -6
- package/dist/iso/outcomes.d.ts +38 -0
- package/dist/iso/outcomes.js +56 -0
- package/dist/iso/page-only.d.ts +5 -0
- package/dist/iso/page-only.js +20 -0
- package/dist/iso/page.d.ts +3 -3
- package/dist/iso/page.js +3 -3
- package/dist/page.d.ts +1 -0
- package/dist/page.d.ts.map +1 -0
- package/dist/page.js +8 -0
- package/dist/server/actions-handler.d.ts +20 -6
- package/dist/server/actions-handler.js +83 -47
- package/dist/server/context.js +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/loaders-handler.d.ts +16 -0
- package/dist/server/loaders-handler.js +94 -17
- package/dist/server/render.d.ts +2 -0
- package/dist/server/render.js +104 -33
- package/dist/server/route-server-modules.d.ts +42 -1
- package/dist/server/route-server-modules.js +184 -0
- package/dist/server/sse.d.ts +24 -1
- package/dist/server/sse.js +56 -4
- package/dist/vite/adapter-cloudflare.d.ts +2 -0
- package/dist/vite/adapter-cloudflare.js +25 -0
- package/dist/vite/adapter-node.d.ts +2 -0
- package/dist/vite/adapter-node.js +49 -0
- package/dist/vite/adapter.d.ts +29 -0
- package/dist/vite/adapter.js +1 -0
- package/dist/vite/client-shim.js +5 -4
- package/dist/vite/guard-strip.js +52 -27
- package/dist/vite/hono-preact.d.ts +6 -6
- package/dist/vite/hono-preact.js +48 -77
- package/dist/vite/index.d.ts +2 -1
- package/dist/vite/index.js +1 -1
- package/dist/vite/node-dev-server.d.ts +4 -0
- package/dist/vite/node-dev-server.js +121 -0
- package/dist/vite/server-entry.d.ts +30 -7
- package/dist/vite/server-entry.js +161 -78
- package/dist/vite/server-exports-contract.d.ts +6 -0
- package/dist/vite/server-exports-contract.js +43 -0
- package/dist/vite/server-loader-validation.js +36 -9
- package/dist/vite/server-loaders-parser.d.ts +17 -1
- package/dist/vite/server-loaders-parser.js +41 -0
- package/dist/vite/server-only.js +20 -2
- 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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
|
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
|
+
};
|
|
@@ -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,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 {};
|
package/dist/iso/internal.d.ts
CHANGED
|
@@ -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
|
|
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';
|
package/dist/iso/internal.js
CHANGED
|
@@ -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.
|
|
7
|
-
//
|
|
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
|
|
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;
|