hono-preact 0.2.0 → 0.4.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/dist/iso/action-result-context.d.ts +22 -0
- package/dist/iso/action-result-context.js +2 -0
- package/dist/iso/action.d.ts +52 -13
- package/dist/iso/action.js +204 -88
- package/dist/iso/cache.d.ts +9 -0
- package/dist/iso/cache.js +26 -0
- package/dist/iso/define-app.d.ts +7 -0
- package/dist/iso/define-loader.d.ts +12 -0
- package/dist/iso/define-loader.js +26 -16
- package/dist/iso/form.d.ts +13 -4
- package/dist/iso/form.js +115 -33
- package/dist/iso/index.d.ts +13 -4
- package/dist/iso/index.js +14 -2
- 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/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/history-shim.d.ts +7 -0
- package/dist/iso/internal/history-shim.js +79 -0
- package/dist/iso/internal/loader-fetch.js +65 -34
- package/dist/iso/internal/loader.d.ts +3 -3
- package/dist/iso/internal/merge-refs.d.ts +4 -0
- package/dist/iso/internal/merge-refs.js +14 -0
- package/dist/iso/internal/persist-registry.d.ts +10 -0
- package/dist/iso/internal/persist-registry.js +24 -0
- package/dist/iso/internal/route-boundary.d.ts +4 -4
- package/dist/iso/internal/route-change.d.ts +8 -2
- package/dist/iso/internal/route-change.js +107 -12
- 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/use-render.d.ts +11 -0
- package/dist/iso/internal/use-render.js +47 -0
- package/dist/iso/internal/view-transition-event.d.ts +23 -0
- package/dist/iso/internal/view-transition-event.js +25 -0
- package/dist/iso/internal.d.ts +12 -1
- package/dist/iso/internal.js +13 -1
- 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 +14 -2
- package/dist/iso/outcomes.js +14 -3
- package/dist/iso/persist.d.ts +14 -0
- package/dist/iso/persist.js +56 -0
- 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/iso/view-transition-lifecycle.d.ts +9 -0
- package/dist/iso/view-transition-lifecycle.js +18 -0
- package/dist/iso/view-transition-name.d.ts +17 -0
- package/dist/iso/view-transition-name.js +79 -0
- package/dist/iso/view-transition-types.d.ts +8 -0
- package/dist/iso/view-transition-types.js +21 -0
- package/dist/server/actions-handler.d.ts +7 -0
- package/dist/server/actions-handler.js +42 -9
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +2 -1
- package/dist/server/loaders-handler.d.ts +8 -0
- package/dist/server/loaders-handler.js +37 -4
- 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.js +136 -55
- package/dist/server/route-server-modules.d.ts +7 -8
- package/dist/server/route-server-modules.js +7 -8
- package/dist/server/speculation-rules.d.ts +3 -0
- package/dist/server/speculation-rules.js +8 -0
- package/dist/server/sse.d.ts +43 -28
- package/dist/server/sse.js +113 -88
- package/dist/vite/client-entry.js +12 -3
- package/dist/vite/server-entry.js +10 -2
- package/package.json +2 -2
|
@@ -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,7 @@ export function defineLoader(fn, opts) {
|
|
|
80
88
|
fn,
|
|
81
89
|
cache: cache,
|
|
82
90
|
params: opts?.params ?? [],
|
|
91
|
+
timeoutMs: opts?.timeoutMs,
|
|
83
92
|
// LoaderUse<T, boolean> structurally collapses to the same shape the
|
|
84
93
|
// partitioner accepts; the cast hides only the generic narrowing on
|
|
85
94
|
// StreamObserver's TChunk/TResult which is invariant. Identity-preserving.
|
|
@@ -97,26 +106,27 @@ export function defineLoader(fn, opts) {
|
|
|
97
106
|
invalidate() {
|
|
98
107
|
cache.invalidate();
|
|
99
108
|
},
|
|
100
|
-
Boundary
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
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), {
|
|
105
113
|
loader: ref,
|
|
106
114
|
fallback,
|
|
107
115
|
errorFallback,
|
|
108
116
|
children,
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
+
},
|
|
119
130
|
};
|
|
120
|
-
ref.View = View;
|
|
121
131
|
return ref;
|
|
122
132
|
}
|
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
|
@@ -7,15 +7,18 @@ export { defineRoutes, Routes } from './define-routes.js';
|
|
|
7
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';
|
|
10
|
+
export { defineAction, useAction, TimeoutError } from './action.js';
|
|
11
11
|
export type { ActionStub, UseActionOptions, UseActionResult, MutateResult, } from './action.js';
|
|
12
12
|
export type { ContentfulStatusCode } from 'hono/utils/http-status';
|
|
13
13
|
export { useReload } from './reload-context.js';
|
|
14
14
|
export { useOptimistic } from './optimistic.js';
|
|
15
|
-
export type { OptimisticHandle } from './optimistic.js';
|
|
15
|
+
export type { OptimisticHandle, UseOptimisticOptions } from './optimistic.js';
|
|
16
16
|
export { useOptimisticAction } from './optimistic-action.js';
|
|
17
17
|
export type { UseOptimisticActionOptions, UseOptimisticActionResult, } from './optimistic-action.js';
|
|
18
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';
|
|
19
22
|
export { createCache } from './cache.js';
|
|
20
23
|
export type { LoaderCache } from './cache.js';
|
|
21
24
|
export { defineServerMiddleware, defineClientMiddleware, } from './define-middleware.js';
|
|
@@ -24,8 +27,8 @@ export { defineStreamObserver } from './define-stream-observer.js';
|
|
|
24
27
|
export type { StreamObserver, ServerStreamCtx, } from './define-stream-observer.js';
|
|
25
28
|
export { defineApp } from './define-app.js';
|
|
26
29
|
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';
|
|
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';
|
|
29
32
|
export { prefetch } from './prefetch.js';
|
|
30
33
|
export { isBrowser, env } from './is-browser.js';
|
|
31
34
|
export { useRouteChange } from './route-change.js';
|
|
@@ -33,3 +36,9 @@ export type { RouteChangeHandler } from './route-change.js';
|
|
|
33
36
|
export { Head } from './head.js';
|
|
34
37
|
export type { HeadProps } from './head.js';
|
|
35
38
|
export { ClientScript } from './client-script.js';
|
|
39
|
+
export { useViewTransitionLifecycle, type ViewTransitionLifecycle, type ViewTransitionPhaseCallback, } from './view-transition-lifecycle.js';
|
|
40
|
+
export type { ViewTransitionEvent, NavDirection, ViewTransitionReason, } from './internal/view-transition-event.js';
|
|
41
|
+
export { useViewTransitionTypes, type ViewTransitionTypesInput, type ViewTransitionTypesNav, } from './view-transition-types.js';
|
|
42
|
+
export { getNavDirection as getViewTransitionDirection } from './internal/history-shim.js';
|
|
43
|
+
export { useViewTransitionName, useViewTransitionClass, ViewTransitionName, ViewTransitionGroup, type ViewTransitionNameProps, type ViewTransitionGroupProps, } from './view-transition-name.js';
|
|
44
|
+
export { Persist, PersistHost, type PersistProps } from './persist.js';
|
package/dist/iso/index.js
CHANGED
|
@@ -8,20 +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';
|
|
11
|
+
export { defineAction, useAction, TimeoutError } from './action.js';
|
|
12
12
|
// Hooks.
|
|
13
13
|
export { useReload } from './reload-context.js';
|
|
14
14
|
export { useOptimistic } from './optimistic.js';
|
|
15
15
|
export { useOptimisticAction } from './optimistic-action.js';
|
|
16
16
|
// Forms.
|
|
17
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';
|
|
18
21
|
// Cache + invalidation.
|
|
19
22
|
export { createCache } from './cache.js';
|
|
20
23
|
// Middleware + outcomes (the new system).
|
|
21
24
|
export { defineServerMiddleware, defineClientMiddleware, } from './define-middleware.js';
|
|
22
25
|
export { defineStreamObserver } from './define-stream-observer.js';
|
|
23
26
|
export { defineApp } from './define-app.js';
|
|
24
|
-
export { redirect, deny, isOutcome, isRedirect, isDeny, isRender, } from './outcomes.js';
|
|
27
|
+
export { redirect, deny, timeoutOutcome, isOutcome, isRedirect, isDeny, isRender, isTimeout, } from './outcomes.js';
|
|
25
28
|
// Utilities.
|
|
26
29
|
export { prefetch } from './prefetch.js';
|
|
27
30
|
export { isBrowser, env } from './is-browser.js';
|
|
@@ -29,3 +32,12 @@ export { isBrowser, env } from './is-browser.js';
|
|
|
29
32
|
export { useRouteChange } from './route-change.js';
|
|
30
33
|
export { Head } from './head.js';
|
|
31
34
|
export { ClientScript } from './client-script.js';
|
|
35
|
+
// View transition lifecycle hook.
|
|
36
|
+
export { useViewTransitionLifecycle, } from './view-transition-lifecycle.js';
|
|
37
|
+
// View transitions types.
|
|
38
|
+
export { useViewTransitionTypes, } from './view-transition-types.js';
|
|
39
|
+
export { getNavDirection as getViewTransitionDirection } from './internal/history-shim.js';
|
|
40
|
+
// View transition name + group hooks and components.
|
|
41
|
+
export { useViewTransitionName, useViewTransitionClass, ViewTransitionName, ViewTransitionGroup, } from './view-transition-name.js';
|
|
42
|
+
// Persist components.
|
|
43
|
+
export { Persist, PersistHost } from './persist.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
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
type Listener = () => void;
|
|
2
|
+
export type StoredActionResult = {
|
|
3
|
+
kind: 'success';
|
|
4
|
+
data: unknown;
|
|
5
|
+
submittedPayload: unknown;
|
|
6
|
+
} | {
|
|
7
|
+
kind: 'deny';
|
|
8
|
+
status: number;
|
|
9
|
+
message: string;
|
|
10
|
+
data?: unknown;
|
|
11
|
+
submittedPayload: unknown;
|
|
12
|
+
} | {
|
|
13
|
+
kind: 'error';
|
|
14
|
+
message: string;
|
|
15
|
+
submittedPayload: unknown | null;
|
|
16
|
+
};
|
|
17
|
+
type Entry = StoredActionResult & {
|
|
18
|
+
module: string;
|
|
19
|
+
action: string;
|
|
20
|
+
};
|
|
21
|
+
export declare function setLastActionResult(module: string, action: string, result: StoredActionResult): void;
|
|
22
|
+
export declare function clearLastActionResult(module: string, action: string): void;
|
|
23
|
+
export declare function getLastActionResult(stub?: {
|
|
24
|
+
__module: string;
|
|
25
|
+
__action: string;
|
|
26
|
+
}): Entry | null;
|
|
27
|
+
export declare function subscribeActionResults(listener: Listener): () => void;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const results = new Map();
|
|
2
|
+
const listeners = new Set();
|
|
3
|
+
function key(module, action) {
|
|
4
|
+
return `${module}::${action}`;
|
|
5
|
+
}
|
|
6
|
+
export function setLastActionResult(module, action, result) {
|
|
7
|
+
const k = key(module, action);
|
|
8
|
+
// Delete-then-set to bump to most-recent position in Map iteration order,
|
|
9
|
+
// so no-stub readers see the latest action result, not the earliest.
|
|
10
|
+
results.delete(k);
|
|
11
|
+
results.set(k, { ...result, module, action });
|
|
12
|
+
for (const l of listeners)
|
|
13
|
+
l();
|
|
14
|
+
}
|
|
15
|
+
export function clearLastActionResult(module, action) {
|
|
16
|
+
if (results.delete(key(module, action))) {
|
|
17
|
+
for (const l of listeners)
|
|
18
|
+
l();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function getLastActionResult(stub) {
|
|
22
|
+
if (stub)
|
|
23
|
+
return results.get(key(stub.__module, stub.__action)) ?? null;
|
|
24
|
+
// No stub: return the most recently written entry.
|
|
25
|
+
let last = null;
|
|
26
|
+
for (const entry of results.values())
|
|
27
|
+
last = entry;
|
|
28
|
+
return last;
|
|
29
|
+
}
|
|
30
|
+
export function subscribeActionResults(listener) {
|
|
31
|
+
listeners.add(listener);
|
|
32
|
+
return () => {
|
|
33
|
+
listeners.delete(listener);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -13,8 +13,7 @@ export const Envelope = ({ as = 'section', children, }) => {
|
|
|
13
13
|
const dataLoader = isBrowser() ? 'null' : JSON.stringify(ctx.data ?? null);
|
|
14
14
|
if (typeof as === 'string') {
|
|
15
15
|
const Tag = as;
|
|
16
|
-
|
|
17
|
-
return (_jsx(Tag, { ...props, children: children }));
|
|
16
|
+
return (_jsx(Tag, { id: id, "data-loader": dataLoader, children: children }));
|
|
18
17
|
}
|
|
19
18
|
const Wrapper = as;
|
|
20
19
|
return (_jsx(Wrapper, { id: id, "data-loader": dataLoader, children: children }));
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type Listener = () => void;
|
|
2
|
+
export declare function beginSubmit(module: string, action: string): void;
|
|
3
|
+
export declare function endSubmit(module: string, action: string): void;
|
|
4
|
+
export declare function isPending(stub?: {
|
|
5
|
+
__module: string;
|
|
6
|
+
__action: string;
|
|
7
|
+
}): boolean;
|
|
8
|
+
export declare function subscribe(listener: Listener): () => void;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const counts = new Map();
|
|
2
|
+
const listeners = new Set();
|
|
3
|
+
function key(module, action) {
|
|
4
|
+
return `${module}::${action}`;
|
|
5
|
+
}
|
|
6
|
+
export function beginSubmit(module, action) {
|
|
7
|
+
const k = key(module, action);
|
|
8
|
+
counts.set(k, (counts.get(k) ?? 0) + 1);
|
|
9
|
+
for (const l of listeners)
|
|
10
|
+
l();
|
|
11
|
+
}
|
|
12
|
+
export function endSubmit(module, action) {
|
|
13
|
+
const k = key(module, action);
|
|
14
|
+
const n = (counts.get(k) ?? 0) - 1;
|
|
15
|
+
if (n <= 0)
|
|
16
|
+
counts.delete(k);
|
|
17
|
+
else
|
|
18
|
+
counts.set(k, n);
|
|
19
|
+
for (const l of listeners)
|
|
20
|
+
l();
|
|
21
|
+
}
|
|
22
|
+
export function isPending(stub) {
|
|
23
|
+
if (stub)
|
|
24
|
+
return (counts.get(key(stub.__module, stub.__action)) ?? 0) > 0;
|
|
25
|
+
return counts.size > 0;
|
|
26
|
+
}
|
|
27
|
+
export function subscribe(listener) {
|
|
28
|
+
listeners.add(listener);
|
|
29
|
+
return () => {
|
|
30
|
+
listeners.delete(listener);
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { NavDirection } from './view-transition-event.js';
|
|
2
|
+
export declare function installHistoryShim(): void;
|
|
3
|
+
export declare function getNavDirection(): NavDirection;
|
|
4
|
+
/** Test-only reset. Do not call from production code. */
|
|
5
|
+
export declare function resetHistoryShimForTesting(): void;
|
|
6
|
+
/** Test-only direction setter. Do not call from production code. */
|
|
7
|
+
export declare function setNavDirectionForTesting(dir: NavDirection): void;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
let installed = false;
|
|
2
|
+
let counter = 0;
|
|
3
|
+
let lastDirection = 'initial';
|
|
4
|
+
let originalPush = null;
|
|
5
|
+
let originalReplace = null;
|
|
6
|
+
let popstateListener = null;
|
|
7
|
+
function readCounterFromState() {
|
|
8
|
+
if (typeof history === 'undefined')
|
|
9
|
+
return 0;
|
|
10
|
+
const state = history.state;
|
|
11
|
+
return state?.__hpVtIdx ?? 0;
|
|
12
|
+
}
|
|
13
|
+
export function installHistoryShim() {
|
|
14
|
+
if (installed)
|
|
15
|
+
return;
|
|
16
|
+
if (typeof history === 'undefined' || typeof window === 'undefined')
|
|
17
|
+
return;
|
|
18
|
+
installed = true;
|
|
19
|
+
counter = readCounterFromState();
|
|
20
|
+
lastDirection = 'initial';
|
|
21
|
+
originalPush = history.pushState.bind(history);
|
|
22
|
+
originalReplace = history.replaceState.bind(history);
|
|
23
|
+
history.pushState = function patchedPush(state, title, url) {
|
|
24
|
+
counter += 1;
|
|
25
|
+
const merged = {
|
|
26
|
+
...(state ?? {}),
|
|
27
|
+
__hpVtIdx: counter,
|
|
28
|
+
};
|
|
29
|
+
originalPush(merged, title, url);
|
|
30
|
+
lastDirection = 'push';
|
|
31
|
+
};
|
|
32
|
+
history.replaceState = function patchedReplace(state, title, url) {
|
|
33
|
+
const merged = {
|
|
34
|
+
...(state ?? {}),
|
|
35
|
+
__hpVtIdx: counter,
|
|
36
|
+
};
|
|
37
|
+
originalReplace(merged, title, url);
|
|
38
|
+
lastDirection = 'replace';
|
|
39
|
+
};
|
|
40
|
+
popstateListener = (e) => {
|
|
41
|
+
const incoming = e.state?.__hpVtIdx ?? 0;
|
|
42
|
+
lastDirection =
|
|
43
|
+
incoming < counter ? 'back' : incoming > counter ? 'forward' : 'replace';
|
|
44
|
+
counter = incoming;
|
|
45
|
+
};
|
|
46
|
+
window.addEventListener('popstate', popstateListener, { capture: true });
|
|
47
|
+
// Stamp the current entry so subsequent diffs are well-defined.
|
|
48
|
+
if (history.state?.__hpVtIdx === undefined) {
|
|
49
|
+
originalReplace({ ...(history.state ?? {}), __hpVtIdx: counter }, '');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function getNavDirection() {
|
|
53
|
+
return lastDirection;
|
|
54
|
+
}
|
|
55
|
+
/** Test-only reset. Do not call from production code. */
|
|
56
|
+
export function resetHistoryShimForTesting() {
|
|
57
|
+
if (installed &&
|
|
58
|
+
typeof history !== 'undefined' &&
|
|
59
|
+
originalPush &&
|
|
60
|
+
originalReplace) {
|
|
61
|
+
history.pushState = originalPush;
|
|
62
|
+
history.replaceState = originalReplace;
|
|
63
|
+
}
|
|
64
|
+
if (typeof window !== 'undefined' && popstateListener) {
|
|
65
|
+
window.removeEventListener('popstate', popstateListener, {
|
|
66
|
+
capture: true,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
installed = false;
|
|
70
|
+
counter = 0;
|
|
71
|
+
lastDirection = 'initial';
|
|
72
|
+
originalPush = null;
|
|
73
|
+
originalReplace = null;
|
|
74
|
+
popstateListener = null;
|
|
75
|
+
}
|
|
76
|
+
/** Test-only direction setter. Do not call from production code. */
|
|
77
|
+
export function setNavDirectionForTesting(dir) {
|
|
78
|
+
lastDirection = dir;
|
|
79
|
+
}
|