hono-preact 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-result-context.d.ts +22 -0
- package/dist/iso/action-result-context.js +2 -0
- package/dist/iso/action.d.ts +60 -25
- package/dist/iso/action.js +210 -58
- package/dist/iso/cache.d.ts +9 -0
- package/dist/iso/cache.js +26 -0
- package/dist/iso/define-app.d.ts +14 -0
- package/dist/iso/define-app.js +3 -0
- package/dist/iso/define-loader.d.ts +31 -0
- package/dist/iso/define-loader.js +30 -16
- 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/form.d.ts +13 -4
- package/dist/iso/form.js +115 -33
- package/dist/iso/index.d.ts +15 -7
- package/dist/iso/index.js +9 -4
- package/dist/iso/internal/action-envelope.d.ts +37 -0
- package/dist/iso/internal/action-envelope.js +47 -0
- package/dist/iso/internal/action-result-store.d.ts +28 -0
- package/dist/iso/internal/action-result-store.js +35 -0
- package/dist/iso/internal/contexts.d.ts +0 -2
- package/dist/iso/internal/contexts.js +0 -1
- package/dist/iso/internal/envelope.js +1 -2
- package/dist/iso/internal/form-submit-store.d.ts +9 -0
- package/dist/iso/internal/form-submit-store.js +32 -0
- package/dist/iso/internal/loader-fetch.js +102 -41
- package/dist/iso/internal/loader-runner.js +105 -8
- package/dist/iso/internal/loader.d.ts +3 -3
- 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 +5 -4
- package/dist/iso/internal/route-boundary.js +16 -0
- package/dist/iso/internal/safe-redirect.d.ts +7 -0
- package/dist/iso/internal/safe-redirect.js +27 -0
- package/dist/iso/internal/sse-decoder.d.ts +1 -1
- package/dist/iso/internal/sse-decoder.js +40 -26
- 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 +12 -5
- package/dist/iso/internal.js +16 -7
- package/dist/iso/optimistic-action.d.ts +10 -1
- package/dist/iso/optimistic-action.js +11 -3
- package/dist/iso/optimistic.d.ts +10 -1
- package/dist/iso/optimistic.js +45 -5
- package/dist/iso/outcomes.d.ts +50 -0
- package/dist/iso/outcomes.js +67 -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/iso/use-action-result.d.ts +25 -0
- package/dist/iso/use-action-result.js +39 -0
- package/dist/iso/use-form-status.d.ts +5 -0
- package/dist/iso/use-form-status.js +13 -0
- 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 +27 -6
- package/dist/server/actions-handler.js +121 -52
- package/dist/server/context.js +1 -1
- package/dist/server/index.d.ts +3 -2
- package/dist/server/index.js +3 -2
- package/dist/server/loaders-handler.d.ts +24 -0
- package/dist/server/loaders-handler.js +128 -18
- package/dist/server/page-action-handler.d.ts +63 -0
- package/dist/server/page-action-handler.js +274 -0
- package/dist/server/page-action-resolvers.d.ts +28 -0
- package/dist/server/page-action-resolvers.js +147 -0
- package/dist/server/render.d.ts +2 -0
- package/dist/server/render.js +142 -33
- package/dist/server/route-server-modules.d.ts +48 -8
- package/dist/server/route-server-modules.js +190 -7
- package/dist/server/speculation-rules.d.ts +3 -0
- package/dist/server/speculation-rules.js +8 -0
- package/dist/server/sse.d.ts +50 -12
- package/dist/server/sse.js +130 -53
- 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 +170 -79
- 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 +33 -5
|
@@ -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,21 @@ export interface LoaderRef<T> {
|
|
|
15
18
|
readonly fn: Loader<T>;
|
|
16
19
|
readonly cache: LoaderCache<T>;
|
|
17
20
|
readonly params: string[] | '*';
|
|
21
|
+
/**
|
|
22
|
+
* Raw value as authored on `defineLoader({ timeoutMs })`. `undefined`
|
|
23
|
+
* means "use the handler's configured default"; `false` means "no
|
|
24
|
+
* timeout, only the request signal aborts".
|
|
25
|
+
*/
|
|
26
|
+
readonly timeoutMs?: number | false;
|
|
27
|
+
/**
|
|
28
|
+
* Per-loader middleware and (for streaming loaders) stream observers,
|
|
29
|
+
* exactly as authored on `defineLoader({ use })`. The handler-side
|
|
30
|
+
* dispatcher calls `partitionUse(ref.use)` to split middleware from
|
|
31
|
+
* observers; both partitions flow through the SSR/RPC streaming pump.
|
|
32
|
+
* Typed as the union the partitioner accepts so the contract is
|
|
33
|
+
* advertised at the consumer rather than hidden behind `unknown`.
|
|
34
|
+
*/
|
|
35
|
+
readonly use: ReadonlyArray<Middleware | StreamObserver<unknown, never>>;
|
|
18
36
|
useData(): T;
|
|
19
37
|
useError(): Error | null;
|
|
20
38
|
invalidate(): void;
|
|
@@ -43,5 +61,18 @@ export type DefineLoaderOpts<T> = {
|
|
|
43
61
|
__loaderName?: string;
|
|
44
62
|
cache?: LoaderCache<T>;
|
|
45
63
|
params?: string[] | '*';
|
|
64
|
+
/**
|
|
65
|
+
* Per-loader timeout in milliseconds. When omitted, the handler applies
|
|
66
|
+
* its configured default (30s). Pass `false` to disable the timeout for
|
|
67
|
+
* this loader (rely solely on the request signal).
|
|
68
|
+
*/
|
|
69
|
+
timeoutMs?: number | false;
|
|
70
|
+
/**
|
|
71
|
+
* Per-loader middleware and (for streaming loaders) stream observers.
|
|
72
|
+
* The element type LoaderUse<T, Streaming> structurally gates stream
|
|
73
|
+
* observers off non-streaming loaders, but a tighter compile-time gate
|
|
74
|
+
* via defineLoader overloads can be added in a follow-up if needed.
|
|
75
|
+
*/
|
|
76
|
+
use?: LoaderUse<T, boolean>;
|
|
46
77
|
};
|
|
47
78
|
export declare function defineLoader<T>(fn: Loader<T>, opts?: DefineLoaderOpts<T>): LoaderRef<T>;
|
|
@@ -42,7 +42,15 @@ function ViewRenderer({ loaderRef, props, render, }) {
|
|
|
42
42
|
const reload = reloadCtx?.reload ?? (() => { });
|
|
43
43
|
return render({ data, error, reload, ...props });
|
|
44
44
|
}
|
|
45
|
+
function validateTimeoutMs(value, context) {
|
|
46
|
+
if (value === undefined || value === false)
|
|
47
|
+
return;
|
|
48
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
49
|
+
throw new RangeError(`${context}: timeoutMs must be a non-negative finite number or false, got ${String(value)}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
45
52
|
export function defineLoader(fn, opts) {
|
|
53
|
+
validateTimeoutMs(opts?.timeoutMs, 'defineLoader');
|
|
46
54
|
const idKey = opts?.__moduleKey
|
|
47
55
|
? opts.__loaderName
|
|
48
56
|
? `${opts.__moduleKey}::${opts.__loaderName}`
|
|
@@ -80,6 +88,11 @@ export function defineLoader(fn, opts) {
|
|
|
80
88
|
fn,
|
|
81
89
|
cache: cache,
|
|
82
90
|
params: opts?.params ?? [],
|
|
91
|
+
timeoutMs: opts?.timeoutMs,
|
|
92
|
+
// LoaderUse<T, boolean> structurally collapses to the same shape the
|
|
93
|
+
// partitioner accepts; the cast hides only the generic narrowing on
|
|
94
|
+
// StreamObserver's TChunk/TResult which is invariant. Identity-preserving.
|
|
95
|
+
use: (opts?.use ?? []),
|
|
83
96
|
useData() {
|
|
84
97
|
const ctx = useContext(LoaderDataContext);
|
|
85
98
|
if (!ctx) {
|
|
@@ -93,26 +106,27 @@ export function defineLoader(fn, opts) {
|
|
|
93
106
|
invalidate() {
|
|
94
107
|
cache.invalidate();
|
|
95
108
|
},
|
|
96
|
-
Boundary
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return h(LoaderHost, {
|
|
109
|
+
// `Boundary` and `View` close over `ref`. The captures are by reference
|
|
110
|
+
// and only deref at call time (component render), so the cycle is safe;
|
|
111
|
+
// both are fully initialized before any consumer can invoke them.
|
|
112
|
+
Boundary: ({ fallback, errorFallback, children }) => h((LoaderHost), {
|
|
101
113
|
loader: ref,
|
|
102
114
|
fallback,
|
|
103
115
|
errorFallback,
|
|
104
116
|
children,
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
117
|
+
}),
|
|
118
|
+
View: (render, viewOpts) => {
|
|
119
|
+
const Wrapped = (props) => h(ref.Boundary, {
|
|
120
|
+
fallback: viewOpts?.fallback,
|
|
121
|
+
errorFallback: viewOpts?.errorFallback,
|
|
122
|
+
children: h((ViewRenderer), {
|
|
123
|
+
loaderRef: ref,
|
|
124
|
+
props,
|
|
125
|
+
render,
|
|
126
|
+
}),
|
|
127
|
+
});
|
|
128
|
+
return Wrapped;
|
|
129
|
+
},
|
|
115
130
|
};
|
|
116
|
-
ref.View = View;
|
|
117
131
|
return ref;
|
|
118
132
|
}
|
|
@@ -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/form.d.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import type { JSX, ComponentChildren } from 'preact';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import type { ActionStub } from './action.js';
|
|
3
|
+
import { type UseOptimisticActionResult } from './optimistic-action.js';
|
|
4
|
+
/**
|
|
5
|
+
* The `action` prop accepts either a plain action stub or the branded value
|
|
6
|
+
* returned by `useOptimisticAction`. The union lets `<Form>` discover the
|
|
7
|
+
* optimistic apply via `OPTIMISTIC_BRAND in action` narrowing without
|
|
8
|
+
* casting away the type.
|
|
9
|
+
*/
|
|
10
|
+
type FormActionInput<TPayload, TResult> = ActionStub<TPayload, TResult, never> | UseOptimisticActionResult<TPayload, TResult, unknown>;
|
|
11
|
+
export type FormProps<TPayload, TResult> = Omit<JSX.HTMLAttributes<HTMLFormElement>, 'action' | 'method' | 'onSubmit' | 'enctype'> & {
|
|
12
|
+
action: FormActionInput<TPayload, TResult>;
|
|
5
13
|
children?: ComponentChildren;
|
|
6
14
|
};
|
|
7
|
-
export declare function Form<TPayload
|
|
15
|
+
export declare function Form<TPayload, TResult>({ action, children, ...rest }: FormProps<TPayload, TResult>): JSX.Element;
|
|
16
|
+
export {};
|
package/dist/iso/form.js
CHANGED
|
@@ -1,40 +1,122 @@
|
|
|
1
|
-
import { jsx as _jsx } from "preact/jsx-runtime";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* Single-value fields stay scalar. Files survive as `File` instances.
|
|
11
|
-
*
|
|
12
|
-
* Consumers should type their `defineAction<TPayload, ...>` to match:
|
|
13
|
-
* `tags: string[]`, `photos: File[]`, etc. for fields that may have multiple
|
|
14
|
-
* values; scalar types for fields that won't.
|
|
15
|
-
*/
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
2
|
+
import { useState, useCallback, useMemo } from 'preact/hooks';
|
|
3
|
+
import { OPTIMISTIC_BRAND, } from './optimistic-action.js';
|
|
4
|
+
import { beginSubmit, endSubmit } from './internal/form-submit-store.js';
|
|
5
|
+
import { setLastActionResult } from './internal/action-result-store.js';
|
|
6
|
+
import { assignSafeRedirect } from './internal/safe-redirect.js';
|
|
7
|
+
function hasOptimisticBrand(action) {
|
|
8
|
+
return OPTIMISTIC_BRAND in action;
|
|
9
|
+
}
|
|
16
10
|
function collectFormData(fd) {
|
|
17
|
-
const
|
|
11
|
+
const out = {};
|
|
18
12
|
for (const [key, value] of fd.entries()) {
|
|
19
|
-
if (key
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
13
|
+
if (key === '__module' || key === '__action')
|
|
14
|
+
continue;
|
|
15
|
+
const existing = out[key];
|
|
16
|
+
out[key] =
|
|
17
|
+
existing === undefined
|
|
18
|
+
? value
|
|
19
|
+
: Array.isArray(existing)
|
|
20
|
+
? [...existing, value]
|
|
21
|
+
: [existing, value];
|
|
28
22
|
}
|
|
29
|
-
return
|
|
23
|
+
return out;
|
|
30
24
|
}
|
|
31
|
-
export function Form({
|
|
32
|
-
const
|
|
25
|
+
export function Form({ action, children, ...rest }) {
|
|
26
|
+
const [pending, setPending] = useState(false);
|
|
27
|
+
const moduleKey = action.__module;
|
|
28
|
+
const actionName = action.__action;
|
|
29
|
+
const optimistic = useMemo(() => (hasOptimisticBrand(action) ? action[OPTIMISTIC_BRAND] : undefined), [action]);
|
|
30
|
+
const handleSubmit = useCallback(async (e) => {
|
|
33
31
|
e.preventDefault();
|
|
34
32
|
const formEl = e.currentTarget;
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
const target = typeof window !== 'undefined'
|
|
34
|
+
? window.location.pathname + window.location.search
|
|
35
|
+
: '/';
|
|
36
|
+
const fd = new FormData(formEl);
|
|
37
|
+
const payload = collectFormData(fd);
|
|
38
|
+
let handle;
|
|
39
|
+
if (optimistic)
|
|
40
|
+
handle = optimistic.addOptimistic(payload);
|
|
41
|
+
setPending(true);
|
|
42
|
+
beginSubmit(moduleKey, actionName);
|
|
43
|
+
try {
|
|
44
|
+
const res = await fetch(target, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
body: fd,
|
|
47
|
+
headers: { Accept: 'application/json' },
|
|
48
|
+
});
|
|
49
|
+
const env = (await res.json().catch(() => null));
|
|
50
|
+
if (!env) {
|
|
51
|
+
handle?.revert();
|
|
52
|
+
if (typeof window !== 'undefined')
|
|
53
|
+
window.location.reload();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (env.__outcome === 'redirect' && typeof env.to === 'string') {
|
|
57
|
+
const navigated = assignSafeRedirect(env.to);
|
|
58
|
+
if (navigated) {
|
|
59
|
+
handle?.settle();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// Cross-origin: revert optimistic, surface as error result so useActionResult sees it.
|
|
63
|
+
handle?.revert();
|
|
64
|
+
setLastActionResult(moduleKey, actionName, {
|
|
65
|
+
kind: 'error',
|
|
66
|
+
message: `Refused cross-origin redirect to ${env.to}`,
|
|
67
|
+
submittedPayload: payload,
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (env.__outcome === 'success') {
|
|
72
|
+
handle?.settle();
|
|
73
|
+
setLastActionResult(moduleKey, actionName, {
|
|
74
|
+
kind: 'success',
|
|
75
|
+
data: env.data,
|
|
76
|
+
submittedPayload: payload,
|
|
77
|
+
});
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (env.__outcome === 'deny') {
|
|
81
|
+
handle?.revert();
|
|
82
|
+
setLastActionResult(moduleKey, actionName, {
|
|
83
|
+
kind: 'deny',
|
|
84
|
+
status: env.status ?? res.status,
|
|
85
|
+
message: env.message ?? `Request denied (${env.status ?? res.status})`,
|
|
86
|
+
data: env.data,
|
|
87
|
+
submittedPayload: payload,
|
|
88
|
+
});
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (env.__outcome === 'error') {
|
|
92
|
+
handle?.revert();
|
|
93
|
+
setLastActionResult(moduleKey, actionName, {
|
|
94
|
+
kind: 'error',
|
|
95
|
+
message: env.message ?? 'Action failed',
|
|
96
|
+
submittedPayload: payload,
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// Unknown outcome (e.g. 'timeout'): treat as error.
|
|
101
|
+
handle?.revert();
|
|
102
|
+
setLastActionResult(moduleKey, actionName, {
|
|
103
|
+
kind: 'error',
|
|
104
|
+
message: env.message ?? `Unexpected outcome: ${env.__outcome ?? 'unknown'}`,
|
|
105
|
+
submittedPayload: payload,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
handle?.revert();
|
|
110
|
+
setLastActionResult(moduleKey, actionName, {
|
|
111
|
+
kind: 'error',
|
|
112
|
+
message: err instanceof Error ? err.message : String(err),
|
|
113
|
+
submittedPayload: payload,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
setPending(false);
|
|
118
|
+
endSubmit(moduleKey, actionName);
|
|
119
|
+
}
|
|
120
|
+
}, [moduleKey, actionName, optimistic]);
|
|
121
|
+
return (_jsxs("form", { ...rest, method: "post", enctype: "multipart/form-data", onSubmit: handleSubmit, children: [_jsx("input", { type: "hidden", name: "__module", value: moduleKey }), _jsx("input", { type: "hidden", name: "__action", value: actionName }), _jsx("fieldset", { disabled: pending, class: "hp-form-fieldset", children: children })] }));
|
|
40
122
|
}
|
package/dist/iso/index.d.ts
CHANGED
|
@@ -4,23 +4,31 @@ 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
|
-
export { defineAction, useAction } from './action.js';
|
|
11
|
-
export type { ActionStub, UseActionOptions, UseActionResult, MutateResult,
|
|
12
|
-
export { ActionGuardError, defineActionGuard } from './action.js';
|
|
10
|
+
export { defineAction, useAction, TimeoutError } 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';
|
|
16
|
-
export type { OptimisticHandle } from './optimistic.js';
|
|
15
|
+
export type { OptimisticHandle, UseOptimisticOptions } from './optimistic.js';
|
|
17
16
|
export { useOptimisticAction } from './optimistic-action.js';
|
|
18
17
|
export type { UseOptimisticActionOptions, UseOptimisticActionResult, } from './optimistic-action.js';
|
|
19
18
|
export { Form } from './form.js';
|
|
19
|
+
export { useActionResult, type ActionResult } from './use-action-result.js';
|
|
20
|
+
export { ActionResultContext, type ActionResultContextValue, } from './action-result-context.js';
|
|
21
|
+
export { useFormStatus, type FormStatus } from './use-form-status.js';
|
|
20
22
|
export { createCache } from './cache.js';
|
|
21
23
|
export type { LoaderCache } from './cache.js';
|
|
22
|
-
export {
|
|
23
|
-
export type {
|
|
24
|
+
export { defineServerMiddleware, defineClientMiddleware, } from './define-middleware.js';
|
|
25
|
+
export type { ServerMiddleware, ClientMiddleware, Middleware, ServerBaseCtx, ServerPageCtx, ServerLoaderCtx, ServerActionCtx, ServerCtx, ClientPageCtx, Scope, Next, } from './define-middleware.js';
|
|
26
|
+
export { defineStreamObserver } from './define-stream-observer.js';
|
|
27
|
+
export type { StreamObserver, ServerStreamCtx, } from './define-stream-observer.js';
|
|
28
|
+
export { defineApp } from './define-app.js';
|
|
29
|
+
export type { AppConfig, AppUseElement } from './define-app.js';
|
|
30
|
+
export { redirect, deny, timeoutOutcome, isOutcome, isRedirect, isDeny, isRender, isTimeout, } from './outcomes.js';
|
|
31
|
+
export type { Outcome, RedirectOutcome, DenyOutcome, RenderOutcome, TimeoutOutcome, RedirectStatusCode, ErrorStatusCode, } from './outcomes.js';
|
|
24
32
|
export { prefetch } from './prefetch.js';
|
|
25
33
|
export { isBrowser, env } from './is-browser.js';
|
|
26
34
|
export { useRouteChange } from './route-change.js';
|
package/dist/iso/index.js
CHANGED
|
@@ -8,18 +8,23 @@ export { Route, Router, lazy, useLocation, useRoute } from 'preact-iso';
|
|
|
8
8
|
export { defineRoutes, Routes } from './define-routes.js';
|
|
9
9
|
// Server bindings.
|
|
10
10
|
export { defineLoader } from './define-loader.js';
|
|
11
|
-
export { defineAction, useAction } from './action.js';
|
|
12
|
-
export { ActionGuardError, defineActionGuard } from './action.js';
|
|
11
|
+
export { defineAction, useAction, TimeoutError } from './action.js';
|
|
13
12
|
// Hooks.
|
|
14
13
|
export { useReload } from './reload-context.js';
|
|
15
14
|
export { useOptimistic } from './optimistic.js';
|
|
16
15
|
export { useOptimisticAction } from './optimistic-action.js';
|
|
17
16
|
// Forms.
|
|
18
17
|
export { Form } from './form.js';
|
|
18
|
+
export { useActionResult } from './use-action-result.js';
|
|
19
|
+
export { ActionResultContext, } from './action-result-context.js';
|
|
20
|
+
export { useFormStatus } from './use-form-status.js';
|
|
19
21
|
// Cache + invalidation.
|
|
20
22
|
export { createCache } from './cache.js';
|
|
21
|
-
//
|
|
22
|
-
export {
|
|
23
|
+
// Middleware + outcomes (the new system).
|
|
24
|
+
export { defineServerMiddleware, defineClientMiddleware, } from './define-middleware.js';
|
|
25
|
+
export { defineStreamObserver } from './define-stream-observer.js';
|
|
26
|
+
export { defineApp } from './define-app.js';
|
|
27
|
+
export { redirect, deny, timeoutOutcome, isOutcome, isRedirect, isDeny, isRender, isTimeout, } from './outcomes.js';
|
|
23
28
|
// Utilities.
|
|
24
29
|
export { prefetch } from './prefetch.js';
|
|
25
30
|
export { isBrowser, env } from './is-browser.js';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ContentfulStatusCode } from 'hono/utils/http-status';
|
|
2
|
+
import type { Outcome } from '../outcomes.js';
|
|
3
|
+
export type ActionEnvelope = {
|
|
4
|
+
__outcome: 'success';
|
|
5
|
+
data: unknown;
|
|
6
|
+
} | {
|
|
7
|
+
__outcome: 'redirect';
|
|
8
|
+
to: string;
|
|
9
|
+
status: number;
|
|
10
|
+
} | {
|
|
11
|
+
__outcome: 'deny';
|
|
12
|
+
status: number;
|
|
13
|
+
message: string;
|
|
14
|
+
data?: unknown;
|
|
15
|
+
} | {
|
|
16
|
+
__outcome: 'error';
|
|
17
|
+
message: string;
|
|
18
|
+
} | {
|
|
19
|
+
__outcome: 'timeout';
|
|
20
|
+
timeoutMs: number;
|
|
21
|
+
};
|
|
22
|
+
export type ActionResolution = {
|
|
23
|
+
kind: 'success';
|
|
24
|
+
data: unknown;
|
|
25
|
+
} | {
|
|
26
|
+
kind: 'outcome';
|
|
27
|
+
outcome: Outcome;
|
|
28
|
+
} | {
|
|
29
|
+
kind: 'error';
|
|
30
|
+
message: string;
|
|
31
|
+
};
|
|
32
|
+
export type SerializedEnvelope = {
|
|
33
|
+
body: ActionEnvelope;
|
|
34
|
+
status: ContentfulStatusCode;
|
|
35
|
+
headers: Record<string, string> | undefined;
|
|
36
|
+
};
|
|
37
|
+
export declare function serializeActionOutcome(resolution: ActionResolution): SerializedEnvelope;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export function serializeActionOutcome(resolution) {
|
|
2
|
+
if (resolution.kind === 'success') {
|
|
3
|
+
return {
|
|
4
|
+
body: { __outcome: 'success', data: resolution.data },
|
|
5
|
+
status: 200,
|
|
6
|
+
headers: undefined,
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
if (resolution.kind === 'error') {
|
|
10
|
+
return {
|
|
11
|
+
body: { __outcome: 'error', message: resolution.message },
|
|
12
|
+
status: 500,
|
|
13
|
+
headers: undefined,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
const { outcome } = resolution;
|
|
17
|
+
if (outcome.__outcome === 'redirect') {
|
|
18
|
+
return {
|
|
19
|
+
body: { __outcome: 'redirect', to: outcome.to, status: outcome.status },
|
|
20
|
+
status: 200,
|
|
21
|
+
headers: outcome.headers,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (outcome.__outcome === 'deny') {
|
|
25
|
+
const body = {
|
|
26
|
+
__outcome: 'deny',
|
|
27
|
+
status: outcome.status,
|
|
28
|
+
message: outcome.message,
|
|
29
|
+
};
|
|
30
|
+
if (outcome.data !== undefined)
|
|
31
|
+
body.data = outcome.data;
|
|
32
|
+
return { body, status: outcome.status, headers: outcome.headers };
|
|
33
|
+
}
|
|
34
|
+
if (outcome.__outcome === 'timeout') {
|
|
35
|
+
return {
|
|
36
|
+
body: { __outcome: 'timeout', timeoutMs: outcome.timeoutMs },
|
|
37
|
+
status: 504,
|
|
38
|
+
headers: undefined,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
// 'render' outcome is page-scope only; should never reach an action.
|
|
42
|
+
return {
|
|
43
|
+
body: { __outcome: 'error', message: 'render outcome is page-scope only' },
|
|
44
|
+
status: 500,
|
|
45
|
+
headers: undefined,
|
|
46
|
+
};
|
|
47
|
+
}
|