hono-preact 0.1.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 +47 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/internal.d.ts +1 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +1 -0
- package/dist/iso/action.d.ts +78 -0
- package/dist/iso/action.js +189 -0
- package/dist/iso/cache.d.ts +17 -0
- package/dist/iso/cache.js +122 -0
- package/dist/iso/client-script.d.ts +2 -0
- package/dist/iso/client-script.js +13 -0
- package/dist/iso/define-loader.d.ts +47 -0
- package/dist/iso/define-loader.js +118 -0
- package/dist/iso/define-page.d.ts +10 -0
- package/dist/iso/define-page.js +7 -0
- package/dist/iso/define-routes.d.ts +34 -0
- package/dist/iso/define-routes.js +251 -0
- package/dist/iso/form.d.ts +7 -0
- package/dist/iso/form.js +40 -0
- package/dist/iso/guard.d.ts +33 -0
- package/dist/iso/guard.js +32 -0
- package/dist/iso/head.d.ts +6 -0
- package/dist/iso/head.js +4 -0
- package/dist/iso/index.d.ts +30 -0
- package/dist/iso/index.js +29 -0
- package/dist/iso/internal/cache-key.d.ts +2 -0
- package/dist/iso/internal/cache-key.js +8 -0
- package/dist/iso/internal/contexts.d.ts +12 -0
- package/dist/iso/internal/contexts.js +7 -0
- package/dist/iso/internal/envelope.d.ts +8 -0
- package/dist/iso/internal/envelope.js +21 -0
- package/dist/iso/internal/guard-noop.d.ts +7 -0
- package/dist/iso/internal/guard-noop.js +6 -0
- package/dist/iso/internal/guards.d.ts +14 -0
- package/dist/iso/internal/guards.js +54 -0
- package/dist/iso/internal/loader-fetch.d.ts +20 -0
- package/dist/iso/internal/loader-fetch.js +123 -0
- package/dist/iso/internal/loader-runner.d.ts +15 -0
- package/dist/iso/internal/loader-runner.js +59 -0
- package/dist/iso/internal/loader-stub.d.ts +8 -0
- package/dist/iso/internal/loader-stub.js +19 -0
- package/dist/iso/internal/loader.d.ts +13 -0
- package/dist/iso/internal/loader.js +31 -0
- package/dist/iso/internal/optimistic-overlay.d.ts +10 -0
- package/dist/iso/internal/optimistic-overlay.js +11 -0
- package/dist/iso/internal/preload.d.ts +15 -0
- package/dist/iso/internal/preload.js +36 -0
- package/dist/iso/internal/route-boundary.d.ts +25 -0
- package/dist/iso/internal/route-boundary.js +24 -0
- package/dist/iso/internal/route-change.d.ts +4 -0
- package/dist/iso/internal/route-change.js +18 -0
- package/dist/iso/internal/route-locations.d.ts +11 -0
- package/dist/iso/internal/route-locations.js +15 -0
- package/dist/iso/internal/sse-decoder.d.ts +5 -0
- package/dist/iso/internal/sse-decoder.js +43 -0
- package/dist/iso/internal/stream-registry.d.ts +60 -0
- package/dist/iso/internal/stream-registry.js +98 -0
- package/dist/iso/internal/streaming-ssr.d.ts +17 -0
- package/dist/iso/internal/streaming-ssr.js +32 -0
- package/dist/iso/internal/use-loader-runner.d.ts +12 -0
- package/dist/iso/internal/use-loader-runner.js +185 -0
- package/dist/iso/internal/wrap-promise.d.ts +4 -0
- package/dist/iso/internal/wrap-promise.js +24 -0
- package/dist/iso/internal.d.ts +19 -0
- package/dist/iso/internal.js +49 -0
- package/dist/iso/is-browser.d.ts +4 -0
- package/dist/iso/is-browser.js +6 -0
- package/dist/iso/optimistic-action.d.ts +19 -0
- package/dist/iso/optimistic-action.js +25 -0
- package/dist/iso/optimistic.d.ts +5 -0
- package/dist/iso/optimistic.js +31 -0
- package/dist/iso/page.d.ts +16 -0
- package/dist/iso/page.js +10 -0
- package/dist/iso/prefetch.d.ts +22 -0
- package/dist/iso/prefetch.js +78 -0
- package/dist/iso/reload-context.d.ts +6 -0
- package/dist/iso/reload-context.js +9 -0
- package/dist/iso/route-change.d.ts +2 -0
- package/dist/iso/route-change.js +10 -0
- package/dist/iso/view-transitions.d.ts +1 -0
- package/dist/iso/view-transitions.js +6 -0
- package/dist/server/actions-handler.d.ts +33 -0
- package/dist/server/actions-handler.js +159 -0
- package/dist/server/context.d.ts +6 -0
- package/dist/server/context.js +6 -0
- package/dist/server/index.d.ts +5 -0
- package/dist/server/index.js +5 -0
- package/dist/server/loaders-handler.d.ts +30 -0
- package/dist/server/loaders-handler.js +117 -0
- package/dist/server/middleware/location.d.ts +1 -0
- package/dist/server/middleware/location.js +10 -0
- package/dist/server/render.d.ts +5 -0
- package/dist/server/render.js +203 -0
- package/dist/server/route-server-modules.d.ts +12 -0
- package/dist/server/route-server-modules.js +13 -0
- package/dist/server/sse.d.ts +22 -0
- package/dist/server/sse.js +83 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +1 -0
- package/dist/vite/client-entry.d.ts +10 -0
- package/dist/vite/client-entry.js +47 -0
- package/dist/vite/client-shim.d.ts +12 -0
- package/dist/vite/client-shim.js +62 -0
- package/dist/vite/guard-strip.d.ts +2 -0
- package/dist/vite/guard-strip.js +96 -0
- package/dist/vite/hono-preact.d.ts +12 -0
- package/dist/vite/hono-preact.js +111 -0
- package/dist/vite/index.d.ts +7 -0
- package/dist/vite/index.js +7 -0
- package/dist/vite/module-key-plugin.d.ts +12 -0
- package/dist/vite/module-key-plugin.js +114 -0
- package/dist/vite/module-key.d.ts +11 -0
- package/dist/vite/module-key.js +20 -0
- package/dist/vite/parser-options.d.ts +16 -0
- package/dist/vite/parser-options.js +22 -0
- package/dist/vite/server-entry.d.ts +26 -0
- package/dist/vite/server-entry.js +201 -0
- package/dist/vite/server-loader-validation.d.ts +2 -0
- package/dist/vite/server-loader-validation.js +73 -0
- package/dist/vite/server-loaders-parser.d.ts +22 -0
- package/dist/vite/server-loaders-parser.js +64 -0
- package/dist/vite/server-only.d.ts +3 -0
- package/dist/vite/server-only.js +244 -0
- package/dist/vite.d.ts +1 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +1 -0
- package/package.json +78 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type ServerLoaderStream = {
|
|
2
|
+
loaderId: string;
|
|
3
|
+
gen: AsyncGenerator<unknown, unknown, unknown>;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Register a streaming loader's remaining generator iterations for the
|
|
7
|
+
* current request. Called from LoaderHost's server-side branch after the
|
|
8
|
+
* first chunk is already in the rendered HTML.
|
|
9
|
+
*/
|
|
10
|
+
export declare function registerServerStreamingLoader(loaderId: string, gen: AsyncGenerator<unknown, unknown, unknown>): void;
|
|
11
|
+
/**
|
|
12
|
+
* Take ownership of the registered streaming loaders for the current
|
|
13
|
+
* request. After this returns, the registry is cleared. Called from
|
|
14
|
+
* `renderPage` after prerender resolves, while still inside
|
|
15
|
+
* `runRequestScope`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function takeServerStreamingLoaders(): ServerLoaderStream[];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { getRequestStore } from '../cache.js';
|
|
2
|
+
const REGISTRY_KEY = Symbol.for('@hono-preact/streaming-ssr-registry');
|
|
3
|
+
/**
|
|
4
|
+
* Register a streaming loader's remaining generator iterations for the
|
|
5
|
+
* current request. Called from LoaderHost's server-side branch after the
|
|
6
|
+
* first chunk is already in the rendered HTML.
|
|
7
|
+
*/
|
|
8
|
+
export function registerServerStreamingLoader(loaderId, gen) {
|
|
9
|
+
const store = getRequestStore();
|
|
10
|
+
if (!store)
|
|
11
|
+
return; // outside any request scope (e.g., client)
|
|
12
|
+
let list = store.get(REGISTRY_KEY);
|
|
13
|
+
if (!list) {
|
|
14
|
+
list = [];
|
|
15
|
+
store.set(REGISTRY_KEY, list);
|
|
16
|
+
}
|
|
17
|
+
list.push({ loaderId, gen });
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Take ownership of the registered streaming loaders for the current
|
|
21
|
+
* request. After this returns, the registry is cleared. Called from
|
|
22
|
+
* `renderPage` after prerender resolves, while still inside
|
|
23
|
+
* `runRequestScope`.
|
|
24
|
+
*/
|
|
25
|
+
export function takeServerStreamingLoaders() {
|
|
26
|
+
const store = getRequestStore();
|
|
27
|
+
if (!store)
|
|
28
|
+
return [];
|
|
29
|
+
const list = store.get(REGISTRY_KEY) ?? [];
|
|
30
|
+
store.set(REGISTRY_KEY, []);
|
|
31
|
+
return list;
|
|
32
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RouteHook } from 'preact-iso';
|
|
2
|
+
import type { LoaderRef } from '../define-loader.js';
|
|
3
|
+
export type LoaderRunnerState<T> = {
|
|
4
|
+
reader: {
|
|
5
|
+
read: () => T;
|
|
6
|
+
};
|
|
7
|
+
overrideData: T | undefined;
|
|
8
|
+
error: Error | null;
|
|
9
|
+
reload: () => void;
|
|
10
|
+
reloading: boolean;
|
|
11
|
+
};
|
|
12
|
+
export declare function useLoaderRunner<T>(loaderRef: LoaderRef<T>, location: RouteHook, id: string): LoaderRunnerState<T>;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
|
2
|
+
import { isBrowser } from '../is-browser.js';
|
|
3
|
+
import { getPreloadedData, deletePreloadedData } from './preload.js';
|
|
4
|
+
import wrapPromise from './wrap-promise.js';
|
|
5
|
+
import { subscribeToLoaderStream } from './stream-registry.js';
|
|
6
|
+
import { runLoader } from './loader-runner.js';
|
|
7
|
+
import { serializeLocationForCache } from './cache-key.js';
|
|
8
|
+
export function useLoaderRunner(loaderRef, location, id) {
|
|
9
|
+
const [reloading, setReloading] = useState(false);
|
|
10
|
+
const [overrideData, setOverrideData] = useState(undefined);
|
|
11
|
+
const [loadError, setLoadError] = useState(null);
|
|
12
|
+
const locationRef = useRef(location);
|
|
13
|
+
locationRef.current = location;
|
|
14
|
+
const abortRef = useRef(null);
|
|
15
|
+
function newAbortSignal() {
|
|
16
|
+
// Abort the previous controller (cancels any in-flight loader),
|
|
17
|
+
// then allocate a fresh one whose signal is passed to the new fn call.
|
|
18
|
+
if (abortRef.current)
|
|
19
|
+
abortRef.current.abort();
|
|
20
|
+
abortRef.current = new AbortController();
|
|
21
|
+
return abortRef.current.signal;
|
|
22
|
+
}
|
|
23
|
+
useEffect(() => () => {
|
|
24
|
+
if (abortRef.current)
|
|
25
|
+
abortRef.current.abort();
|
|
26
|
+
}, []);
|
|
27
|
+
// Cleanup of the SSR preload attribute is deferred to after commit so
|
|
28
|
+
// we never mutate the DOM during the render pass (Preact reconciliation
|
|
29
|
+
// doesn't formally support that, and re-renders could observe a phantom
|
|
30
|
+
// half-cleared element). The render path sets `preloadConsumedRef` when
|
|
31
|
+
// it reads the payload; this effect clears the attribute exactly once,
|
|
32
|
+
// on the first commit that consumed it.
|
|
33
|
+
const preloadConsumedRef = useRef(false);
|
|
34
|
+
const preloadClearedRef = useRef(false);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (preloadConsumedRef.current && !preloadClearedRef.current) {
|
|
37
|
+
preloadClearedRef.current = true;
|
|
38
|
+
deletePreloadedData(id);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
// True while either the initial Suspense fetch or an explicit reload is in
|
|
42
|
+
// flight. Tracked via a ref so reload() can read it without recapturing on
|
|
43
|
+
// every state change, and so the wrapPromise branch below can flip it
|
|
44
|
+
// during render without scheduling an extra setState.
|
|
45
|
+
const inFlightRef = useRef(false);
|
|
46
|
+
const queuedReloadRef = useRef(false);
|
|
47
|
+
const runReloadRef = useRef(() => { });
|
|
48
|
+
const runReload = useCallback(() => {
|
|
49
|
+
inFlightRef.current = true;
|
|
50
|
+
setReloading(true);
|
|
51
|
+
setLoadError(null);
|
|
52
|
+
const promise = runLoader(loaderRef, locationRef.current, id, newAbortSignal(), {
|
|
53
|
+
onChunk: (value) => {
|
|
54
|
+
setOverrideData(value);
|
|
55
|
+
if (isBrowser()) {
|
|
56
|
+
loaderRef.cache.set(value, serializeLocationForCache(locationRef.current, loaderRef.params));
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
onError: (err) => setLoadError(err),
|
|
60
|
+
onEnd: () => {
|
|
61
|
+
/* nothing to do */
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
promise
|
|
65
|
+
.then((result) => {
|
|
66
|
+
if (isBrowser())
|
|
67
|
+
loaderRef.cache.set(result, serializeLocationForCache(locationRef.current, loaderRef.params));
|
|
68
|
+
setOverrideData(result);
|
|
69
|
+
setReloading(false);
|
|
70
|
+
inFlightRef.current = false;
|
|
71
|
+
if (queuedReloadRef.current) {
|
|
72
|
+
queuedReloadRef.current = false;
|
|
73
|
+
runReloadRef.current();
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
.catch((err) => {
|
|
77
|
+
setLoadError(err instanceof Error ? err : new Error(String(err)));
|
|
78
|
+
setReloading(false);
|
|
79
|
+
inFlightRef.current = false;
|
|
80
|
+
queuedReloadRef.current = false;
|
|
81
|
+
});
|
|
82
|
+
}, [loaderRef]);
|
|
83
|
+
runReloadRef.current = runReload;
|
|
84
|
+
const reload = useCallback(() => {
|
|
85
|
+
if (inFlightRef.current) {
|
|
86
|
+
queuedReloadRef.current = true;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
runReloadRef.current();
|
|
90
|
+
}, []);
|
|
91
|
+
// Stable reader: only rebuilt when location or loader identity changes.
|
|
92
|
+
// Without this, every re-render (e.g. from setReloading) would call
|
|
93
|
+
// wrapPromise(...) again, fire a duplicate XHR, and throw a fresh promise
|
|
94
|
+
// into Suspense — unmounting the children and wiping any optimistic UI
|
|
95
|
+
// state below.
|
|
96
|
+
//
|
|
97
|
+
// The location key includes path AND searchParams so /movies?genre=action →
|
|
98
|
+
// /movies?genre=drama refetches even though preact-iso doesn't remount on
|
|
99
|
+
// querystring changes.
|
|
100
|
+
const readerRef = useRef(null);
|
|
101
|
+
const locKey = serializeLocationForCache(location, loaderRef.params);
|
|
102
|
+
const prevLocKey = useRef(locKey);
|
|
103
|
+
const prevLoaderId = useRef(loaderRef.__id);
|
|
104
|
+
const locationChanged = prevLocKey.current !== locKey;
|
|
105
|
+
const loaderChanged = prevLoaderId.current !== loaderRef.__id;
|
|
106
|
+
if (readerRef.current === null || locationChanged || loaderChanged) {
|
|
107
|
+
prevLocKey.current = locKey;
|
|
108
|
+
prevLoaderId.current = loaderRef.__id;
|
|
109
|
+
if (locationChanged || loaderChanged)
|
|
110
|
+
setOverrideData(undefined);
|
|
111
|
+
const preloaded = getPreloadedData(id);
|
|
112
|
+
const isFirstRender = readerRef.current === null;
|
|
113
|
+
if (preloaded !== null) {
|
|
114
|
+
// Record that we consumed the SSR preload payload so the useEffect
|
|
115
|
+
// below can clear the DOM attribute AFTER commit instead of mutating
|
|
116
|
+
// the DOM during render.
|
|
117
|
+
preloadConsumedRef.current = true;
|
|
118
|
+
loaderRef.cache.set(preloaded, locKey);
|
|
119
|
+
readerRef.current = { read: () => preloaded };
|
|
120
|
+
if (isBrowser()) {
|
|
121
|
+
const unsub = subscribeToLoaderStream(id, {
|
|
122
|
+
push: (value) => {
|
|
123
|
+
setOverrideData(value);
|
|
124
|
+
loaderRef.cache.set(value, locKey);
|
|
125
|
+
},
|
|
126
|
+
end: () => {
|
|
127
|
+
/* nothing to do */
|
|
128
|
+
},
|
|
129
|
+
error: (err) => setLoadError(err),
|
|
130
|
+
});
|
|
131
|
+
// Unsubscribe on unmount: attach to the abortRef signal.
|
|
132
|
+
if (abortRef.current) {
|
|
133
|
+
abortRef.current.signal.addEventListener('abort', unsub);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
abortRef.current = new AbortController();
|
|
137
|
+
abortRef.current.signal.addEventListener('abort', unsub);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else if (isBrowser() && isFirstRender && loaderRef.cache.has(locKey)) {
|
|
142
|
+
const cached = loaderRef.cache.get(locKey);
|
|
143
|
+
readerRef.current = { read: () => cached };
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
inFlightRef.current = true;
|
|
147
|
+
const settle = () => {
|
|
148
|
+
inFlightRef.current = false;
|
|
149
|
+
if (queuedReloadRef.current) {
|
|
150
|
+
queuedReloadRef.current = false;
|
|
151
|
+
runReloadRef.current();
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
const fetchPromise = runLoader(loaderRef, location, id, newAbortSignal(), {
|
|
155
|
+
onChunk: (value) => {
|
|
156
|
+
setOverrideData(value);
|
|
157
|
+
if (isBrowser())
|
|
158
|
+
loaderRef.cache.set(value, locKey);
|
|
159
|
+
},
|
|
160
|
+
onError: (err) => setLoadError(err),
|
|
161
|
+
onEnd: () => {
|
|
162
|
+
/* nothing to do */
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
readerRef.current = wrapPromise(fetchPromise
|
|
166
|
+
.then((r) => {
|
|
167
|
+
if (isBrowser())
|
|
168
|
+
loaderRef.cache.set(r, locKey);
|
|
169
|
+
settle();
|
|
170
|
+
return r;
|
|
171
|
+
})
|
|
172
|
+
.catch((err) => {
|
|
173
|
+
settle();
|
|
174
|
+
throw err;
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
reader: readerRef.current,
|
|
180
|
+
overrideData,
|
|
181
|
+
error: loadError,
|
|
182
|
+
reload,
|
|
183
|
+
reloading,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function wrapPromise(promise) {
|
|
2
|
+
let status = 'pending';
|
|
3
|
+
let result;
|
|
4
|
+
let error;
|
|
5
|
+
const suspender = promise.then((res) => {
|
|
6
|
+
status = 'success';
|
|
7
|
+
result = res;
|
|
8
|
+
}, (err) => {
|
|
9
|
+
status = 'error';
|
|
10
|
+
error = err;
|
|
11
|
+
});
|
|
12
|
+
const read = () => {
|
|
13
|
+
switch (status) {
|
|
14
|
+
case 'pending':
|
|
15
|
+
throw suspender;
|
|
16
|
+
case 'error':
|
|
17
|
+
throw error;
|
|
18
|
+
default:
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
return { read };
|
|
23
|
+
}
|
|
24
|
+
export default wrapPromise;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { Loader } from './internal/loader.js';
|
|
2
|
+
export { Envelope } from './internal/envelope.js';
|
|
3
|
+
export { RouteBoundary } from './internal/route-boundary.js';
|
|
4
|
+
export { Guards, GuardGate, useGuardResult } from './internal/guards.js';
|
|
5
|
+
export { OptimisticOverlay } from './internal/optimistic-overlay.js';
|
|
6
|
+
export { LoaderIdContext, LoaderDataContext, GuardResultContext, } from './internal/contexts.js';
|
|
7
|
+
export { ReloadContext } from './reload-context.js';
|
|
8
|
+
export { RouteLocationsContext, RouteLocationsProvider, } from './internal/route-locations.js';
|
|
9
|
+
export { getPreloadedData, deletePreloadedData } from './internal/preload.js';
|
|
10
|
+
export { runRequestScope, getRequestStore, captureRequestScope, } from './cache.js';
|
|
11
|
+
export { default as wrapPromise } from './internal/wrap-promise.js';
|
|
12
|
+
export { runServerGuards, runClientGuards } from './guard.js';
|
|
13
|
+
export { HonoRequestContext } from './internal/contexts.js';
|
|
14
|
+
export { __dispatchRouteChange, __subscribeRouteChange, } from './internal/route-change.js';
|
|
15
|
+
export { installStreamRegistry, subscribeToLoaderStream, } from './internal/stream-registry.js';
|
|
16
|
+
export { registerServerStreamingLoader, takeServerStreamingLoaders, } from './internal/streaming-ssr.js';
|
|
17
|
+
export type { ServerLoaderStream } from './internal/streaming-ssr.js';
|
|
18
|
+
export { __$createLoaderStub_hpiso } from './internal/loader-stub.js';
|
|
19
|
+
export { __$guardNoop_hpiso } from './internal/guard-noop.js';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// @hono-preact/iso/internal -- escape hatch for advanced consumers.
|
|
2
|
+
//
|
|
3
|
+
// These primitives compose the default <Page> pipeline. They're kept
|
|
4
|
+
// behind a subpath so the front door (@hono-preact/iso) stays small.
|
|
5
|
+
// Use them when definePage bindings or <Page> props don't express
|
|
6
|
+
// what you need (e.g. distinct fallbacks for guards vs. loader, custom
|
|
7
|
+
// pipeline ordering, advanced SSR work).
|
|
8
|
+
//
|
|
9
|
+
// STABILITY: this subpath is intentionally less stable than the package's
|
|
10
|
+
// main surface. Symbols may be renamed, retyped, or removed in any
|
|
11
|
+
// non-major release. Pin a specific framework version if your code reaches
|
|
12
|
+
// in here.
|
|
13
|
+
//
|
|
14
|
+
// The file is split into two sections:
|
|
15
|
+
//
|
|
16
|
+
// 1. ADVANCED USER ESCAPE HATCHES — primitives users may reasonably
|
|
17
|
+
// compose by hand when `definePage` bindings aren't enough. Reach for
|
|
18
|
+
// these knowingly; expect to read the source.
|
|
19
|
+
//
|
|
20
|
+
// 2. FRAMEWORK-EMITTED (DO NOT IMPORT FROM USER CODE) — symbols the
|
|
21
|
+
// framework's own Vite plugins emit `import` statements for, then
|
|
22
|
+
// reference in code they generate. They're exported here only because
|
|
23
|
+
// the emitted code needs a real import target. Importing them
|
|
24
|
+
// yourself bypasses everything the public API does and your code
|
|
25
|
+
// will break at a non-major upgrade.
|
|
26
|
+
// ─── Section 1: advanced user escape hatches ─────────────────────────────
|
|
27
|
+
export { Loader } from './internal/loader.js';
|
|
28
|
+
export { Envelope } from './internal/envelope.js';
|
|
29
|
+
export { RouteBoundary } from './internal/route-boundary.js';
|
|
30
|
+
export { Guards, GuardGate, useGuardResult } from './internal/guards.js';
|
|
31
|
+
export { OptimisticOverlay } from './internal/optimistic-overlay.js';
|
|
32
|
+
export { LoaderIdContext, LoaderDataContext, GuardResultContext, } from './internal/contexts.js';
|
|
33
|
+
export { ReloadContext } from './reload-context.js';
|
|
34
|
+
export { RouteLocationsContext, RouteLocationsProvider, } from './internal/route-locations.js';
|
|
35
|
+
export { getPreloadedData, deletePreloadedData } from './internal/preload.js';
|
|
36
|
+
export { runRequestScope, getRequestStore, captureRequestScope, } from './cache.js';
|
|
37
|
+
export { default as wrapPromise } from './internal/wrap-promise.js';
|
|
38
|
+
export { runServerGuards, runClientGuards } from './guard.js';
|
|
39
|
+
export { HonoRequestContext } from './internal/contexts.js';
|
|
40
|
+
export { __dispatchRouteChange, __subscribeRouteChange, } from './internal/route-change.js';
|
|
41
|
+
export { installStreamRegistry, subscribeToLoaderStream, } from './internal/stream-registry.js';
|
|
42
|
+
export { registerServerStreamingLoader, takeServerStreamingLoaders, } from './internal/streaming-ssr.js';
|
|
43
|
+
// ─── Section 2: framework-emitted (DO NOT IMPORT FROM USER CODE) ─────────
|
|
44
|
+
// The `__$..._hpiso` naming makes the convention visible at every grep:
|
|
45
|
+
// these symbols are referenced by code the framework's Vite plugins emit
|
|
46
|
+
// (serverOnlyPlugin's loader stubs, guardStripPlugin's no-op replacement).
|
|
47
|
+
// User code that imports them couples to plugin internals.
|
|
48
|
+
export { __$createLoaderStub_hpiso } from './internal/loader-stub.js';
|
|
49
|
+
export { __$guardNoop_hpiso } from './internal/guard-noop.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type UseActionOptions, type UseActionResult, type ActionStub } from './action.js';
|
|
2
|
+
import type { LoaderRef } from './define-loader.js';
|
|
3
|
+
export type UseOptimisticActionOptions<TPayload, TResult, TBase, TChunk = never> = Omit<UseActionOptions<TPayload, TResult, TChunk>, 'invalidate' | 'onMutate' | 'onError' | 'onSuccess'> & {
|
|
4
|
+
base: TBase;
|
|
5
|
+
apply: (current: TBase, payload: TPayload) => TBase;
|
|
6
|
+
invalidate?: 'auto' | ReadonlyArray<LoaderRef<unknown>>;
|
|
7
|
+
onSuccess?: (data: TResult) => void;
|
|
8
|
+
onError?: (err: Error) => void;
|
|
9
|
+
};
|
|
10
|
+
export type UseOptimisticActionResult<TPayload, TResult, TBase> = UseActionResult<TPayload, TResult> & {
|
|
11
|
+
value: TBase;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Like `useAction`, but with an optimistic-update wrapper. `TChunk` defaults
|
|
15
|
+
* to `never` so existing non-streaming call sites are unaffected. Pass the
|
|
16
|
+
* stub's chunk type explicitly (or infer it via the stub's third generic)
|
|
17
|
+
* when the action is streaming and you need a typed `onChunk` callback.
|
|
18
|
+
*/
|
|
19
|
+
export declare function useOptimisticAction<TPayload, TResult, TBase, TChunk = never>(stub: ActionStub<TPayload, TResult, TChunk>, options: UseOptimisticActionOptions<TPayload, TResult, TBase, TChunk>): UseOptimisticActionResult<TPayload, TResult, TBase>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useAction, } from './action.js';
|
|
2
|
+
import { useOptimistic } from './optimistic.js';
|
|
3
|
+
/**
|
|
4
|
+
* Like `useAction`, but with an optimistic-update wrapper. `TChunk` defaults
|
|
5
|
+
* to `never` so existing non-streaming call sites are unaffected. Pass the
|
|
6
|
+
* stub's chunk type explicitly (or infer it via the stub's third generic)
|
|
7
|
+
* when the action is streaming and you need a typed `onChunk` callback.
|
|
8
|
+
*/
|
|
9
|
+
export function useOptimisticAction(stub, options) {
|
|
10
|
+
const { base, apply, onSuccess, onError, ...actionOpts } = options;
|
|
11
|
+
const [value, addOptimistic] = useOptimistic(base, apply);
|
|
12
|
+
const action = useAction(stub, {
|
|
13
|
+
...actionOpts,
|
|
14
|
+
onMutate: (payload) => addOptimistic(payload),
|
|
15
|
+
onSuccess: (data, handle) => {
|
|
16
|
+
handle.settle();
|
|
17
|
+
onSuccess?.(data);
|
|
18
|
+
},
|
|
19
|
+
onError: (err, handle) => {
|
|
20
|
+
handle.revert();
|
|
21
|
+
onError?.(err);
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
return { ...action, value };
|
|
25
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useCallback, useReducer, useRef } from 'preact/hooks';
|
|
2
|
+
export function useOptimistic(base, reducer) {
|
|
3
|
+
const queueRef = useRef([]);
|
|
4
|
+
const lastBaseRef = useRef(base);
|
|
5
|
+
const idRef = useRef(0);
|
|
6
|
+
const [, forceRender] = useReducer((c) => c + 1, 0);
|
|
7
|
+
if (!Object.is(lastBaseRef.current, base)) {
|
|
8
|
+
queueRef.current = queueRef.current.filter((e) => e.status !== 'ready');
|
|
9
|
+
lastBaseRef.current = base;
|
|
10
|
+
}
|
|
11
|
+
const value = queueRef.current.reduce((acc, e) => reducer(acc, e.payload), base);
|
|
12
|
+
const addOptimistic = useCallback((payload) => {
|
|
13
|
+
const id = ++idRef.current;
|
|
14
|
+
queueRef.current = [...queueRef.current, { id, payload, status: 'active' }];
|
|
15
|
+
forceRender();
|
|
16
|
+
return {
|
|
17
|
+
settle: () => {
|
|
18
|
+
const entry = queueRef.current.find((e) => e.id === id);
|
|
19
|
+
if (entry && entry.status === 'active') {
|
|
20
|
+
entry.status = 'ready';
|
|
21
|
+
forceRender();
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
revert: () => {
|
|
25
|
+
queueRef.current = queueRef.current.filter((e) => e.id !== id);
|
|
26
|
+
forceRender();
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}, []);
|
|
30
|
+
return [value, addOptimistic];
|
|
31
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ComponentChildren, ComponentType, JSX } from 'preact';
|
|
2
|
+
import type { RouteHook } from 'preact-iso';
|
|
3
|
+
import type { GuardFn } from './guard.js';
|
|
4
|
+
export type WrapperProps = {
|
|
5
|
+
id: string;
|
|
6
|
+
'data-loader': string;
|
|
7
|
+
children: ComponentChildren;
|
|
8
|
+
};
|
|
9
|
+
export type PageProps = {
|
|
10
|
+
location: RouteHook;
|
|
11
|
+
guards?: GuardFn[];
|
|
12
|
+
errorFallback?: JSX.Element | ((error: Error, reset: () => void) => JSX.Element);
|
|
13
|
+
Wrapper?: ComponentType<WrapperProps>;
|
|
14
|
+
children: ComponentChildren;
|
|
15
|
+
};
|
|
16
|
+
export declare function Page({ location, guards, errorFallback, Wrapper, children, }: PageProps): JSX.Element;
|
package/dist/iso/page.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx } from "preact/jsx-runtime";
|
|
2
|
+
import { useId } from 'preact/hooks';
|
|
3
|
+
import { Guards } from './internal/guards.js';
|
|
4
|
+
import { RouteBoundary } from './internal/route-boundary.js';
|
|
5
|
+
const DefaultWrapper = (props) => (_jsx("section", { ...props }));
|
|
6
|
+
export function Page({ location, guards, errorFallback, Wrapper, children, }) {
|
|
7
|
+
const id = useId();
|
|
8
|
+
const W = Wrapper ?? DefaultWrapper;
|
|
9
|
+
return (_jsx(RouteBoundary, { errorFallback: errorFallback, children: _jsx(Guards, { guards: guards, location: location, children: _jsx(W, { id: id, "data-loader": "null", children: children }) }) }));
|
|
10
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { RouteHook } from 'preact-iso';
|
|
2
|
+
import type { LoaderCache } from './cache.js';
|
|
3
|
+
import type { LoaderRef } from './define-loader.js';
|
|
4
|
+
export interface PrefetchOptions<T> {
|
|
5
|
+
url?: string;
|
|
6
|
+
route?: string;
|
|
7
|
+
location?: RouteHook;
|
|
8
|
+
cache?: LoaderCache<T>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Prefetch a loader's data and write the result into its cache.
|
|
12
|
+
*
|
|
13
|
+
* In the browser this delegates to the same RPC path that `loader.View()` uses
|
|
14
|
+
* at runtime: a POST to `/__loaders`. In SSR or test environments it falls
|
|
15
|
+
* back to invoking the loader function directly. This matches `runLoader`'s
|
|
16
|
+
* dispatch and means consumers do not need to know which side they are on.
|
|
17
|
+
*
|
|
18
|
+
* For non-streaming loaders the returned promise resolves with the final
|
|
19
|
+
* value. For streaming loaders it resolves with the first chunk; subsequent
|
|
20
|
+
* chunks update the cache as they arrive.
|
|
21
|
+
*/
|
|
22
|
+
export declare function prefetch<T>(ref: LoaderRef<T>, opts?: PrefetchOptions<T>): Promise<T>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { exec } from 'preact-iso';
|
|
2
|
+
import { isBrowser } from './is-browser.js';
|
|
3
|
+
import { runLoader } from './internal/loader-runner.js';
|
|
4
|
+
import { serializeLocationForCache } from './internal/cache-key.js';
|
|
5
|
+
function buildLocation(opts) {
|
|
6
|
+
if (!opts.url) {
|
|
7
|
+
return { path: '', searchParams: {}, pathParams: {} };
|
|
8
|
+
}
|
|
9
|
+
const parsed = new URL(opts.url, 'http://_');
|
|
10
|
+
let path = parsed.pathname;
|
|
11
|
+
if (path.length > 1 && path.endsWith('/'))
|
|
12
|
+
path = path.slice(0, -1);
|
|
13
|
+
const searchParams = {};
|
|
14
|
+
parsed.searchParams.forEach((value, key) => {
|
|
15
|
+
searchParams[key] = value;
|
|
16
|
+
});
|
|
17
|
+
let pathParams = {};
|
|
18
|
+
if (opts.route) {
|
|
19
|
+
const matched = exec(path, opts.route, {
|
|
20
|
+
path,
|
|
21
|
+
query: searchParams,
|
|
22
|
+
params: {},
|
|
23
|
+
});
|
|
24
|
+
if (matched && matched.pathParams) {
|
|
25
|
+
pathParams = matched.pathParams;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return { path, searchParams, pathParams };
|
|
29
|
+
}
|
|
30
|
+
let prefetchSeq = 0;
|
|
31
|
+
/**
|
|
32
|
+
* Prefetch a loader's data and write the result into its cache.
|
|
33
|
+
*
|
|
34
|
+
* In the browser this delegates to the same RPC path that `loader.View()` uses
|
|
35
|
+
* at runtime: a POST to `/__loaders`. In SSR or test environments it falls
|
|
36
|
+
* back to invoking the loader function directly. This matches `runLoader`'s
|
|
37
|
+
* dispatch and means consumers do not need to know which side they are on.
|
|
38
|
+
*
|
|
39
|
+
* For non-streaming loaders the returned promise resolves with the final
|
|
40
|
+
* value. For streaming loaders it resolves with the first chunk; subsequent
|
|
41
|
+
* chunks update the cache as they arrive.
|
|
42
|
+
*/
|
|
43
|
+
export async function prefetch(ref, opts = {}) {
|
|
44
|
+
const location = opts.location ?? buildLocation({ url: opts.url, route: opts.route });
|
|
45
|
+
const cache = opts.cache ?? ref.cache;
|
|
46
|
+
// Compute the cache key the loader runtime would use, so a warm cache for
|
|
47
|
+
// THIS specific location short-circuits the fetch. Without this check, a
|
|
48
|
+
// hover handler that fires repeatedly (mouse over → off → back over the
|
|
49
|
+
// same link, an intersection observer re-entering visibility, an idle
|
|
50
|
+
// prefetch scheduled twice) would issue a redundant request every time.
|
|
51
|
+
// The docs already promise no-op-on-cache-hit behavior; this makes the
|
|
52
|
+
// code match.
|
|
53
|
+
const locKey = serializeLocationForCache(location, ref.params);
|
|
54
|
+
if (cache?.has(locKey)) {
|
|
55
|
+
const cached = cache.get(locKey);
|
|
56
|
+
if (cached !== null)
|
|
57
|
+
return cached;
|
|
58
|
+
}
|
|
59
|
+
const id = `prefetch:${++prefetchSeq}`;
|
|
60
|
+
const signal = new AbortController().signal;
|
|
61
|
+
const result = await runLoader(ref, location, id, signal, {
|
|
62
|
+
onChunk: (v) => {
|
|
63
|
+
cache?.set(v, locKey);
|
|
64
|
+
},
|
|
65
|
+
onError: () => {
|
|
66
|
+
// Errors after the first chunk are swallowed: prefetch is best-effort
|
|
67
|
+
// and the page itself will see the same failure when it actually loads.
|
|
68
|
+
},
|
|
69
|
+
onEnd: () => { },
|
|
70
|
+
});
|
|
71
|
+
// Key the final write on locKey so two prefetches for different URLs
|
|
72
|
+
// (hovering /movies/41 then /movies/42) don't collide on the legacy
|
|
73
|
+
// single-slot "locKey: null matches any" fallback the cache supports for
|
|
74
|
+
// back-compat.
|
|
75
|
+
if (isBrowser())
|
|
76
|
+
cache?.set(result, locKey);
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createContext } from 'preact';
|
|
2
|
+
import { useContext } from 'preact/hooks';
|
|
3
|
+
export const ReloadContext = createContext(undefined);
|
|
4
|
+
export function useReload() {
|
|
5
|
+
const ctx = useContext(ReloadContext);
|
|
6
|
+
if (!ctx)
|
|
7
|
+
throw new Error('useReload() must be called inside a `loader.View` render function or inside a `loader.Boundary`.');
|
|
8
|
+
return ctx;
|
|
9
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'preact/hooks';
|
|
2
|
+
import { __subscribeRouteChange } from './internal/route-change.js';
|
|
3
|
+
export function useRouteChange(handler) {
|
|
4
|
+
// Keep the latest handler in a ref so rerenders don't churn the subscription.
|
|
5
|
+
const ref = useRef(handler);
|
|
6
|
+
ref.current = handler;
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
return __subscribeRouteChange((to, from) => ref.current(to, from));
|
|
9
|
+
}, []);
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function ViewTransitions(): null;
|