hono-preact 0.3.0 → 0.5.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/define-routes.d.ts +0 -1
- package/dist/iso/define-routes.js +10 -3
- package/dist/iso/form.js +6 -0
- package/dist/iso/index.d.ts +6 -0
- package/dist/iso/index.js +9 -0
- package/dist/iso/internal/history-shim.d.ts +20 -0
- package/dist/iso/internal/history-shim.js +110 -0
- package/dist/iso/internal/merge-refs.d.ts +4 -0
- package/dist/iso/internal/merge-refs.js +14 -0
- package/dist/iso/internal/page-middleware-host.js +148 -45
- package/dist/iso/internal/persist-registry.d.ts +10 -0
- package/dist/iso/internal/persist-registry.js +24 -0
- package/dist/iso/internal/route-change.d.ts +25 -2
- package/dist/iso/internal/route-change.js +414 -13
- 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 +6 -1
- package/dist/iso/internal.js +6 -1
- package/dist/iso/persist.d.ts +14 -0
- package/dist/iso/persist.js +56 -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 +19 -0
- package/dist/iso/view-transition-types.js +36 -0
- package/dist/server/render.js +95 -52
- package/dist/vite/client-entry.js +16 -9
- package/package.json +2 -2
|
@@ -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,19 @@
|
|
|
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
|
+
/**
|
|
9
|
+
* Register a global, route-aware view-transition type rule. The resolver runs on
|
|
10
|
+
* every navigation with `{ to, from, direction }` and returns the type(s) to add
|
|
11
|
+
* to that navigation's transition (a static string/array adds the same type(s) to
|
|
12
|
+
* every navigation). Returns an unsubscribe.
|
|
13
|
+
*
|
|
14
|
+
* Unlike {@link useViewTransitionTypes}, this is not tied to a mounted component,
|
|
15
|
+
* so it covers entering AND leaving a section (a layout hook is not subscribed yet
|
|
16
|
+
* on enter and is already torn down on leave). No-op on the server (no document).
|
|
17
|
+
*/
|
|
18
|
+
export declare function subscribeViewTransitionTypes(input: ViewTransitionTypesInput): () => void;
|
|
19
|
+
export declare function useViewTransitionTypes(input: ViewTransitionTypesInput): void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'preact/hooks';
|
|
2
|
+
import { __subscribePhase } from './internal/route-change.js';
|
|
3
|
+
/**
|
|
4
|
+
* Register a global, route-aware view-transition type rule. The resolver runs on
|
|
5
|
+
* every navigation with `{ to, from, direction }` and returns the type(s) to add
|
|
6
|
+
* to that navigation's transition (a static string/array adds the same type(s) to
|
|
7
|
+
* every navigation). Returns an unsubscribe.
|
|
8
|
+
*
|
|
9
|
+
* Unlike {@link useViewTransitionTypes}, this is not tied to a mounted component,
|
|
10
|
+
* so it covers entering AND leaving a section (a layout hook is not subscribed yet
|
|
11
|
+
* on enter and is already torn down on leave). No-op on the server (no document).
|
|
12
|
+
*/
|
|
13
|
+
export function subscribeViewTransitionTypes(input) {
|
|
14
|
+
if (typeof document === 'undefined')
|
|
15
|
+
return () => { };
|
|
16
|
+
return __subscribePhase('beforeTransition', (event) => {
|
|
17
|
+
const resolved = typeof input === 'function'
|
|
18
|
+
? input({ to: event.to, from: event.from, direction: event.direction })
|
|
19
|
+
: input;
|
|
20
|
+
if (resolved == null)
|
|
21
|
+
return;
|
|
22
|
+
if (typeof resolved === 'string')
|
|
23
|
+
event.types.push(resolved);
|
|
24
|
+
else
|
|
25
|
+
for (const t of resolved)
|
|
26
|
+
event.types.push(t);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export function useViewTransitionTypes(input) {
|
|
30
|
+
const ref = useRef(input);
|
|
31
|
+
ref.current = input;
|
|
32
|
+
useEffect(() => subscribeViewTransitionTypes((nav) => {
|
|
33
|
+
const v = ref.current;
|
|
34
|
+
return typeof v === 'function' ? v(nav) : v;
|
|
35
|
+
}), []);
|
|
36
|
+
}
|
package/dist/server/render.js
CHANGED
|
@@ -221,65 +221,105 @@ export async function renderPage(c, node, options) {
|
|
|
221
221
|
// throw and, for the per-loader catch, get logged as a synthetic error
|
|
222
222
|
// chunk that nobody can read anyway).
|
|
223
223
|
let aborted = false;
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
224
|
+
// Multi-producer backpressure via TransformStream. Each loader pump writes
|
|
225
|
+
// to a shared `writer`, awaiting `writer.ready` before each write so that
|
|
226
|
+
// the per-loader iteration is paced by the consumer's read rate (not by
|
|
227
|
+
// however fast the generator yields). Without this, a tight-loop streaming
|
|
228
|
+
// loader would buffer chunks into the ReadableStream queue unbounded; see
|
|
229
|
+
// render-stream.test.tsx "pauses production when the HTML consumer is
|
|
230
|
+
// slow (backpressure)".
|
|
231
|
+
const { writable, readable: responseStream } = new TransformStream();
|
|
232
|
+
const writer = writable.getWriter();
|
|
233
|
+
// When the consumer cancels the readable side (e.g. Hono drops the response,
|
|
234
|
+
// or the runtime tears down the request), the writable side transitions to
|
|
235
|
+
// an errored state and `writer.closed` rejects. Propagate to the loader
|
|
236
|
+
// generators symmetrically with the request-signal abort path below. The
|
|
237
|
+
// `aborted` guard makes the self-triggered case (`writer.abort()` in our
|
|
238
|
+
// own finally) a no-op.
|
|
239
|
+
writer.closed.catch(() => {
|
|
240
|
+
if (aborted)
|
|
241
|
+
return;
|
|
242
|
+
aborted = true;
|
|
243
|
+
for (const { gen } of streamingLoaders) {
|
|
244
|
+
gen.return(undefined).catch(() => {
|
|
245
|
+
/* swallow */
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
// Re-enter the captured request scope so generator continuations and
|
|
250
|
+
// anything they touch (e.g. `getRequestHonoContext`, per-request loader
|
|
251
|
+
// caches) see the same per-request store the initial prerender saw.
|
|
252
|
+
void bindRequestScope(async () => {
|
|
253
|
+
// Yield one microtask before doing anything else. `renderPage` is still
|
|
254
|
+
// on the synchronous frame that constructs this response (TransformStream
|
|
255
|
+
// is created, then `c.body(...)` runs and commits the headers). Resuming
|
|
256
|
+
// a generator can call `setCookie(ctx.c, ...)`, which mutates Hono's
|
|
257
|
+
// prepared headers; deferring the pump guarantees the response is built
|
|
258
|
+
// first, so post-first-yield header writes are consistently excluded
|
|
259
|
+
// rather than racing construction. Cookies must be set before the
|
|
260
|
+
// loader's first yield to reach the streamed response.
|
|
261
|
+
await Promise.resolve();
|
|
262
|
+
try {
|
|
263
|
+
if (aborted)
|
|
264
|
+
return;
|
|
265
|
+
await writer.ready;
|
|
266
|
+
if (aborted)
|
|
267
|
+
return;
|
|
268
|
+
await writer.write(encoder.encode(`<!doctype html>${beforeBody}\n${bootstrap}\n`));
|
|
269
|
+
// Drive each pending generator in parallel; emit script tags per chunk.
|
|
270
|
+
await Promise.all(streamingLoaders.map(async ({ loaderId, gen }) => {
|
|
230
271
|
try {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
// excluded rather than racing construction. Cookies must be set
|
|
242
|
-
// before the loader's first yield to reach the streamed response.
|
|
243
|
-
await Promise.resolve();
|
|
244
|
-
// Drive each pending generator in parallel; emit script tags per chunk.
|
|
245
|
-
await Promise.all(streamingLoaders.map(async ({ loaderId, gen }) => {
|
|
246
|
-
try {
|
|
247
|
-
while (!aborted) {
|
|
248
|
-
const step = await gen.next();
|
|
249
|
-
if (aborted)
|
|
250
|
-
return;
|
|
251
|
-
if (step.done) {
|
|
252
|
-
controller.enqueue(encoder.encode(`<script>window.__HP_STREAM__.end(${jsonForScript(loaderId)});document.currentScript.remove()</script>\n`));
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
controller.enqueue(encoder.encode(`<script>window.__HP_STREAM__.push(${jsonForScript(loaderId)},${jsonForScript(step.value)});document.currentScript.remove()</script>\n`));
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
catch (err) {
|
|
259
|
-
if (aborted)
|
|
260
|
-
return;
|
|
261
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
262
|
-
const name = err instanceof Error ? err.name : 'Error';
|
|
263
|
-
controller.enqueue(encoder.encode(`<script>window.__HP_STREAM__.error(${jsonForScript(loaderId)},${jsonForScript({ message, name })});document.currentScript.remove()</script>\n`));
|
|
272
|
+
while (!aborted) {
|
|
273
|
+
const step = await gen.next();
|
|
274
|
+
if (aborted)
|
|
275
|
+
return;
|
|
276
|
+
await writer.ready;
|
|
277
|
+
if (aborted)
|
|
278
|
+
return;
|
|
279
|
+
if (step.done) {
|
|
280
|
+
await writer.write(encoder.encode(`<script>window.__HP_STREAM__.end(${jsonForScript(loaderId)});document.currentScript.remove()</script>\n`));
|
|
281
|
+
return;
|
|
264
282
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
controller.enqueue(encoder.encode(afterBody));
|
|
283
|
+
await writer.write(encoder.encode(`<script>window.__HP_STREAM__.push(${jsonForScript(loaderId)},${jsonForScript(step.value)});document.currentScript.remove()</script>\n`));
|
|
284
|
+
}
|
|
268
285
|
}
|
|
269
|
-
|
|
270
|
-
if (
|
|
271
|
-
|
|
286
|
+
catch (err) {
|
|
287
|
+
if (aborted)
|
|
288
|
+
return;
|
|
289
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
290
|
+
const name = err instanceof Error ? err.name : 'Error';
|
|
291
|
+
try {
|
|
292
|
+
await writer.ready;
|
|
293
|
+
if (aborted)
|
|
294
|
+
return;
|
|
295
|
+
await writer.write(encoder.encode(`<script>window.__HP_STREAM__.error(${jsonForScript(loaderId)},${jsonForScript({ message, name })});document.currentScript.remove()</script>\n`));
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
/* swallow: writable side closed/errored */
|
|
299
|
+
}
|
|
272
300
|
}
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
301
|
+
}));
|
|
302
|
+
if (!aborted) {
|
|
303
|
+
await writer.ready;
|
|
304
|
+
if (!aborted)
|
|
305
|
+
await writer.write(encoder.encode(afterBody));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
/* swallow: writable side closed/errored mid-pump */
|
|
310
|
+
}
|
|
311
|
+
finally {
|
|
312
|
+
if (aborted) {
|
|
313
|
+
writer.abort().catch(() => {
|
|
279
314
|
/* swallow */
|
|
280
315
|
});
|
|
281
316
|
}
|
|
282
|
-
|
|
317
|
+
else {
|
|
318
|
+
writer.close().catch(() => {
|
|
319
|
+
/* swallow */
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
283
323
|
});
|
|
284
324
|
requestSignal.addEventListener('abort', () => {
|
|
285
325
|
aborted = true;
|
|
@@ -288,6 +328,9 @@ export async function renderPage(c, node, options) {
|
|
|
288
328
|
/* swallow */
|
|
289
329
|
});
|
|
290
330
|
}
|
|
331
|
+
writer.abort().catch(() => {
|
|
332
|
+
/* swallow */
|
|
333
|
+
});
|
|
291
334
|
});
|
|
292
335
|
// Route through `c.body()` rather than `new Response(...)` so Hono merges
|
|
293
336
|
// its prepared headers into the streamed response. A streaming loader's
|
|
@@ -2,24 +2,31 @@ import * as path from 'node:path';
|
|
|
2
2
|
export const VIRTUAL_CLIENT_ENTRY_ID = 'virtual:hono-preact/client';
|
|
3
3
|
const RESOLVED_ID = '\0' + VIRTUAL_CLIENT_ENTRY_ID;
|
|
4
4
|
export function generateClientEntrySource(opts) {
|
|
5
|
-
return (`import { h, hydrate } from 'preact';\n` +
|
|
5
|
+
return (`import { h, hydrate, render as renderPreact } from 'preact';\n` +
|
|
6
6
|
`import { LocationProvider } from 'preact-iso';\n` +
|
|
7
|
-
`import { Routes } from 'hono-preact';\n` +
|
|
8
|
-
`import {
|
|
7
|
+
`import { Routes, PersistHost } from 'hono-preact';\n` +
|
|
8
|
+
`import { installNavTransitionScheduler, installStreamRegistry, installHistoryShim } from 'hono-preact/internal';\n` +
|
|
9
9
|
`import routes from '${opts.routesAbsPath}';\n` +
|
|
10
10
|
`\n` +
|
|
11
|
+
`installHistoryShim();\n` +
|
|
12
|
+
`installNavTransitionScheduler();\n` +
|
|
11
13
|
`installStreamRegistry();\n` +
|
|
12
14
|
`\n` +
|
|
13
|
-
`let
|
|
14
|
-
`
|
|
15
|
-
`
|
|
16
|
-
`
|
|
17
|
-
`
|
|
15
|
+
`let persistHost = document.getElementById('__hp_persist_root');\n` +
|
|
16
|
+
`if (!persistHost) {\n` +
|
|
17
|
+
` persistHost = document.createElement('div');\n` +
|
|
18
|
+
` persistHost.id = '__hp_persist_root';\n` +
|
|
19
|
+
` document.body.appendChild(persistHost);\n` +
|
|
18
20
|
`}\n` +
|
|
21
|
+
`renderPreact(h(PersistHost, null), persistHost);\n` +
|
|
19
22
|
`\n` +
|
|
23
|
+
// View transitions are driven by installNavTransitionScheduler() above: it
|
|
24
|
+
// overrides Preact's render scheduler so a navigation's re-render runs inside
|
|
25
|
+
// document.startViewTransition (capturing the outgoing route as the old
|
|
26
|
+
// snapshot before the new one swaps in). No per-navigation wiring needed.
|
|
20
27
|
`hydrate(\n` +
|
|
21
28
|
` h(LocationProvider, null,\n` +
|
|
22
|
-
` h(Routes, { routes
|
|
29
|
+
` h(Routes, { routes })\n` +
|
|
23
30
|
` ),\n` +
|
|
24
31
|
` document.getElementById('app')\n` +
|
|
25
32
|
`);\n`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hono-preact",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Hono on the edge, Preact in the browser, manifest driven routes, typed RPC, streaming everywhere.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"hono",
|
|
@@ -95,8 +95,8 @@
|
|
|
95
95
|
},
|
|
96
96
|
"devDependencies": {
|
|
97
97
|
"typescript": "*",
|
|
98
|
-
"@hono-preact/server": "0.1.0",
|
|
99
98
|
"@hono-preact/iso": "0.1.0",
|
|
99
|
+
"@hono-preact/server": "0.1.0",
|
|
100
100
|
"@hono-preact/vite": "0.1.0"
|
|
101
101
|
},
|
|
102
102
|
"scripts": {
|