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
package/dist/iso/internal.js
CHANGED
|
@@ -28,17 +28,26 @@ export { Loader } from './internal/loader.js';
|
|
|
28
28
|
export { Envelope } from './internal/envelope.js';
|
|
29
29
|
export { RouteBoundary } from './internal/route-boundary.js';
|
|
30
30
|
export { OptimisticOverlay } from './internal/optimistic-overlay.js';
|
|
31
|
+
export { serializeActionOutcome, } from './internal/action-envelope.js';
|
|
31
32
|
export { LoaderIdContext, LoaderDataContext } from './internal/contexts.js';
|
|
32
33
|
export { ReloadContext } from './reload-context.js';
|
|
33
34
|
export { RouteLocationsContext, RouteLocationsProvider, } from './internal/route-locations.js';
|
|
34
35
|
export { getPreloadedData, deletePreloadedData } from './internal/preload.js';
|
|
35
|
-
export { runRequestScope, getRequestStore, captureRequestScope, } from './cache.js';
|
|
36
|
+
export { runRequestScope, getRequestStore, captureRequestScope, getActionResultSlot, setActionResultSlot, } from './cache.js';
|
|
36
37
|
export { default as wrapPromise } from './internal/wrap-promise.js';
|
|
37
38
|
export { HonoRequestContext } from './internal/contexts.js';
|
|
38
39
|
export { PageMiddlewareHost } from './internal/page-middleware-host.js';
|
|
39
40
|
export { __dispatchRouteChange, __subscribeRouteChange, } from './internal/route-change.js';
|
|
41
|
+
export { installHistoryShim, getNavDirection, } from './internal/history-shim.js';
|
|
42
|
+
export { __subscribePhase } from './internal/route-change.js';
|
|
43
|
+
export { ViewTransitionEvent, } from './internal/view-transition-event.js';
|
|
44
|
+
export { useRender } from './internal/use-render.js';
|
|
45
|
+
export { mergeRefs } from './internal/merge-refs.js';
|
|
40
46
|
export { installStreamRegistry, subscribeToLoaderStream, } from './internal/stream-registry.js';
|
|
41
47
|
export { registerServerStreamingLoader, takeServerStreamingLoaders, } from './internal/streaming-ssr.js';
|
|
48
|
+
export { beginSubmit, endSubmit, isPending, subscribe as subscribeFormSubmit, } from './internal/form-submit-store.js';
|
|
49
|
+
export { assignSafeRedirect, isSameOrigin } from './internal/safe-redirect.js';
|
|
50
|
+
export { setLastActionResult, clearLastActionResult, getLastActionResult, subscribeActionResults, } from './internal/action-result-store.js';
|
|
42
51
|
// Middleware dispatcher + observer fanout. Internal-stability subpath.
|
|
43
52
|
export { dispatchServer, dispatchClient, } from './internal/middleware-runner.js';
|
|
44
53
|
export { partitionUse } from './internal/use-partitioner.js';
|
|
@@ -49,3 +58,6 @@ export { fanStart, fanChunk, fanEnd, fanError, fanAbort, } from './internal/stre
|
|
|
49
58
|
// (serverOnlyPlugin's loader stubs, guardStripPlugin's no-op replacement).
|
|
50
59
|
// User code that imports them couples to plugin internals.
|
|
51
60
|
export { __$createLoaderStub_hpiso } from './internal/loader-stub.js';
|
|
61
|
+
// SSE decoder; useful in tests and advanced consumers that need to read
|
|
62
|
+
// a streaming loader/action response as a sequence of typed SSE events.
|
|
63
|
+
export { readSSE } from './internal/sse-decoder.js';
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import { type UseActionOptions, type UseActionResult, type ActionStub } from './action.js';
|
|
2
|
+
import { type OptimisticHandle } from './optimistic.js';
|
|
2
3
|
import type { LoaderRef } from './define-loader.js';
|
|
4
|
+
export declare const OPTIMISTIC_BRAND: unique symbol;
|
|
5
|
+
export type OptimisticBinding<TPayload, TBase> = {
|
|
6
|
+
apply: (current: TBase, payload: TPayload) => TBase;
|
|
7
|
+
addOptimistic: (payload: TPayload) => OptimisticHandle;
|
|
8
|
+
};
|
|
3
9
|
export type UseOptimisticActionOptions<TPayload, TResult, TBase, TChunk = never> = Omit<UseActionOptions<TPayload, TResult, TChunk>, 'invalidate' | 'onMutate' | 'onError' | 'onSuccess'> & {
|
|
4
10
|
base: TBase;
|
|
5
11
|
apply: (current: TBase, payload: TPayload) => TBase;
|
|
6
12
|
invalidate?: 'auto' | ReadonlyArray<LoaderRef<unknown>>;
|
|
7
13
|
onSuccess?: (data: TResult) => void;
|
|
8
14
|
onError?: (err: Error) => void;
|
|
15
|
+
/** Forwarded to the internal `useOptimistic` call. */
|
|
16
|
+
transition?: boolean;
|
|
9
17
|
};
|
|
10
|
-
export type UseOptimisticActionResult<TPayload, TResult, TBase> = UseActionResult<TPayload, TResult> & {
|
|
18
|
+
export type UseOptimisticActionResult<TPayload, TResult, TBase> = ActionStub<TPayload, TResult, never> & UseActionResult<TPayload, TResult> & {
|
|
11
19
|
value: TBase;
|
|
20
|
+
readonly [OPTIMISTIC_BRAND]: OptimisticBinding<TPayload, TBase>;
|
|
12
21
|
};
|
|
13
22
|
/**
|
|
14
23
|
* Like `useAction`, but with an optimistic-update wrapper. `TChunk` defaults
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useAction, } from './action.js';
|
|
2
2
|
import { useOptimistic } from './optimistic.js';
|
|
3
|
+
export const OPTIMISTIC_BRAND = Symbol('hono-preact.optimistic');
|
|
3
4
|
/**
|
|
4
5
|
* Like `useAction`, but with an optimistic-update wrapper. `TChunk` defaults
|
|
5
6
|
* to `never` so existing non-streaming call sites are unaffected. Pass the
|
|
@@ -7,8 +8,8 @@ import { useOptimistic } from './optimistic.js';
|
|
|
7
8
|
* when the action is streaming and you need a typed `onChunk` callback.
|
|
8
9
|
*/
|
|
9
10
|
export function useOptimisticAction(stub, options) {
|
|
10
|
-
const { base, apply, onSuccess, onError, ...actionOpts } = options;
|
|
11
|
-
const [value, addOptimistic] = useOptimistic(base, apply);
|
|
11
|
+
const { base, apply, onSuccess, onError, transition, ...actionOpts } = options;
|
|
12
|
+
const [value, addOptimistic] = useOptimistic(base, apply, { transition });
|
|
12
13
|
const action = useAction(stub, {
|
|
13
14
|
...actionOpts,
|
|
14
15
|
onMutate: (payload) => addOptimistic(payload),
|
|
@@ -21,5 +22,12 @@ export function useOptimisticAction(stub, options) {
|
|
|
21
22
|
onError?.(err);
|
|
22
23
|
},
|
|
23
24
|
});
|
|
24
|
-
return {
|
|
25
|
+
return {
|
|
26
|
+
__module: stub.__module,
|
|
27
|
+
__action: stub.__action,
|
|
28
|
+
useAction: stub.useAction,
|
|
29
|
+
...action,
|
|
30
|
+
value,
|
|
31
|
+
[OPTIMISTIC_BRAND]: { apply, addOptimistic },
|
|
32
|
+
};
|
|
25
33
|
}
|
package/dist/iso/optimistic.d.ts
CHANGED
|
@@ -2,4 +2,13 @@ export type OptimisticHandle = {
|
|
|
2
2
|
settle: () => void;
|
|
3
3
|
revert: () => void;
|
|
4
4
|
};
|
|
5
|
-
export
|
|
5
|
+
export type UseOptimisticOptions = {
|
|
6
|
+
/**
|
|
7
|
+
* When true, the settle and revert paths are wrapped in
|
|
8
|
+
* `document.startViewTransition`. The initial optimistic update is never
|
|
9
|
+
* wrapped (it must paint same-frame). Falls back to a synchronous update
|
|
10
|
+
* when `document.startViewTransition` is unavailable.
|
|
11
|
+
*/
|
|
12
|
+
transition?: boolean;
|
|
13
|
+
};
|
|
14
|
+
export declare function useOptimistic<TBase, TPayload>(base: TBase, reducer: (current: TBase, payload: TPayload) => TBase, options?: UseOptimisticOptions): [TBase, (payload: TPayload) => OptimisticHandle];
|
package/dist/iso/optimistic.js
CHANGED
|
@@ -1,14 +1,50 @@
|
|
|
1
1
|
import { useCallback, useReducer, useRef } from 'preact/hooks';
|
|
2
|
-
export function useOptimistic(base, reducer) {
|
|
2
|
+
export function useOptimistic(base, reducer, options) {
|
|
3
3
|
const queueRef = useRef([]);
|
|
4
4
|
const lastBaseRef = useRef(base);
|
|
5
5
|
const idRef = useRef(0);
|
|
6
6
|
const [, forceRender] = useReducer((c) => c + 1, 0);
|
|
7
|
+
const transitionRef = useRef(options?.transition === true);
|
|
8
|
+
transitionRef.current = options?.transition === true;
|
|
7
9
|
if (!Object.is(lastBaseRef.current, base)) {
|
|
8
10
|
queueRef.current = queueRef.current.filter((e) => e.status !== 'ready');
|
|
9
11
|
lastBaseRef.current = base;
|
|
10
12
|
}
|
|
11
13
|
const value = queueRef.current.reduce((acc, e) => reducer(acc, e.payload), base);
|
|
14
|
+
// Reads `transitionRef.current` at invocation time, not capture time, so it
|
|
15
|
+
// is safe to close over from the memoized `addOptimistic` (useCallback([]))
|
|
16
|
+
// below. Each render rebinds this function and writes the latest option
|
|
17
|
+
// value into the ref; settle/revert created by the stale memoized callback
|
|
18
|
+
// still see the up-to-date `transition` setting through the ref.
|
|
19
|
+
//
|
|
20
|
+
// The callback returns a promise that resolves on the next animation frame
|
|
21
|
+
// so the browser snapshots Preact's POST-render DOM as "new state". Without
|
|
22
|
+
// the rAF wait, `forceRender()` (an async dispatch through useReducer) has
|
|
23
|
+
// not yet flushed when `startViewTransition` snapshots, and the transition
|
|
24
|
+
// captures identical before/after frames with no visible animation.
|
|
25
|
+
const runWithTransition = (mutator) => {
|
|
26
|
+
if (transitionRef.current &&
|
|
27
|
+
typeof document !== 'undefined' &&
|
|
28
|
+
typeof document.startViewTransition === 'function') {
|
|
29
|
+
document.startViewTransition(async () => {
|
|
30
|
+
mutator();
|
|
31
|
+
await new Promise((resolve) => {
|
|
32
|
+
if (typeof requestAnimationFrame === 'function') {
|
|
33
|
+
requestAnimationFrame(() => resolve());
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
// Non-DOM environment (shouldn't reach this branch given the
|
|
37
|
+
// outer check, but defensive). Resolve on next microtask so
|
|
38
|
+
// Preact's scheduled render runs.
|
|
39
|
+
queueMicrotask(resolve);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
mutator();
|
|
46
|
+
}
|
|
47
|
+
};
|
|
12
48
|
const addOptimistic = useCallback((payload) => {
|
|
13
49
|
const id = ++idRef.current;
|
|
14
50
|
queueRef.current = [...queueRef.current, { id, payload, status: 'active' }];
|
|
@@ -17,13 +53,17 @@ export function useOptimistic(base, reducer) {
|
|
|
17
53
|
settle: () => {
|
|
18
54
|
const entry = queueRef.current.find((e) => e.id === id);
|
|
19
55
|
if (entry && entry.status === 'active') {
|
|
20
|
-
|
|
21
|
-
|
|
56
|
+
runWithTransition(() => {
|
|
57
|
+
entry.status = 'ready';
|
|
58
|
+
forceRender();
|
|
59
|
+
});
|
|
22
60
|
}
|
|
23
61
|
},
|
|
24
62
|
revert: () => {
|
|
25
|
-
|
|
26
|
-
|
|
63
|
+
runWithTransition(() => {
|
|
64
|
+
queueRef.current = queueRef.current.filter((e) => e.id !== id);
|
|
65
|
+
forceRender();
|
|
66
|
+
});
|
|
27
67
|
},
|
|
28
68
|
};
|
|
29
69
|
}, []);
|
package/dist/iso/outcomes.d.ts
CHANGED
|
@@ -13,12 +13,17 @@ export type DenyOutcome = {
|
|
|
13
13
|
status: ErrorStatusCode;
|
|
14
14
|
message: string;
|
|
15
15
|
headers: Record<string, string> | undefined;
|
|
16
|
+
data?: unknown;
|
|
16
17
|
};
|
|
17
18
|
export type RenderOutcome = {
|
|
18
19
|
__outcome: 'render';
|
|
19
20
|
Component: FunctionComponent;
|
|
20
21
|
};
|
|
21
|
-
export type
|
|
22
|
+
export type TimeoutOutcome = {
|
|
23
|
+
__outcome: 'timeout';
|
|
24
|
+
timeoutMs: number;
|
|
25
|
+
};
|
|
26
|
+
export type Outcome = RedirectOutcome | DenyOutcome | RenderOutcome | TimeoutOutcome;
|
|
22
27
|
type RedirectInput = string | {
|
|
23
28
|
to: string;
|
|
24
29
|
status?: RedirectStatusCode;
|
|
@@ -29,10 +34,17 @@ type DenyInput = {
|
|
|
29
34
|
status: ErrorStatusCode;
|
|
30
35
|
message?: string;
|
|
31
36
|
headers?: Record<string, string>;
|
|
37
|
+
data?: unknown;
|
|
38
|
+
};
|
|
39
|
+
type DenyOpts = {
|
|
40
|
+
headers?: Record<string, string>;
|
|
41
|
+
data?: unknown;
|
|
32
42
|
};
|
|
33
|
-
export declare function deny(status: ErrorStatusCode, message?: string): DenyOutcome;
|
|
43
|
+
export declare function deny(status: ErrorStatusCode, message?: string, opts?: DenyOpts): DenyOutcome;
|
|
34
44
|
export declare function deny(spec: DenyInput): DenyOutcome;
|
|
35
45
|
export declare function isOutcome(value: unknown): value is Outcome;
|
|
36
46
|
export declare function isRedirect(value: unknown): value is RedirectOutcome;
|
|
37
47
|
export declare function isDeny(value: unknown): value is DenyOutcome;
|
|
38
48
|
export declare function isRender(value: unknown): value is RenderOutcome;
|
|
49
|
+
export declare function timeoutOutcome(timeoutMs: number): TimeoutOutcome;
|
|
50
|
+
export declare function isTimeout(value: unknown): value is TimeoutOutcome;
|
package/dist/iso/outcomes.js
CHANGED
|
@@ -14,7 +14,7 @@ export function redirect(input) {
|
|
|
14
14
|
headers: input.headers,
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
|
-
export function deny(a, b) {
|
|
17
|
+
export function deny(a, b, c) {
|
|
18
18
|
// `JSON.stringify` drops `undefined` properties, so a deny outcome with no
|
|
19
19
|
// message would arrive at the client without a `message` field and the
|
|
20
20
|
// client decoders would fall back to a generic "Loader/Action failed with
|
|
@@ -28,13 +28,15 @@ export function deny(a, b) {
|
|
|
28
28
|
status: a.status,
|
|
29
29
|
message: a.message ?? `Request denied (${a.status})`,
|
|
30
30
|
headers: a.headers,
|
|
31
|
+
...(a.data !== undefined ? { data: a.data } : {}),
|
|
31
32
|
};
|
|
32
33
|
}
|
|
33
34
|
return {
|
|
34
35
|
__outcome: 'deny',
|
|
35
36
|
status: a,
|
|
36
37
|
message: b ?? `Request denied (${a})`,
|
|
37
|
-
headers:
|
|
38
|
+
headers: c?.headers,
|
|
39
|
+
...(c?.data !== undefined ? { data: c.data } : {}),
|
|
38
40
|
};
|
|
39
41
|
}
|
|
40
42
|
export function isOutcome(value) {
|
|
@@ -43,7 +45,10 @@ export function isOutcome(value) {
|
|
|
43
45
|
if (!('__outcome' in value))
|
|
44
46
|
return false;
|
|
45
47
|
const tag = value.__outcome;
|
|
46
|
-
return tag === 'redirect' ||
|
|
48
|
+
return (tag === 'redirect' ||
|
|
49
|
+
tag === 'deny' ||
|
|
50
|
+
tag === 'render' ||
|
|
51
|
+
tag === 'timeout');
|
|
47
52
|
}
|
|
48
53
|
export function isRedirect(value) {
|
|
49
54
|
return isOutcome(value) && value.__outcome === 'redirect';
|
|
@@ -54,3 +59,9 @@ export function isDeny(value) {
|
|
|
54
59
|
export function isRender(value) {
|
|
55
60
|
return isOutcome(value) && value.__outcome === 'render';
|
|
56
61
|
}
|
|
62
|
+
export function timeoutOutcome(timeoutMs) {
|
|
63
|
+
return { __outcome: 'timeout', timeoutMs };
|
|
64
|
+
}
|
|
65
|
+
export function isTimeout(value) {
|
|
66
|
+
return isOutcome(value) && value.__outcome === 'timeout';
|
|
67
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ComponentChildren, VNode } from 'preact';
|
|
2
|
+
export interface PersistProps {
|
|
3
|
+
id: string;
|
|
4
|
+
viewTransitionName?: string;
|
|
5
|
+
children?: ComponentChildren;
|
|
6
|
+
}
|
|
7
|
+
export declare function Persist(props: PersistProps): VNode;
|
|
8
|
+
export declare namespace Persist {
|
|
9
|
+
var displayName: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function PersistHost(): VNode;
|
|
12
|
+
export declare namespace PersistHost {
|
|
13
|
+
var displayName: string;
|
|
14
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jsx as _jsx } from "preact/jsx-runtime";
|
|
2
|
+
import { Fragment, h } from 'preact';
|
|
3
|
+
import { useLayoutEffect, useReducer } from 'preact/hooks';
|
|
4
|
+
import { __persistRegistryWrite, __persistRegistryRead, __persistRegistrySubscribe, } from './internal/persist-registry.js';
|
|
5
|
+
import { useViewTransitionName } from './view-transition-name.js';
|
|
6
|
+
import { isBrowser } from './is-browser.js';
|
|
7
|
+
export function Persist(props) {
|
|
8
|
+
const browser = isBrowser();
|
|
9
|
+
// Hook is called unconditionally. The effect short-circuits on the server,
|
|
10
|
+
// so SSR's render output (children inline) remains the only side effect.
|
|
11
|
+
// No deps array: runs after every render so children/viewTransitionName
|
|
12
|
+
// updates flow through without stale captures.
|
|
13
|
+
useLayoutEffect(() => {
|
|
14
|
+
if (!browser)
|
|
15
|
+
return;
|
|
16
|
+
const entry = {
|
|
17
|
+
children: props.children,
|
|
18
|
+
viewTransitionName: props.viewTransitionName,
|
|
19
|
+
};
|
|
20
|
+
__persistRegistryWrite(props.id, entry);
|
|
21
|
+
// Intentionally no cleanup: Persist does NOT clear the registry on unmount.
|
|
22
|
+
// Keeping the last-known children lets PersistHost continue to render
|
|
23
|
+
// across route changes where Persist temporarily disappears.
|
|
24
|
+
});
|
|
25
|
+
// SSR renders children inline so first paint matches steady state;
|
|
26
|
+
// the client renders nothing inline because PersistHost owns the DOM.
|
|
27
|
+
return browser ? h(Fragment, null) : h(Fragment, null, props.children);
|
|
28
|
+
}
|
|
29
|
+
Persist.displayName = 'Persist';
|
|
30
|
+
function PersistSlot(props) {
|
|
31
|
+
const ref = useViewTransitionName(props.entry.viewTransitionName);
|
|
32
|
+
return (_jsx("div", { "data-hp-persist-slot": props.id, ref: ref, children: props.entry.children }));
|
|
33
|
+
}
|
|
34
|
+
PersistSlot.displayName = 'PersistSlot';
|
|
35
|
+
export function PersistHost() {
|
|
36
|
+
// useReducer instead of useState: guarantees a re-render on each dispatch
|
|
37
|
+
// even if an intermediate render has already drained the "new" state.
|
|
38
|
+
// This matters for the ordering race: Persist's useLayoutEffect may run
|
|
39
|
+
// either before or after PersistHost's. Using useReducer ensures the
|
|
40
|
+
// forced tick after subscribe always queues a fresh render regardless of
|
|
41
|
+
// React/Preact batching.
|
|
42
|
+
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
|
43
|
+
// useLayoutEffect (not useEffect) so the subscription is in place before
|
|
44
|
+
// sibling effects run. The immediate forceUpdate after subscribe re-reads
|
|
45
|
+
// the registry to catch any Persist sibling whose useLayoutEffect already
|
|
46
|
+
// wrote between PersistHost's render and this subscribe call (sibling order
|
|
47
|
+
// is render order, but effect order can differ per host).
|
|
48
|
+
useLayoutEffect(() => {
|
|
49
|
+
const unsub = __persistRegistrySubscribe(() => forceUpdate(undefined));
|
|
50
|
+
forceUpdate(undefined);
|
|
51
|
+
return unsub;
|
|
52
|
+
}, []);
|
|
53
|
+
const map = __persistRegistryRead();
|
|
54
|
+
return (_jsx(Fragment, { children: Array.from(map.entries()).map(([id, entry]) => (_jsx(PersistSlot, { id: id, entry: entry }, id))) }));
|
|
55
|
+
}
|
|
56
|
+
PersistHost.displayName = 'PersistHost';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ActionStub } from './action.js';
|
|
2
|
+
export type ActionResult<TPayload, TResult> = {
|
|
3
|
+
kind: 'success';
|
|
4
|
+
data: TResult;
|
|
5
|
+
submittedPayload: TPayload;
|
|
6
|
+
} | {
|
|
7
|
+
kind: 'deny';
|
|
8
|
+
status: number;
|
|
9
|
+
message: string;
|
|
10
|
+
data?: unknown;
|
|
11
|
+
/**
|
|
12
|
+
* The payload as parsed from the request. For form submissions, this is
|
|
13
|
+
* a `Record<string, FormDataEntryValue | FormDataEntryValue[]>` where
|
|
14
|
+
* each value is a string or File (never a parsed primitive like `number`
|
|
15
|
+
* or `boolean`). The `TPayload` typing reflects the dev-declared shape,
|
|
16
|
+
* not the runtime structural shape. Read individual fields knowing they
|
|
17
|
+
* arrive as form-data entries.
|
|
18
|
+
*/
|
|
19
|
+
submittedPayload: TPayload;
|
|
20
|
+
} | {
|
|
21
|
+
kind: 'error';
|
|
22
|
+
message: string;
|
|
23
|
+
submittedPayload: TPayload | null;
|
|
24
|
+
} | null;
|
|
25
|
+
export declare function useActionResult<TPayload = unknown, TResult = unknown>(stub?: ActionStub<TPayload, TResult, never>): ActionResult<TPayload, TResult>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useContext } from 'preact/hooks';
|
|
2
|
+
import { useSyncExternalStore } from 'preact/compat';
|
|
3
|
+
import { ActionResultContext } from './action-result-context.js';
|
|
4
|
+
import { getLastActionResult, subscribeActionResults, } from './internal/action-result-store.js';
|
|
5
|
+
import { isBrowser } from './is-browser.js';
|
|
6
|
+
export function useActionResult(stub) {
|
|
7
|
+
const ssr = useContext(ActionResultContext);
|
|
8
|
+
const client = useSyncExternalStore(subscribeActionResults, () => isBrowser() ? getLastActionResult(stub) : null);
|
|
9
|
+
// Client store wins when populated: a JS-on submit has produced a result.
|
|
10
|
+
// SSR context is the fallback for the PE deny re-render path (no JS state).
|
|
11
|
+
const source = client ?? ssr;
|
|
12
|
+
if (!source)
|
|
13
|
+
return null;
|
|
14
|
+
if (stub &&
|
|
15
|
+
(source.module !== stub.__module || source.action !== stub.__action)) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
if (source.kind === 'success') {
|
|
19
|
+
return {
|
|
20
|
+
kind: 'success',
|
|
21
|
+
data: source.data,
|
|
22
|
+
submittedPayload: source.submittedPayload,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (source.kind === 'deny') {
|
|
26
|
+
return {
|
|
27
|
+
kind: 'deny',
|
|
28
|
+
status: source.status,
|
|
29
|
+
message: source.message,
|
|
30
|
+
data: source.data,
|
|
31
|
+
submittedPayload: source.submittedPayload,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
kind: 'error',
|
|
36
|
+
message: source.message,
|
|
37
|
+
submittedPayload: source.submittedPayload,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useSyncExternalStore } from 'preact/compat';
|
|
2
|
+
import { isPending, subscribe } from './internal/form-submit-store.js';
|
|
3
|
+
import { isBrowser } from './is-browser.js';
|
|
4
|
+
// Generic over the stub's payload/result so callers can pass any
|
|
5
|
+
// `ActionStub<TPayload, TResult, never>` without contravariant-position
|
|
6
|
+
// assignment errors. The hook only reads `__module` and `__action`.
|
|
7
|
+
export function useFormStatus(stub) {
|
|
8
|
+
// preact/compat (10.29) ships only the 2-arg signature of useSyncExternalStore.
|
|
9
|
+
// The SSR "always idle" behavior that React 18's getServerSnapshot would
|
|
10
|
+
// provide is achieved via the isBrowser() guard inside getSnapshot.
|
|
11
|
+
const pending = useSyncExternalStore(subscribe, () => isBrowser() ? isPending(stub) : false);
|
|
12
|
+
return { pending };
|
|
13
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ViewTransitionEvent } from './internal/view-transition-event.js';
|
|
2
|
+
export type ViewTransitionPhaseCallback = (event: ViewTransitionEvent) => void | Promise<void>;
|
|
3
|
+
export interface ViewTransitionLifecycle {
|
|
4
|
+
onBeforeTransition?: ViewTransitionPhaseCallback;
|
|
5
|
+
onBeforeSwap?: ViewTransitionPhaseCallback;
|
|
6
|
+
onAfterSwap?: ViewTransitionPhaseCallback;
|
|
7
|
+
onAfterTransition?: ViewTransitionPhaseCallback;
|
|
8
|
+
}
|
|
9
|
+
export declare function useViewTransitionLifecycle(lifecycle: ViewTransitionLifecycle): void;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'preact/hooks';
|
|
2
|
+
import { __subscribePhase } from './internal/route-change.js';
|
|
3
|
+
export function useViewTransitionLifecycle(lifecycle) {
|
|
4
|
+
const ref = useRef(lifecycle);
|
|
5
|
+
ref.current = lifecycle;
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const unsubs = [
|
|
8
|
+
__subscribePhase('beforeTransition', (e) => ref.current.onBeforeTransition?.(e)),
|
|
9
|
+
__subscribePhase('beforeSwap', (e) => ref.current.onBeforeSwap?.(e)),
|
|
10
|
+
__subscribePhase('afterSwap', (e) => ref.current.onAfterSwap?.(e)),
|
|
11
|
+
__subscribePhase('afterTransition', (e) => ref.current.onAfterTransition?.(e)),
|
|
12
|
+
];
|
|
13
|
+
return () => {
|
|
14
|
+
for (const u of unsubs)
|
|
15
|
+
u();
|
|
16
|
+
};
|
|
17
|
+
}, []);
|
|
18
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ComponentChildren, VNode } from 'preact';
|
|
2
|
+
import { type UseRenderRender } from './internal/use-render.js';
|
|
3
|
+
export declare function useViewTransitionName(name: string | null | undefined): (node: Element | null) => void;
|
|
4
|
+
export declare function useViewTransitionClass(cls: string | string[] | null | undefined): (node: Element | null) => void;
|
|
5
|
+
export interface ViewTransitionNameProps {
|
|
6
|
+
name: string | null | undefined;
|
|
7
|
+
groupClass?: string | string[];
|
|
8
|
+
render?: UseRenderRender;
|
|
9
|
+
children?: ComponentChildren;
|
|
10
|
+
}
|
|
11
|
+
export declare function ViewTransitionName(props: ViewTransitionNameProps): VNode;
|
|
12
|
+
export interface ViewTransitionGroupProps {
|
|
13
|
+
class: string | string[];
|
|
14
|
+
render?: UseRenderRender;
|
|
15
|
+
children?: ComponentChildren;
|
|
16
|
+
}
|
|
17
|
+
export declare function ViewTransitionGroup(props: ViewTransitionGroupProps): VNode;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useCallback, useLayoutEffect, useRef } from 'preact/hooks';
|
|
2
|
+
import { mergeRefs } from './internal/merge-refs.js';
|
|
3
|
+
import { useRender } from './internal/use-render.js';
|
|
4
|
+
function isStyledElement(node) {
|
|
5
|
+
return (node !== null && (node instanceof HTMLElement || node instanceof SVGElement));
|
|
6
|
+
}
|
|
7
|
+
function applyCssProp(node, property, value) {
|
|
8
|
+
if (!node)
|
|
9
|
+
return;
|
|
10
|
+
if (value == null || value === '') {
|
|
11
|
+
node.style.removeProperty(property);
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
node.style.setProperty(property, value);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function useViewTransitionName(name) {
|
|
18
|
+
const nodeRef = useRef(null);
|
|
19
|
+
const nameRef = useRef(name);
|
|
20
|
+
nameRef.current = name;
|
|
21
|
+
// Sync when name changes on a node we already hold.
|
|
22
|
+
useLayoutEffect(() => {
|
|
23
|
+
applyCssProp(nodeRef.current, 'view-transition-name', name);
|
|
24
|
+
}, [name]);
|
|
25
|
+
// Stable ref callback: applies on attach, clears the previous node on swap.
|
|
26
|
+
return useCallback((node) => {
|
|
27
|
+
if (nodeRef.current && nodeRef.current !== node) {
|
|
28
|
+
nodeRef.current.style.removeProperty('view-transition-name');
|
|
29
|
+
}
|
|
30
|
+
if (isStyledElement(node)) {
|
|
31
|
+
nodeRef.current = node;
|
|
32
|
+
applyCssProp(node, 'view-transition-name', nameRef.current);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
nodeRef.current = null;
|
|
36
|
+
}
|
|
37
|
+
}, []);
|
|
38
|
+
}
|
|
39
|
+
export function useViewTransitionClass(cls) {
|
|
40
|
+
const value = cls == null ? null : Array.isArray(cls) ? cls.join(' ') : cls;
|
|
41
|
+
const nodeRef = useRef(null);
|
|
42
|
+
const valueRef = useRef(value);
|
|
43
|
+
valueRef.current = value;
|
|
44
|
+
useLayoutEffect(() => {
|
|
45
|
+
applyCssProp(nodeRef.current, 'view-transition-class', value);
|
|
46
|
+
}, [value]);
|
|
47
|
+
return useCallback((node) => {
|
|
48
|
+
if (nodeRef.current && nodeRef.current !== node) {
|
|
49
|
+
nodeRef.current.style.removeProperty('view-transition-class');
|
|
50
|
+
}
|
|
51
|
+
if (isStyledElement(node)) {
|
|
52
|
+
nodeRef.current = node;
|
|
53
|
+
applyCssProp(node, 'view-transition-class', valueRef.current);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
nodeRef.current = null;
|
|
57
|
+
}
|
|
58
|
+
}, []);
|
|
59
|
+
}
|
|
60
|
+
export function ViewTransitionName(props) {
|
|
61
|
+
const nameRef = useViewTransitionName(props.name);
|
|
62
|
+
const classRef = useViewTransitionClass(props.groupClass);
|
|
63
|
+
const ref = mergeRefs(nameRef, classRef);
|
|
64
|
+
return useRender({
|
|
65
|
+
render: props.render,
|
|
66
|
+
defaultTag: 'div',
|
|
67
|
+
props: { ref },
|
|
68
|
+
children: props.children,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
export function ViewTransitionGroup(props) {
|
|
72
|
+
const classRef = useViewTransitionClass(props.class);
|
|
73
|
+
return useRender({
|
|
74
|
+
render: props.render,
|
|
75
|
+
defaultTag: 'div',
|
|
76
|
+
props: { ref: classRef },
|
|
77
|
+
children: props.children,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { NavDirection } from './internal/view-transition-event.js';
|
|
2
|
+
export interface ViewTransitionTypesNav {
|
|
3
|
+
to: string;
|
|
4
|
+
from: string | undefined;
|
|
5
|
+
direction: NavDirection;
|
|
6
|
+
}
|
|
7
|
+
export type ViewTransitionTypesInput = string | string[] | ((nav: ViewTransitionTypesNav) => string | string[] | null | undefined);
|
|
8
|
+
export declare function useViewTransitionTypes(input: ViewTransitionTypesInput): void;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'preact/hooks';
|
|
2
|
+
import { __subscribePhase } from './internal/route-change.js';
|
|
3
|
+
export function useViewTransitionTypes(input) {
|
|
4
|
+
const ref = useRef(input);
|
|
5
|
+
ref.current = input;
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
return __subscribePhase('beforeTransition', (event) => {
|
|
8
|
+
const v = ref.current;
|
|
9
|
+
const resolved = typeof v === 'function'
|
|
10
|
+
? v({ to: event.to, from: event.from, direction: event.direction })
|
|
11
|
+
: v;
|
|
12
|
+
if (resolved == null)
|
|
13
|
+
return;
|
|
14
|
+
if (typeof resolved === 'string')
|
|
15
|
+
event.types.push(resolved);
|
|
16
|
+
else
|
|
17
|
+
for (const t of resolved)
|
|
18
|
+
event.types.push(t);
|
|
19
|
+
});
|
|
20
|
+
}, []);
|
|
21
|
+
}
|
|
@@ -42,6 +42,13 @@ export interface ActionsHandlerOptions {
|
|
|
42
42
|
* awaits the result either way. Default returns an empty array.
|
|
43
43
|
*/
|
|
44
44
|
resolvePageUse?: (moduleKey: string) => ReadonlyArray<unknown> | Promise<ReadonlyArray<unknown>>;
|
|
45
|
+
/**
|
|
46
|
+
* Default action timeout in milliseconds applied when an action does not
|
|
47
|
+
* declare its own `timeoutMs`. Defaults to 30000 (30 seconds). Pass
|
|
48
|
+
* `false` to disable the default (only action-level `timeoutMs` enforces
|
|
49
|
+
* a deadline).
|
|
50
|
+
*/
|
|
51
|
+
defaultTimeoutMs?: number | false;
|
|
45
52
|
}
|
|
46
53
|
export declare function actionsHandler(glob: LazyGlob | EagerGlob, opts?: ActionsHandlerOptions): MiddlewareHandler;
|
|
47
54
|
export {};
|