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
package/README.md
CHANGED
|
@@ -37,7 +37,8 @@ Full walkthrough: https://framework.sbesh.com/docs/quick-start
|
|
|
37
37
|
|
|
38
38
|
## Subpaths
|
|
39
39
|
|
|
40
|
-
- `hono-preact`: iso runtime exports (routes, pages, loaders, actions, forms,
|
|
40
|
+
- `hono-preact`: iso runtime exports (routes, pages, loaders, actions, forms, middleware, outcomes).
|
|
41
|
+
- `hono-preact/page`: page-scope outcome kitchen sink (`redirect`, `deny`, `render`, predicates).
|
|
41
42
|
- `hono-preact/server`: server entry, `renderPage`, SSR streaming helpers.
|
|
42
43
|
- `hono-preact/vite`: `honoPreact()` plugin for Vite.
|
|
43
44
|
- `hono-preact/internal`: advanced exports for tooling authors. No stability guarantee.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './vite/adapter-cloudflare';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter-cloudflare.d.ts","sourceRoot":"","sources":["../src/adapter-cloudflare.ts"],"names":[],"mappings":"AACA,cAAc,sCAAsC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './vite/adapter-node';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter-node.d.ts","sourceRoot":"","sources":["../src/adapter-node.ts"],"names":[],"mappings":"AACA,cAAc,gCAAgC,CAAC"}
|
package/dist/internal.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from './iso/internal
|
|
1
|
+
export * from './iso/internal';
|
package/dist/internal.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from './iso/internal
|
|
1
|
+
export * from './iso/internal.js';
|
package/dist/iso/action.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Context } from 'hono';
|
|
2
|
-
import type { ContentfulStatusCode } from 'hono/utils/http-status';
|
|
3
2
|
import type { LoaderRef } from './define-loader.js';
|
|
3
|
+
import type { ActionUse } from './internal/use-types.js';
|
|
4
4
|
export type ActionStub<TPayload, TResult, TChunk = never> = {
|
|
5
5
|
readonly __module: string;
|
|
6
6
|
readonly __action: string;
|
|
@@ -12,7 +12,15 @@ export type ActionCtx = {
|
|
|
12
12
|
signal: AbortSignal;
|
|
13
13
|
};
|
|
14
14
|
export type ActionFn<TPayload, TResult, TChunk = never> = ((ctx: ActionCtx, payload: TPayload) => Promise<TResult>) | ((ctx: ActionCtx, payload: TPayload) => Promise<ReadableStream<TChunk>>) | ((ctx: ActionCtx, payload: TPayload) => AsyncGenerator<TChunk, TResult, unknown>);
|
|
15
|
-
export
|
|
15
|
+
export type DefineActionOpts<TChunk = never, TResult = unknown> = {
|
|
16
|
+
/**
|
|
17
|
+
* Per-action middleware and (for streaming actions) stream observers.
|
|
18
|
+
* Attached to the function as a non-enumerable-feeling property; the
|
|
19
|
+
* actions-handler reads it via the dispatcher (Task 18).
|
|
20
|
+
*/
|
|
21
|
+
use?: ActionUse<TChunk, TResult, boolean>;
|
|
22
|
+
};
|
|
23
|
+
export declare function defineAction<TPayload, TResult, TChunk = never>(fn: ActionFn<TPayload, TResult, TChunk>, opts?: DefineActionOpts<TChunk, TResult>): ActionStub<TPayload, TResult, TChunk>;
|
|
16
24
|
export type UseActionOptions<TPayload, TResult, TChunk = never, TSnapshot = unknown> = {
|
|
17
25
|
/**
|
|
18
26
|
* How to update loader caches after the action commits. Three modes:
|
|
@@ -64,15 +72,3 @@ export type UseActionResult<TPayload, TResult> = {
|
|
|
64
72
|
data: TResult | null;
|
|
65
73
|
};
|
|
66
74
|
export declare function useAction<TPayload, TResult, TChunk = never, TSnapshot = unknown>(stub: ActionStub<TPayload, TResult, TChunk>, options?: UseActionOptions<TPayload, TResult, TChunk, TSnapshot>): UseActionResult<TPayload, TResult>;
|
|
67
|
-
export type ActionGuardContext = {
|
|
68
|
-
c: Context;
|
|
69
|
-
module: string;
|
|
70
|
-
action: string;
|
|
71
|
-
payload: unknown;
|
|
72
|
-
};
|
|
73
|
-
export type ActionGuardFn = (ctx: ActionGuardContext, next: () => Promise<void>) => Promise<void>;
|
|
74
|
-
export declare class ActionGuardError extends Error {
|
|
75
|
-
readonly status: ContentfulStatusCode;
|
|
76
|
-
constructor(message: string, status?: ContentfulStatusCode);
|
|
77
|
-
}
|
|
78
|
-
export declare const defineActionGuard: (fn: ActionGuardFn) => ActionGuardFn;
|
package/dist/iso/action.js
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import { useCallback, useContext, useRef, useState } from 'preact/hooks';
|
|
2
2
|
import { ReloadContext } from './reload-context.js';
|
|
3
3
|
import { ActiveLoaderIdContext } from './internal/contexts.js';
|
|
4
|
-
export function defineAction(fn) {
|
|
5
|
-
// Runtime no-op: returns fn as-is.
|
|
6
|
-
//
|
|
4
|
+
export function defineAction(fn, opts) {
|
|
5
|
+
// Runtime no-op for the call itself: returns fn as-is. The ActionStub type
|
|
6
|
+
// is enforced only by TypeScript and the Vite plugin. The dispatcher reads
|
|
7
|
+
// `use` off the function-as-stub when running the chain.
|
|
8
|
+
//
|
|
9
|
+
// Use `Object.defineProperty` instead of direct assignment so a frozen
|
|
10
|
+
// module export (strict ESM, HMR-frozen modules) doesn't throw. The
|
|
11
|
+
// actions-handler reads via `(fn as { use?: ReadonlyArray<unknown> }).use`,
|
|
12
|
+
// which works whether the property was set by assignment or defineProperty.
|
|
13
|
+
if (opts?.use) {
|
|
14
|
+
Object.defineProperty(fn, 'use', {
|
|
15
|
+
value: opts.use,
|
|
16
|
+
configurable: true,
|
|
17
|
+
writable: true,
|
|
18
|
+
enumerable: false,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
7
21
|
return fn;
|
|
8
22
|
}
|
|
9
23
|
function hasFileValues(payload) {
|
|
@@ -67,13 +81,44 @@ export function useAction(stub, options) {
|
|
|
67
81
|
});
|
|
68
82
|
}
|
|
69
83
|
if (!response.ok) {
|
|
70
|
-
const body = (await response.json());
|
|
71
|
-
|
|
84
|
+
const body = (await response.json().catch(() => ({})));
|
|
85
|
+
// Deny outcomes carry `message` instead of the legacy `error`
|
|
86
|
+
// field; prefer the descriptive message when present. The deny()
|
|
87
|
+
// constructor defaults the message for first-party callers, but a
|
|
88
|
+
// hand-rolled envelope from custom server middleware might still
|
|
89
|
+
// ship without one; fall back to a deny-aware label so the user
|
|
90
|
+
// sees a hint that the status came from an explicit deny rather
|
|
91
|
+
// than a generic transport failure.
|
|
92
|
+
let msg;
|
|
93
|
+
if (body.__outcome === 'deny') {
|
|
94
|
+
msg =
|
|
95
|
+
typeof body.message === 'string'
|
|
96
|
+
? body.message
|
|
97
|
+
: `Request denied (${response.status})`;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
msg = body.error ?? `Action failed with status ${response.status}`;
|
|
101
|
+
}
|
|
102
|
+
throw new Error(msg);
|
|
72
103
|
}
|
|
73
104
|
const contentType = response.headers.get('Content-Type') ?? '';
|
|
74
|
-
// Server-side
|
|
75
|
-
//
|
|
76
|
-
// promise will never settle, but the page is navigating away
|
|
105
|
+
// Server-side middleware that throws `redirect(...)` comes back as
|
|
106
|
+
// a redirect outcome envelope. Hand off to the browser; the rest of
|
|
107
|
+
// this promise will never settle, but the page is navigating away
|
|
108
|
+
// anyway.
|
|
109
|
+
//
|
|
110
|
+
// Trust boundary: `to` is taken straight from the JSON body and
|
|
111
|
+
// passed to `window.location.assign`. The framework's own handlers
|
|
112
|
+
// emit safe (typically same-origin) values, but a compromised or
|
|
113
|
+
// misconfigured server (or a proxy injecting JSON) could push the
|
|
114
|
+
// client anywhere. We don't validate origin here for v0.1; treat
|
|
115
|
+
// your own server as part of the trusted boundary. A same-origin
|
|
116
|
+
// check is a deferred enhancement (see C4 in the middleware review).
|
|
117
|
+
//
|
|
118
|
+
// We use `response.clone().json()` to peek at the body without
|
|
119
|
+
// consuming it: if the response is NOT a redirect outcome the
|
|
120
|
+
// downstream `await response.json()` still needs to read it. Clone
|
|
121
|
+
// is cheap on a small JSON payload.
|
|
77
122
|
if (!contentType.includes('text/event-stream')) {
|
|
78
123
|
const peek = (await response
|
|
79
124
|
.clone()
|
|
@@ -81,11 +126,11 @@ export function useAction(stub, options) {
|
|
|
81
126
|
.catch(() => undefined));
|
|
82
127
|
if (peek !== null &&
|
|
83
128
|
typeof peek === 'object' &&
|
|
84
|
-
peek
|
|
85
|
-
'
|
|
86
|
-
|
|
129
|
+
peek.__outcome === 'redirect' &&
|
|
130
|
+
typeof peek.to === 'string') {
|
|
131
|
+
const to = peek.to;
|
|
87
132
|
if (typeof window !== 'undefined') {
|
|
88
|
-
window.location.assign(
|
|
133
|
+
window.location.assign(to);
|
|
89
134
|
}
|
|
90
135
|
// Cast through `as` because TS can't see this promise never settles.
|
|
91
136
|
return await new Promise(() => { });
|
|
@@ -178,12 +223,3 @@ export function useAction(stub, options) {
|
|
|
178
223
|
}, []);
|
|
179
224
|
return { mutate, pending, error, data };
|
|
180
225
|
}
|
|
181
|
-
export class ActionGuardError extends Error {
|
|
182
|
-
status;
|
|
183
|
-
constructor(message, status = 403) {
|
|
184
|
-
super(message);
|
|
185
|
-
this.status = status;
|
|
186
|
-
this.name = 'ActionGuardError';
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
export const defineActionGuard = (fn) => fn;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ServerMiddleware, ClientMiddleware } from './define-middleware.js';
|
|
2
|
+
import type { StreamObserver } from './define-stream-observer.js';
|
|
3
|
+
export type AppUseElement = ServerMiddleware<'page'> | ClientMiddleware | StreamObserver<unknown, never>;
|
|
4
|
+
export type AppConfig = {
|
|
5
|
+
use?: ReadonlyArray<AppUseElement>;
|
|
6
|
+
};
|
|
7
|
+
export declare function defineApp(config: AppConfig): AppConfig;
|
|
@@ -2,6 +2,9 @@ import type { ComponentChildren, ComponentType, FunctionComponent } from 'preact
|
|
|
2
2
|
import type { Context } from 'hono';
|
|
3
3
|
import type { RouteHook } from 'preact-iso';
|
|
4
4
|
import { type LoaderCache } from './cache.js';
|
|
5
|
+
import type { LoaderUse } from './internal/use-types.js';
|
|
6
|
+
import type { Middleware } from './define-middleware.js';
|
|
7
|
+
import type { StreamObserver } from './define-stream-observer.js';
|
|
5
8
|
export type LoaderCtx = {
|
|
6
9
|
c: Context;
|
|
7
10
|
location: RouteHook;
|
|
@@ -15,6 +18,15 @@ export interface LoaderRef<T> {
|
|
|
15
18
|
readonly fn: Loader<T>;
|
|
16
19
|
readonly cache: LoaderCache<T>;
|
|
17
20
|
readonly params: string[] | '*';
|
|
21
|
+
/**
|
|
22
|
+
* Per-loader middleware and (for streaming loaders) stream observers,
|
|
23
|
+
* exactly as authored on `defineLoader({ use })`. The handler-side
|
|
24
|
+
* dispatcher calls `partitionUse(ref.use)` to split middleware from
|
|
25
|
+
* observers; both partitions flow through the SSR/RPC streaming pump.
|
|
26
|
+
* Typed as the union the partitioner accepts so the contract is
|
|
27
|
+
* advertised at the consumer rather than hidden behind `unknown`.
|
|
28
|
+
*/
|
|
29
|
+
readonly use: ReadonlyArray<Middleware | StreamObserver<unknown, never>>;
|
|
18
30
|
useData(): T;
|
|
19
31
|
useError(): Error | null;
|
|
20
32
|
invalidate(): void;
|
|
@@ -43,5 +55,12 @@ export type DefineLoaderOpts<T> = {
|
|
|
43
55
|
__loaderName?: string;
|
|
44
56
|
cache?: LoaderCache<T>;
|
|
45
57
|
params?: string[] | '*';
|
|
58
|
+
/**
|
|
59
|
+
* Per-loader middleware and (for streaming loaders) stream observers.
|
|
60
|
+
* The element type LoaderUse<T, Streaming> structurally gates stream
|
|
61
|
+
* observers off non-streaming loaders, but a tighter compile-time gate
|
|
62
|
+
* via defineLoader overloads can be added in a follow-up if needed.
|
|
63
|
+
*/
|
|
64
|
+
use?: LoaderUse<T, boolean>;
|
|
46
65
|
};
|
|
47
66
|
export declare function defineLoader<T>(fn: Loader<T>, opts?: DefineLoaderOpts<T>): LoaderRef<T>;
|
|
@@ -80,6 +80,10 @@ export function defineLoader(fn, opts) {
|
|
|
80
80
|
fn,
|
|
81
81
|
cache: cache,
|
|
82
82
|
params: opts?.params ?? [],
|
|
83
|
+
// LoaderUse<T, boolean> structurally collapses to the same shape the
|
|
84
|
+
// partitioner accepts; the cast hides only the generic narrowing on
|
|
85
|
+
// StreamObserver's TChunk/TResult which is invariant. Identity-preserving.
|
|
86
|
+
use: (opts?.use ?? []),
|
|
83
87
|
useData() {
|
|
84
88
|
const ctx = useContext(LoaderDataContext);
|
|
85
89
|
if (!ctx) {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Context } from 'hono';
|
|
2
|
+
import type { RouteHook } from 'preact-iso';
|
|
3
|
+
import type { Outcome } from './outcomes.js';
|
|
4
|
+
export type Scope = 'page' | 'loader' | 'action';
|
|
5
|
+
export type ServerBaseCtx = {
|
|
6
|
+
c: Context;
|
|
7
|
+
signal: AbortSignal;
|
|
8
|
+
};
|
|
9
|
+
export type ServerPageCtx = ServerBaseCtx & {
|
|
10
|
+
scope: 'page';
|
|
11
|
+
location: RouteHook;
|
|
12
|
+
};
|
|
13
|
+
export type ServerLoaderCtx = ServerBaseCtx & {
|
|
14
|
+
scope: 'loader';
|
|
15
|
+
location: RouteHook;
|
|
16
|
+
module: string;
|
|
17
|
+
loader: string;
|
|
18
|
+
};
|
|
19
|
+
export type ServerActionCtx = ServerBaseCtx & {
|
|
20
|
+
scope: 'action';
|
|
21
|
+
module: string;
|
|
22
|
+
action: string;
|
|
23
|
+
payload: unknown;
|
|
24
|
+
};
|
|
25
|
+
export type ServerCtx<S extends Scope = Scope> = S extends 'page' ? ServerPageCtx : S extends 'loader' ? ServerLoaderCtx : S extends 'action' ? ServerActionCtx : ServerPageCtx | ServerLoaderCtx | ServerActionCtx;
|
|
26
|
+
export type ClientPageCtx = {
|
|
27
|
+
scope: 'page';
|
|
28
|
+
location: RouteHook;
|
|
29
|
+
};
|
|
30
|
+
export type Next = () => Promise<unknown>;
|
|
31
|
+
export type ServerMiddleware<S extends Scope = Scope> = {
|
|
32
|
+
__kind: 'middleware';
|
|
33
|
+
runs: 'server';
|
|
34
|
+
fn: (ctx: ServerCtx<S>, next: Next) => Promise<void | Outcome>;
|
|
35
|
+
};
|
|
36
|
+
export type ClientMiddleware = {
|
|
37
|
+
__kind: 'middleware';
|
|
38
|
+
runs: 'client';
|
|
39
|
+
fn: (ctx: ClientPageCtx, next: Next) => Promise<void | Outcome>;
|
|
40
|
+
};
|
|
41
|
+
export type Middleware = ServerMiddleware | ClientMiddleware;
|
|
42
|
+
export declare function defineServerMiddleware<S extends Scope = Scope>(fn: ServerMiddleware<S>['fn']): ServerMiddleware<S>;
|
|
43
|
+
export declare function defineClientMiddleware(fn: ClientMiddleware['fn']): ClientMiddleware;
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import type { ComponentType, FunctionComponent, JSX } from 'preact';
|
|
2
2
|
import type { RouteHook } from 'preact-iso';
|
|
3
|
-
import type {
|
|
3
|
+
import type { PageUse } from './internal/use-types.js';
|
|
4
4
|
import { type WrapperProps } from './page.js';
|
|
5
5
|
export type PageBindings = {
|
|
6
6
|
Wrapper?: ComponentType<WrapperProps>;
|
|
7
7
|
errorFallback?: JSX.Element | ((error: Error, reset: () => void) => JSX.Element);
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Page-scope middleware and stream observers. The dispatcher partitions
|
|
10
|
+
* server vs client members by their `runs` tag, so mixed arrays of
|
|
11
|
+
* defineServerMiddleware + defineClientMiddleware work as one list.
|
|
12
|
+
*/
|
|
13
|
+
use?: PageUse;
|
|
9
14
|
};
|
|
10
15
|
export declare function definePage(Component: ComponentType, bindings?: PageBindings): FunctionComponent<RouteHook>;
|
package/dist/iso/define-page.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "preact/jsx-runtime";
|
|
2
2
|
import { Page } from './page.js';
|
|
3
3
|
export function definePage(Component, bindings) {
|
|
4
|
-
const PageRoute = (location) => (_jsx(Page, { Wrapper: bindings?.Wrapper, errorFallback: bindings?.errorFallback,
|
|
4
|
+
const PageRoute = (location) => (_jsx(Page, { Wrapper: bindings?.Wrapper, errorFallback: bindings?.errorFallback, use: bindings?.use, location: location, children: _jsx(Component, {}) }));
|
|
5
5
|
PageRoute.displayName = `definePage(${Component.displayName ?? Component.name ?? 'Anonymous'})`;
|
|
6
6
|
return PageRoute;
|
|
7
7
|
}
|
|
@@ -7,7 +7,7 @@ export type ViewProps = RouteHook;
|
|
|
7
7
|
type LazyImport<T> = () => Promise<{
|
|
8
8
|
default: T;
|
|
9
9
|
}>;
|
|
10
|
-
type LazyServerImport = () => Promise<unknown>;
|
|
10
|
+
export type LazyServerImport = () => Promise<unknown>;
|
|
11
11
|
export type RouteDef = {
|
|
12
12
|
path: string;
|
|
13
13
|
view?: LazyImport<ComponentType<ViewProps>>;
|
|
@@ -20,10 +20,33 @@ export type FlatRoute = {
|
|
|
20
20
|
component: ComponentType<ViewProps>;
|
|
21
21
|
key: string;
|
|
22
22
|
};
|
|
23
|
+
export type ServerRoute = {
|
|
24
|
+
/** Absolute route path the server module belongs to. */
|
|
25
|
+
path: string;
|
|
26
|
+
/** Lazy `.server.*` module loader. */
|
|
27
|
+
server: LazyServerImport;
|
|
28
|
+
/**
|
|
29
|
+
* Lazy server-module loaders for every server-bearing ancestor in the
|
|
30
|
+
* route tree, outermost first, NOT including this route's own server.
|
|
31
|
+
* Used by the server-side pageUse resolver to compose page-layer
|
|
32
|
+
* middleware along the actual tree of layouts rather than relying on
|
|
33
|
+
* URL-prefix matching (which conflates siblings that share a path
|
|
34
|
+
* prefix, e.g. `/demo/projects` and `/demo/projects/:projectId/...`).
|
|
35
|
+
*/
|
|
36
|
+
ancestors: ReadonlyArray<LazyServerImport>;
|
|
37
|
+
};
|
|
23
38
|
export type RoutesManifest = {
|
|
24
39
|
tree: ReadonlyArray<RouteDef>;
|
|
25
40
|
flat: ReadonlyArray<FlatRoute>;
|
|
26
41
|
serverImports: ReadonlyArray<LazyServerImport>;
|
|
42
|
+
/**
|
|
43
|
+
* Path-keyed view of every server module in the tree. Lets server-side
|
|
44
|
+
* consumers (e.g. the page-layer `use` resolver) load a per-page module
|
|
45
|
+
* by the route path that matched, without re-walking the tree. The order
|
|
46
|
+
* mirrors `serverImports`; the two arrays exist side-by-side because most
|
|
47
|
+
* existing call sites only need the lazy thunks.
|
|
48
|
+
*/
|
|
49
|
+
serverRoutes: ReadonlyArray<ServerRoute>;
|
|
27
50
|
};
|
|
28
51
|
export declare function defineRoutes(tree: RouteDef[]): RoutesManifest;
|
|
29
52
|
export type RoutesProps = {
|
|
@@ -67,6 +67,39 @@ function collectServerImports(routes) {
|
|
|
67
67
|
walk(routes);
|
|
68
68
|
return out;
|
|
69
69
|
}
|
|
70
|
+
function collectServerRoutes(routes, parentPath = '') {
|
|
71
|
+
const out = [];
|
|
72
|
+
// `serverStack` tracks the lazy server-thunks for every server-bearing
|
|
73
|
+
// route on the path from the tree root down to (but not including) the
|
|
74
|
+
// node being emitted. Pushing on the way in / popping on the way out
|
|
75
|
+
// means each emitted ServerRoute captures its TRUE tree-walk ancestry,
|
|
76
|
+
// not whichever other patterns happen to share a URL prefix.
|
|
77
|
+
const walk = (rs, pp, serverStack) => {
|
|
78
|
+
for (const r of rs) {
|
|
79
|
+
const here = pp === '' ? r.path : pp + (r.path === '' ? '' : '/' + r.path);
|
|
80
|
+
if (r.server) {
|
|
81
|
+
// Capture the stack BEFORE pushing self -- ancestors exclude self.
|
|
82
|
+
out.push({
|
|
83
|
+
path: here,
|
|
84
|
+
server: r.server,
|
|
85
|
+
ancestors: serverStack.slice(),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (r.children) {
|
|
89
|
+
if (r.server) {
|
|
90
|
+
serverStack.push(r.server);
|
|
91
|
+
walk(r.children, here, serverStack);
|
|
92
|
+
serverStack.pop();
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
walk(r.children, here, serverStack);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
walk(routes, parentPath, []);
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
70
103
|
/**
|
|
71
104
|
* Memoize `lazy(view)` per view-thunk identity. When the same `view` thunk is
|
|
72
105
|
* referenced by multiple route registrations (e.g. `/docs` and `/docs/*`),
|
|
@@ -240,6 +273,7 @@ export function defineRoutes(tree) {
|
|
|
240
273
|
tree,
|
|
241
274
|
flat: flattenTree(tree, viewCache, keyCache),
|
|
242
275
|
serverImports: collectServerImports(tree),
|
|
276
|
+
serverRoutes: collectServerRoutes(tree),
|
|
243
277
|
};
|
|
244
278
|
}
|
|
245
279
|
export const Routes = ({ routes, onRouteChange, }) => {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ServerLoaderCtx, ServerActionCtx } from './define-middleware.js';
|
|
2
|
+
export type ServerStreamCtx = ServerLoaderCtx | ServerActionCtx;
|
|
3
|
+
export type StreamObserver<TChunk = unknown, TResult = void> = {
|
|
4
|
+
__kind: 'observer';
|
|
5
|
+
onStart?: (ctx: ServerStreamCtx) => void;
|
|
6
|
+
onChunk?: (ctx: ServerStreamCtx, chunk: TChunk, index: number) => void;
|
|
7
|
+
onEnd?: (ctx: ServerStreamCtx, info: {
|
|
8
|
+
chunks: number;
|
|
9
|
+
result: TResult;
|
|
10
|
+
}) => void;
|
|
11
|
+
onError?: (ctx: ServerStreamCtx, err: unknown, info: {
|
|
12
|
+
chunks: number;
|
|
13
|
+
}) => void;
|
|
14
|
+
onAbort?: (ctx: ServerStreamCtx, info: {
|
|
15
|
+
chunks: number;
|
|
16
|
+
}) => void;
|
|
17
|
+
};
|
|
18
|
+
type Spec<TChunk, TResult> = Omit<StreamObserver<TChunk, TResult>, '__kind'>;
|
|
19
|
+
export declare function defineStreamObserver<TChunk = unknown, TResult = void>(spec: Spec<TChunk, TResult>): StreamObserver<TChunk, TResult>;
|
|
20
|
+
export {};
|
package/dist/iso/index.d.ts
CHANGED
|
@@ -4,12 +4,11 @@ export { definePage } from './define-page.js';
|
|
|
4
4
|
export type { PageBindings } from './define-page.js';
|
|
5
5
|
export { Route, Router, lazy, useLocation, useRoute } from 'preact-iso';
|
|
6
6
|
export { defineRoutes, Routes } from './define-routes.js';
|
|
7
|
-
export type { RouteDef, RoutesManifest, FlatRoute, LayoutProps, ViewProps, } from './define-routes.js';
|
|
7
|
+
export type { RouteDef, RoutesManifest, FlatRoute, ServerRoute, LayoutProps, ViewProps, } from './define-routes.js';
|
|
8
8
|
export { defineLoader } from './define-loader.js';
|
|
9
9
|
export type { LoaderRef, LoaderCtx, Loader as LoaderFn, } from './define-loader.js';
|
|
10
10
|
export { defineAction, useAction } from './action.js';
|
|
11
|
-
export type { ActionStub, UseActionOptions, UseActionResult, MutateResult,
|
|
12
|
-
export { ActionGuardError, defineActionGuard } from './action.js';
|
|
11
|
+
export type { ActionStub, UseActionOptions, UseActionResult, MutateResult, } from './action.js';
|
|
13
12
|
export type { ContentfulStatusCode } from 'hono/utils/http-status';
|
|
14
13
|
export { useReload } from './reload-context.js';
|
|
15
14
|
export { useOptimistic } from './optimistic.js';
|
|
@@ -19,8 +18,14 @@ export type { UseOptimisticActionOptions, UseOptimisticActionResult, } from './o
|
|
|
19
18
|
export { Form } from './form.js';
|
|
20
19
|
export { createCache } from './cache.js';
|
|
21
20
|
export type { LoaderCache } from './cache.js';
|
|
22
|
-
export {
|
|
23
|
-
export type {
|
|
21
|
+
export { defineServerMiddleware, defineClientMiddleware, } from './define-middleware.js';
|
|
22
|
+
export type { ServerMiddleware, ClientMiddleware, Middleware, ServerBaseCtx, ServerPageCtx, ServerLoaderCtx, ServerActionCtx, ServerCtx, ClientPageCtx, Scope, Next, } from './define-middleware.js';
|
|
23
|
+
export { defineStreamObserver } from './define-stream-observer.js';
|
|
24
|
+
export type { StreamObserver, ServerStreamCtx, } from './define-stream-observer.js';
|
|
25
|
+
export { defineApp } from './define-app.js';
|
|
26
|
+
export type { AppConfig, AppUseElement } from './define-app.js';
|
|
27
|
+
export { redirect, deny, isOutcome, isRedirect, isDeny, isRender, } from './outcomes.js';
|
|
28
|
+
export type { Outcome, RedirectOutcome, DenyOutcome, RenderOutcome, RedirectStatusCode, ErrorStatusCode, } from './outcomes.js';
|
|
24
29
|
export { prefetch } from './prefetch.js';
|
|
25
30
|
export { isBrowser, env } from './is-browser.js';
|
|
26
31
|
export { useRouteChange } from './route-change.js';
|
package/dist/iso/index.js
CHANGED
|
@@ -9,7 +9,6 @@ export { defineRoutes, Routes } from './define-routes.js';
|
|
|
9
9
|
// Server bindings.
|
|
10
10
|
export { defineLoader } from './define-loader.js';
|
|
11
11
|
export { defineAction, useAction } from './action.js';
|
|
12
|
-
export { ActionGuardError, defineActionGuard } from './action.js';
|
|
13
12
|
// Hooks.
|
|
14
13
|
export { useReload } from './reload-context.js';
|
|
15
14
|
export { useOptimistic } from './optimistic.js';
|
|
@@ -18,8 +17,11 @@ export { useOptimisticAction } from './optimistic-action.js';
|
|
|
18
17
|
export { Form } from './form.js';
|
|
19
18
|
// Cache + invalidation.
|
|
20
19
|
export { createCache } from './cache.js';
|
|
21
|
-
//
|
|
22
|
-
export {
|
|
20
|
+
// Middleware + outcomes (the new system).
|
|
21
|
+
export { defineServerMiddleware, defineClientMiddleware, } from './define-middleware.js';
|
|
22
|
+
export { defineStreamObserver } from './define-stream-observer.js';
|
|
23
|
+
export { defineApp } from './define-app.js';
|
|
24
|
+
export { redirect, deny, isOutcome, isRedirect, isDeny, isRender, } from './outcomes.js';
|
|
23
25
|
// Utilities.
|
|
24
26
|
export { prefetch } from './prefetch.js';
|
|
25
27
|
export { isBrowser, env } from './is-browser.js';
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { Context } from 'hono';
|
|
2
|
-
import type { GuardResult } from '../guard.js';
|
|
3
2
|
export declare const HonoRequestContext: import("preact").Context<{
|
|
4
3
|
context?: Context;
|
|
5
4
|
}>;
|
|
@@ -7,6 +6,5 @@ export declare const LoaderIdContext: import("preact").Context<string | null>;
|
|
|
7
6
|
export declare const LoaderDataContext: import("preact").Context<{
|
|
8
7
|
data: unknown;
|
|
9
8
|
} | null>;
|
|
10
|
-
export declare const GuardResultContext: import("preact").Context<GuardResult | null>;
|
|
11
9
|
export declare const ActiveLoaderIdContext: import("preact").Context<symbol | null>;
|
|
12
10
|
export declare const LoaderErrorContext: import("preact").Context<Error | null>;
|
|
@@ -2,6 +2,5 @@ import { createContext } from 'preact';
|
|
|
2
2
|
export const HonoRequestContext = createContext({});
|
|
3
3
|
export const LoaderIdContext = createContext(null);
|
|
4
4
|
export const LoaderDataContext = createContext(null);
|
|
5
|
-
export const GuardResultContext = createContext(null);
|
|
6
5
|
export const ActiveLoaderIdContext = createContext(null);
|
|
7
6
|
export const LoaderErrorContext = createContext(null);
|
|
@@ -15,22 +15,52 @@ export async function fetchLoaderData(moduleKey, loaderName, location, signal, c
|
|
|
15
15
|
signal,
|
|
16
16
|
});
|
|
17
17
|
if (!res.ok) {
|
|
18
|
+
// Try to parse a deny outcome envelope first; it carries `message`
|
|
19
|
+
// rather than `error`. Fall back to the legacy `{ error }` shape.
|
|
18
20
|
const body = (await res.json().catch(() => ({})));
|
|
21
|
+
if (body.__outcome === 'deny') {
|
|
22
|
+
// The `deny()` constructor defaults `message` for first-party
|
|
23
|
+
// callers, but a hand-rolled envelope from custom server middleware
|
|
24
|
+
// might still arrive without one. Surface a deny-aware fallback
|
|
25
|
+
// instead of the generic "Loader failed" so the user sees a hint
|
|
26
|
+
// that the status came from an explicit deny.
|
|
27
|
+
const msg = typeof body.message === 'string'
|
|
28
|
+
? body.message
|
|
29
|
+
: `Request denied (${res.status})`;
|
|
30
|
+
throw new Error(msg);
|
|
31
|
+
}
|
|
19
32
|
throw new Error(body.error ?? `Loader failed with status ${res.status}`);
|
|
20
33
|
}
|
|
21
34
|
const contentType = res.headers.get('Content-Type') ?? '';
|
|
22
35
|
if (!contentType.includes('text/event-stream')) {
|
|
23
36
|
const json = (await res.json());
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
37
|
+
// Collision risk note (deferred from middleware review C6): a loader
|
|
38
|
+
// that legitimately returns data shaped `{ __outcome: 'redirect', to:
|
|
39
|
+
// <string> }` would be misinterpreted here and navigate the browser.
|
|
40
|
+
// The probability is low (the magic key is namespaced and unusual) and
|
|
41
|
+
// a wire-version sentinel like `__envelope: 'hono-preact/redirect'` or
|
|
42
|
+
// a `X-Hono-Preact-Outcome: redirect` response header would close the
|
|
43
|
+
// gap. Deferred for v0.2: body-key sniffing is the documented contract
|
|
44
|
+
// for v0.1.
|
|
45
|
+
// Server-side middleware that throws `redirect(...)` comes back as a
|
|
46
|
+
// redirect outcome envelope. Hand off to the browser via
|
|
47
|
+
// `location.assign` and return a promise that never settles: the
|
|
48
|
+
// current document is being replaced, no caller will see a value.
|
|
49
|
+
//
|
|
50
|
+
// Trust boundary: `to` is taken straight from the JSON body and passed
|
|
51
|
+
// to `window.location.assign`. The framework's own handlers emit safe
|
|
52
|
+
// (typically same-origin) values, but a compromised or misconfigured
|
|
53
|
+
// server (or a proxy injecting JSON) could push the client anywhere.
|
|
54
|
+
// We don't validate origin here for v0.1; treat your own server as
|
|
55
|
+
// part of the trusted boundary. A same-origin check is a deferred
|
|
56
|
+
// enhancement (see C4 in the middleware review).
|
|
28
57
|
if (json !== null &&
|
|
29
58
|
typeof json === 'object' &&
|
|
30
|
-
'
|
|
31
|
-
typeof json.
|
|
59
|
+
json.__outcome === 'redirect' &&
|
|
60
|
+
typeof json.to === 'string') {
|
|
61
|
+
const to = json.to;
|
|
32
62
|
if (typeof window !== 'undefined') {
|
|
33
|
-
window.location.assign(
|
|
63
|
+
window.location.assign(to);
|
|
34
64
|
}
|
|
35
65
|
return new Promise(() => {
|
|
36
66
|
/* never resolves; page is navigating */
|