hono-preact 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +2 -1
  2. package/dist/adapter-cloudflare.d.ts +1 -0
  3. package/dist/adapter-cloudflare.d.ts.map +1 -0
  4. package/dist/adapter-cloudflare.js +2 -0
  5. package/dist/adapter-node.d.ts +1 -0
  6. package/dist/adapter-node.d.ts.map +1 -0
  7. package/dist/adapter-node.js +2 -0
  8. package/dist/internal.d.ts +1 -1
  9. package/dist/internal.js +1 -1
  10. package/dist/iso/action-result-context.d.ts +22 -0
  11. package/dist/iso/action-result-context.js +2 -0
  12. package/dist/iso/action.d.ts +60 -25
  13. package/dist/iso/action.js +210 -58
  14. package/dist/iso/cache.d.ts +9 -0
  15. package/dist/iso/cache.js +26 -0
  16. package/dist/iso/define-app.d.ts +14 -0
  17. package/dist/iso/define-app.js +3 -0
  18. package/dist/iso/define-loader.d.ts +31 -0
  19. package/dist/iso/define-loader.js +30 -16
  20. package/dist/iso/define-middleware.d.ts +43 -0
  21. package/dist/iso/define-middleware.js +6 -0
  22. package/dist/iso/define-page.d.ts +7 -2
  23. package/dist/iso/define-page.js +1 -1
  24. package/dist/iso/define-routes.d.ts +24 -1
  25. package/dist/iso/define-routes.js +34 -0
  26. package/dist/iso/define-stream-observer.d.ts +20 -0
  27. package/dist/iso/define-stream-observer.js +3 -0
  28. package/dist/iso/form.d.ts +13 -4
  29. package/dist/iso/form.js +115 -33
  30. package/dist/iso/index.d.ts +15 -7
  31. package/dist/iso/index.js +9 -4
  32. package/dist/iso/internal/action-envelope.d.ts +37 -0
  33. package/dist/iso/internal/action-envelope.js +47 -0
  34. package/dist/iso/internal/action-result-store.d.ts +28 -0
  35. package/dist/iso/internal/action-result-store.js +35 -0
  36. package/dist/iso/internal/contexts.d.ts +0 -2
  37. package/dist/iso/internal/contexts.js +0 -1
  38. package/dist/iso/internal/envelope.js +1 -2
  39. package/dist/iso/internal/form-submit-store.d.ts +9 -0
  40. package/dist/iso/internal/form-submit-store.js +32 -0
  41. package/dist/iso/internal/loader-fetch.js +102 -41
  42. package/dist/iso/internal/loader-runner.js +105 -8
  43. package/dist/iso/internal/loader.d.ts +3 -3
  44. package/dist/iso/internal/middleware-runner.d.ts +22 -0
  45. package/dist/iso/internal/middleware-runner.js +79 -0
  46. package/dist/iso/internal/page-middleware-host.d.ts +13 -0
  47. package/dist/iso/internal/page-middleware-host.js +119 -0
  48. package/dist/iso/internal/route-boundary.d.ts +5 -4
  49. package/dist/iso/internal/route-boundary.js +16 -0
  50. package/dist/iso/internal/safe-redirect.d.ts +7 -0
  51. package/dist/iso/internal/safe-redirect.js +27 -0
  52. package/dist/iso/internal/sse-decoder.d.ts +1 -1
  53. package/dist/iso/internal/sse-decoder.js +40 -26
  54. package/dist/iso/internal/stream-observer-runner.d.ts +13 -0
  55. package/dist/iso/internal/stream-observer-runner.js +48 -0
  56. package/dist/iso/internal/use-partitioner.d.ts +9 -0
  57. package/dist/iso/internal/use-partitioner.js +11 -0
  58. package/dist/iso/internal/use-types.d.ts +7 -0
  59. package/dist/iso/internal/use-types.js +1 -0
  60. package/dist/iso/internal.d.ts +12 -5
  61. package/dist/iso/internal.js +16 -7
  62. package/dist/iso/optimistic-action.d.ts +10 -1
  63. package/dist/iso/optimistic-action.js +11 -3
  64. package/dist/iso/optimistic.d.ts +10 -1
  65. package/dist/iso/optimistic.js +45 -5
  66. package/dist/iso/outcomes.d.ts +50 -0
  67. package/dist/iso/outcomes.js +67 -0
  68. package/dist/iso/page-only.d.ts +5 -0
  69. package/dist/iso/page-only.js +20 -0
  70. package/dist/iso/page.d.ts +3 -3
  71. package/dist/iso/page.js +3 -3
  72. package/dist/iso/use-action-result.d.ts +25 -0
  73. package/dist/iso/use-action-result.js +39 -0
  74. package/dist/iso/use-form-status.d.ts +5 -0
  75. package/dist/iso/use-form-status.js +13 -0
  76. package/dist/page.d.ts +1 -0
  77. package/dist/page.d.ts.map +1 -0
  78. package/dist/page.js +8 -0
  79. package/dist/server/actions-handler.d.ts +27 -6
  80. package/dist/server/actions-handler.js +121 -52
  81. package/dist/server/context.js +1 -1
  82. package/dist/server/index.d.ts +3 -2
  83. package/dist/server/index.js +3 -2
  84. package/dist/server/loaders-handler.d.ts +24 -0
  85. package/dist/server/loaders-handler.js +128 -18
  86. package/dist/server/page-action-handler.d.ts +63 -0
  87. package/dist/server/page-action-handler.js +274 -0
  88. package/dist/server/page-action-resolvers.d.ts +28 -0
  89. package/dist/server/page-action-resolvers.js +147 -0
  90. package/dist/server/render.d.ts +2 -0
  91. package/dist/server/render.js +142 -33
  92. package/dist/server/route-server-modules.d.ts +48 -8
  93. package/dist/server/route-server-modules.js +190 -7
  94. package/dist/server/speculation-rules.d.ts +3 -0
  95. package/dist/server/speculation-rules.js +8 -0
  96. package/dist/server/sse.d.ts +50 -12
  97. package/dist/server/sse.js +130 -53
  98. package/dist/vite/adapter-cloudflare.d.ts +2 -0
  99. package/dist/vite/adapter-cloudflare.js +25 -0
  100. package/dist/vite/adapter-node.d.ts +2 -0
  101. package/dist/vite/adapter-node.js +49 -0
  102. package/dist/vite/adapter.d.ts +29 -0
  103. package/dist/vite/adapter.js +1 -0
  104. package/dist/vite/client-shim.js +5 -4
  105. package/dist/vite/guard-strip.js +52 -27
  106. package/dist/vite/hono-preact.d.ts +6 -6
  107. package/dist/vite/hono-preact.js +48 -77
  108. package/dist/vite/index.d.ts +2 -1
  109. package/dist/vite/index.js +1 -1
  110. package/dist/vite/node-dev-server.d.ts +4 -0
  111. package/dist/vite/node-dev-server.js +121 -0
  112. package/dist/vite/server-entry.d.ts +30 -7
  113. package/dist/vite/server-entry.js +170 -79
  114. package/dist/vite/server-exports-contract.d.ts +6 -0
  115. package/dist/vite/server-exports-contract.js +43 -0
  116. package/dist/vite/server-loader-validation.js +36 -9
  117. package/dist/vite/server-loaders-parser.d.ts +17 -1
  118. package/dist/vite/server-loaders-parser.js +41 -0
  119. package/dist/vite/server-only.js +20 -2
  120. package/package.json +33 -5
@@ -1,22 +1,60 @@
1
1
  import type { Context } from 'hono';
2
- export type SseGeneratorOptions = {
3
- /** When true, the generator's return value is emitted as `event: result`. */
2
+ import type { StreamObserver, ServerStreamCtx } from '../iso/index';
3
+ /**
4
+ * Options shared by both SSE response helpers. Encodes the lifecycle the SSE
5
+ * pump runs through:
6
+ *
7
+ * - Observer fanout: `onStart` fires before the first chunk, `onChunk` per
8
+ * value yielded by the source, `onEnd` on normal completion, `onError` on
9
+ * a thrown error, `onAbort` when the consumer cancels the response stream
10
+ * before the source finishes.
11
+ * - Timeout discrimination: when `signal.aborted` and `signal.reason` is a
12
+ * `TimeoutError` `DOMException`, the catch path emits `event: timeout`
13
+ * with `{ timeoutMs }` instead of the generic `event: error` frame.
14
+ */
15
+ export type SseResponseOptions = {
16
+ /**
17
+ * When true, the generator's return value (if defined) is emitted as
18
+ * `event: result` before the stream closes. Only meaningful for
19
+ * generator-sourced responses; ignored for `ReadableStream` sources.
20
+ */
4
21
  emitResult?: boolean;
22
+ /**
23
+ * Stream observers harvested from the loader/action's `use` array (the
24
+ * non-middleware partition). Hooks are isolated: a throwing observer
25
+ * never corrupts the stream.
26
+ */
27
+ observers?: ReadonlyArray<StreamObserver<unknown, never>>;
28
+ /** Server-stream ctx threaded to each observer hook. */
29
+ observerCtx?: ServerStreamCtx;
30
+ /**
31
+ * The handler's timeout signal (from `AbortSignal.timeout(timeoutMs)`),
32
+ * inspected in the catch path to distinguish a deadline-driven abort
33
+ * from a generic throw.
34
+ */
35
+ signal?: AbortSignal;
36
+ /** Used only with `signal`; the timeout value reported in the frame. */
37
+ timeoutMs?: number;
5
38
  };
39
+ /** Alias retained for source compatibility with earlier code. */
40
+ export type SseGeneratorOptions = SseResponseOptions;
6
41
  /**
7
42
  * Wrap an async generator as an SSE response.
8
43
  *
9
- * Each yield is JSON-encoded and written as a `data:` event.
10
- * If `emitResult` is true and the generator's return value is defined,
11
- * it is written as `event: result\ndata: <json>` before the stream closes.
12
- * If the generator throws, an `event: error\ndata: {"message","name"}` frame
13
- * is written and the stream closes cleanly (Hono's default error handler is
14
- * never invoked because we catch inside the callback).
44
+ * Each yield is JSON-encoded and written as a `data:` event. If `emitResult`
45
+ * is true and the generator's return value is defined, it is written as
46
+ * `event: result\ndata: <json>` before the stream closes. If the generator
47
+ * throws, an `event: error` or `event: timeout` frame is written and the
48
+ * stream closes cleanly. Observer lifecycle hooks (`onStart` / `onChunk` /
49
+ * `onEnd` / `onError` / `onAbort`) fire from inside the pump.
15
50
  */
16
- export declare function sseGeneratorResponse(c: Context, gen: AsyncGenerator<unknown, unknown, unknown>, options?: SseGeneratorOptions): Response;
51
+ export declare function sseGeneratorResponse(_c: Context, gen: AsyncGenerator<unknown, unknown, unknown>, options?: SseResponseOptions): Response;
17
52
  /**
18
- * Wrap a ReadableStream<T> (with T a JSON-encodable value) as an SSE response.
19
- * Each enqueued chunk is JSON-encoded and written as a `data:` event.
53
+ * Wrap a `ReadableStream<T>` (with `T` a JSON-encodable value) as an SSE
54
+ * response. Each enqueued chunk is JSON-encoded and written as a `data:`
55
+ * event. Observer lifecycle hooks fire identically to `sseGeneratorResponse`;
56
+ * `emitResult` is not meaningful here (streams have no return value) and is
57
+ * ignored.
20
58
  */
21
- export declare function sseReadableStreamResponse(c: Context, source: ReadableStream<unknown>): Response;
59
+ export declare function sseReadableStreamResponse(_c: Context, source: ReadableStream<unknown>, options?: SseResponseOptions): Response;
22
60
  export declare function isAsyncGenerator(value: unknown): value is AsyncGenerator<unknown, unknown, unknown>;
@@ -1,78 +1,155 @@
1
- import { streamSSE } from 'hono/streaming';
1
+ import { fanStart, fanChunk, fanEnd, fanError, fanAbort, } from '../iso/internal.js';
2
+ function sseEncodeTransform() {
3
+ const encoder = new TextEncoder();
4
+ return new TransformStream({
5
+ transform(frame, controller) {
6
+ const lines = [];
7
+ if (frame.event)
8
+ lines.push(`event: ${frame.event}`);
9
+ if (frame.id)
10
+ lines.push(`id: ${frame.id}`);
11
+ lines.push(`data: ${frame.data}`);
12
+ controller.enqueue(encoder.encode(lines.join('\n') + '\n\n'));
13
+ },
14
+ });
15
+ }
16
+ function isTimeoutAbort(signal) {
17
+ return Boolean(signal?.aborted &&
18
+ signal.reason instanceof DOMException &&
19
+ signal.reason.name === 'TimeoutError');
20
+ }
2
21
  function encodeErrorPayload(err) {
3
22
  const message = err instanceof Error ? err.message : String(err);
4
23
  const name = err instanceof Error ? err.name : 'Error';
5
24
  return JSON.stringify({ message, name });
6
25
  }
7
26
  /**
8
- * Wrap an async generator as an SSE response.
27
+ * Adapt a `ReadableStream<T>` as an async generator (with no return value).
28
+ * The reader is released in `finally`, which fires either when the consumer
29
+ * stops iterating or when the source is exhausted.
30
+ */
31
+ async function* iterReadable(stream) {
32
+ const reader = stream.getReader();
33
+ try {
34
+ while (true) {
35
+ const { done, value } = await reader.read();
36
+ if (done)
37
+ return;
38
+ yield value;
39
+ }
40
+ }
41
+ finally {
42
+ reader.cancel().catch(() => undefined);
43
+ }
44
+ }
45
+ /**
46
+ * The shared pump implementation. Iterates `source` (a generator that may
47
+ * return a final value), encodes each yielded value as a JSON `data:` frame,
48
+ * runs the observer lifecycle, and translates errors into `event: error` or
49
+ * `event: timeout` frames.
9
50
  *
10
- * Each yield is JSON-encoded and written as a `data:` event.
11
- * If `emitResult` is true and the generator's return value is defined,
12
- * it is written as `event: result\ndata: <json>` before the stream closes.
13
- * If the generator throws, an `event: error\ndata: {"message","name"}` frame
14
- * is written and the stream closes cleanly (Hono's default error handler is
15
- * never invoked because we catch inside the callback).
51
+ * Observer state (`chunks`, `started`, `finished`) lives in the outer
52
+ * function scope so the outer ReadableStream's `cancel()` callback can fire
53
+ * `onAbort` when the consumer cancels mid-stream.
16
54
  */
17
- export function sseGeneratorResponse(c, gen, options = {}) {
18
- const { emitResult = false } = options;
19
- return streamSSE(c, async (stream) => {
55
+ function buildSseResponse(source, options) {
56
+ const { emitResult = false, observers, observerCtx, signal, timeoutMs, } = options;
57
+ const obs = observers ?? [];
58
+ let chunks = 0;
59
+ let started = false;
60
+ let finished = false;
61
+ async function* framePump() {
62
+ if (obs.length > 0 && observerCtx) {
63
+ fanStart(obs, observerCtx);
64
+ started = true;
65
+ }
20
66
  try {
21
- while (!stream.aborted) {
22
- const step = await gen.next();
67
+ while (true) {
68
+ const step = await source.next();
23
69
  if (step.done) {
24
70
  if (emitResult && step.value !== undefined) {
25
- await stream.writeSSE({
26
- event: 'result',
27
- data: JSON.stringify(step.value),
28
- });
71
+ yield { event: 'result', data: JSON.stringify(step.value) };
72
+ }
73
+ if (started && observerCtx) {
74
+ fanEnd(obs, observerCtx, { chunks, result: step.value });
29
75
  }
76
+ finished = true;
30
77
  return;
31
78
  }
32
- await stream.writeSSE({ data: JSON.stringify(step.value) });
79
+ yield { data: JSON.stringify(step.value) };
80
+ if (started && observerCtx) {
81
+ fanChunk(obs, observerCtx, step.value, chunks);
82
+ }
83
+ chunks += 1;
33
84
  }
34
- // Aborted; release the generator cleanly.
35
- await gen.return(undefined).catch(() => {
36
- /* swallow */
37
- });
38
85
  }
39
86
  catch (err) {
40
- await gen.return(undefined).catch(() => {
41
- /* swallow */
42
- });
43
- await stream.writeSSE({
44
- event: 'error',
45
- data: encodeErrorPayload(err),
46
- });
87
+ await source.return(undefined).catch(() => undefined);
88
+ if (started && observerCtx) {
89
+ fanError(obs, observerCtx, err, { chunks });
90
+ }
91
+ if (isTimeoutAbort(signal) && typeof timeoutMs === 'number') {
92
+ yield { event: 'timeout', data: JSON.stringify({ timeoutMs }) };
93
+ }
94
+ else {
95
+ yield { event: 'error', data: encodeErrorPayload(err) };
96
+ }
97
+ finished = true;
47
98
  }
99
+ }
100
+ const pump = framePump();
101
+ const body = new ReadableStream({
102
+ async pull(controller) {
103
+ const { value, done } = await pump.next();
104
+ if (done) {
105
+ controller.close();
106
+ }
107
+ else {
108
+ controller.enqueue(value);
109
+ }
110
+ },
111
+ cancel() {
112
+ // Consumer cancelled before the pump completed. Notify observers via
113
+ // `onAbort` exactly when we've genuinely been aborted mid-stream
114
+ // (i.e. the source started but didn't finish naturally).
115
+ if (!finished && started && observerCtx) {
116
+ fanAbort(obs, observerCtx, { chunks });
117
+ }
118
+ pump.return(undefined).catch(() => undefined);
119
+ source.return(undefined).catch(() => undefined);
120
+ },
121
+ }).pipeThrough(sseEncodeTransform());
122
+ return new Response(body, {
123
+ headers: {
124
+ 'content-type': 'text/event-stream',
125
+ 'cache-control': 'no-cache',
126
+ },
48
127
  });
49
128
  }
50
129
  /**
51
- * Wrap a ReadableStream<T> (with T a JSON-encodable value) as an SSE response.
52
- * Each enqueued chunk is JSON-encoded and written as a `data:` event.
130
+ * Wrap an async generator as an SSE response.
131
+ *
132
+ * Each yield is JSON-encoded and written as a `data:` event. If `emitResult`
133
+ * is true and the generator's return value is defined, it is written as
134
+ * `event: result\ndata: <json>` before the stream closes. If the generator
135
+ * throws, an `event: error` or `event: timeout` frame is written and the
136
+ * stream closes cleanly. Observer lifecycle hooks (`onStart` / `onChunk` /
137
+ * `onEnd` / `onError` / `onAbort`) fire from inside the pump.
53
138
  */
54
- export function sseReadableStreamResponse(c, source) {
55
- return streamSSE(c, async (stream) => {
56
- const reader = source.getReader();
57
- try {
58
- while (!stream.aborted) {
59
- const { done, value } = await reader.read();
60
- if (done)
61
- return;
62
- await stream.writeSSE({ data: JSON.stringify(value) });
63
- }
64
- }
65
- catch (err) {
66
- await stream.writeSSE({
67
- event: 'error',
68
- data: encodeErrorPayload(err),
69
- });
70
- }
71
- finally {
72
- reader.cancel().catch(() => {
73
- /* swallow */
74
- });
75
- }
139
+ export function sseGeneratorResponse(_c, gen, options = {}) {
140
+ return buildSseResponse(gen, options);
141
+ }
142
+ /**
143
+ * Wrap a `ReadableStream<T>` (with `T` a JSON-encodable value) as an SSE
144
+ * response. Each enqueued chunk is JSON-encoded and written as a `data:`
145
+ * event. Observer lifecycle hooks fire identically to `sseGeneratorResponse`;
146
+ * `emitResult` is not meaningful here (streams have no return value) and is
147
+ * ignored.
148
+ */
149
+ export function sseReadableStreamResponse(_c, source, options = {}) {
150
+ return buildSseResponse(iterReadable(source), {
151
+ ...options,
152
+ emitResult: false,
76
153
  });
77
154
  }
78
155
  export function isAsyncGenerator(value) {
@@ -0,0 +1,2 @@
1
+ import type { HonoPreactAdapter } from './adapter.js';
2
+ export declare function cloudflareAdapter(): HonoPreactAdapter;
@@ -0,0 +1,25 @@
1
+ // packages/vite/src/adapter-cloudflare.ts
2
+ //
3
+ // Standalone module. NOT re-exported by index.ts: importing `hono-preact/vite`
4
+ // must never pull in `@cloudflare/vite-plugin`. Only importing
5
+ // `hono-preact/adapter-cloudflare` loads this file.
6
+ import { cloudflare } from '@cloudflare/vite-plugin';
7
+ export function cloudflareAdapter() {
8
+ return {
9
+ name: 'cloudflare',
10
+ vitePlugins() {
11
+ // `@cloudflare/vite-plugin` drives both workerd dev and the build via
12
+ // the Environment API, and reads the worker entry from wrangler.jsonc
13
+ // `main`. It needs no entry argument from the framework.
14
+ // `cloudflare()` may return a single plugin or an array; normalize so
15
+ // the HonoPreactAdapter contract (a flat Plugin[]) holds either way.
16
+ const produced = cloudflare();
17
+ return Array.isArray(produced) ? produced : [produced];
18
+ },
19
+ wrapEntry(ctx) {
20
+ // A Hono app's default export is already a valid Workers fetch handler,
21
+ // so the platform tail is a bare re-export of the core app module.
22
+ return `export { default } from ${JSON.stringify(ctx.coreAppModuleId)};\n`;
23
+ },
24
+ };
25
+ }
@@ -0,0 +1,2 @@
1
+ import type { HonoPreactAdapter } from './adapter.js';
2
+ export declare function nodeAdapter(): HonoPreactAdapter;
@@ -0,0 +1,49 @@
1
+ import { nodeBuildPlugin, nodeDevServerPlugin } from './node-dev-server.js';
2
+ export function nodeAdapter() {
3
+ return {
4
+ name: 'node',
5
+ vitePlugins(ctx) {
6
+ return [nodeBuildPlugin(ctx), nodeDevServerPlugin(ctx)];
7
+ },
8
+ wrapEntry(ctx) {
9
+ const hasApi = ctx.apiModuleId != null;
10
+ const apiImport = hasApi
11
+ ? `import * as __api from ${JSON.stringify(ctx.apiModuleId)};\n`
12
+ : '';
13
+ const injectExport = hasApi
14
+ ? `export const injectWebSocket = __api.injectWebSocket;\n`
15
+ : '';
16
+ const injectBoot = hasApi
17
+ ? ` if (__api.injectWebSocket) __api.injectWebSocket(server);\n`
18
+ : '';
19
+ // The outer app serves built client assets under /static/* and mounts
20
+ // the framework's core Hono app at the root.
21
+ //
22
+ // The serve() boot is guarded by `import.meta.env.PROD`. In `vite dev`
23
+ // the Node dev plugin loads this wrapper through the SSR module runner
24
+ // purely to obtain `app` (and `injectWebSocket`); PROD is false there so
25
+ // no rogue HTTP server starts. In the production build it constant-folds
26
+ // to true and the bundle boots a real server.
27
+ return (`import { serve } from '@hono/node-server';\n` +
28
+ `import { serveStatic } from '@hono/node-server/serve-static';\n` +
29
+ `import { Hono } from 'hono';\n` +
30
+ `import coreApp from ${JSON.stringify(ctx.coreAppModuleId)};\n` +
31
+ apiImport +
32
+ `\n` +
33
+ `const app = new Hono()\n` +
34
+ ` .use('/static/*', serveStatic({ root: './dist/client' }))\n` +
35
+ ` .route('/', coreApp);\n` +
36
+ `\n` +
37
+ `export { app };\n` +
38
+ `export default app;\n` +
39
+ injectExport +
40
+ `\n` +
41
+ `if (import.meta.env.PROD) {\n` +
42
+ ` const port = Number(process.env.PORT) || 3000;\n` +
43
+ ` const server = serve({ fetch: app.fetch, port });\n` +
44
+ ` console.log(\`hono-preact: listening on http://localhost:\${port}\`);\n` +
45
+ injectBoot +
46
+ `}\n`);
47
+ },
48
+ };
49
+ }
@@ -0,0 +1,29 @@
1
+ import type { Plugin } from 'vite';
2
+ /**
3
+ * Static context the framework hands an adapter. `command` and `outDir`
4
+ * are intentionally absent: they are not known when honoPreact() builds its
5
+ * plugin array. Adapters that need them read them from their own plugin
6
+ * hooks (config / configResolved).
7
+ */
8
+ export interface HonoPreactAdapterContext {
9
+ /** Vite project root (process.cwd() when honoPreact() is called). */
10
+ root: string;
11
+ /** Absolute path of the framework-generated core Hono app module. */
12
+ coreAppModuleId: string;
13
+ /** Absolute path where the adapter's wrapEntry() output is written. */
14
+ entryWrapperId: string;
15
+ /** Absolute path of the user's api module, if it exists. Used by adapters
16
+ * that need to reach api-module exports (e.g. the Node adapter's
17
+ * WebSocket `injectWebSocket`). Undefined when the project has no api.ts. */
18
+ apiModuleId?: string;
19
+ }
20
+ /**
21
+ * A deployment target. `vitePlugins()` contributes the terminal build/dev
22
+ * plugins; `wrapEntry()` returns the platform tail that imports the core
23
+ * Hono app module and adapts it to the runtime.
24
+ */
25
+ export interface HonoPreactAdapter {
26
+ name: string;
27
+ vitePlugins(ctx: HonoPreactAdapterContext): Plugin[];
28
+ wrapEntry(ctx: HonoPreactAdapterContext): string;
29
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -17,10 +17,11 @@ export function clientShimPlugin(clientEntry) {
17
17
  return {
18
18
  name: 'hono-preact:client-shim',
19
19
  enforce: 'pre',
20
- apply(_, { command, mode }) {
21
- // Inject during dev (`vite serve`) and the client build only. The SSR
22
- // build runs in Node/Workers and does not need the shim.
23
- return command === 'serve' || (command === 'build' && mode === 'client');
20
+ apply(_, { command }) {
21
+ // The shim is needed for dev and for the build. The `transform` hook
22
+ // below self-gates to the client entry module, so it never injects
23
+ // into SSR/worker code regardless of build environment.
24
+ return command === 'serve' || command === 'build';
24
25
  },
25
26
  configResolved(config) {
26
27
  resolvedEntry = path.resolve(config.root, clientEntry);
@@ -2,10 +2,33 @@ import { parse } from '@babel/parser';
2
2
  import MagicString from 'magic-string';
3
3
  import { BABEL_PARSER_PLUGINS } from './parser-options.js';
4
4
  const ISO_PACKAGE_SOURCES = new Set(['../iso/index.js', 'hono-preact']);
5
- const NOOP_IMPORT_SOURCE = 'hono-preact/internal';
6
- const NOOP_LOCAL_NAME = '__$guardNoop_hpiso';
7
- function collectLocalBindings(ast, targets) {
5
+ // In the server bundle we strip anything client-only. The replacement
6
+ // `fn` arity matches the documented `(ctx, next) => Promise<void | Outcome>`
7
+ // shape so any user introspecting `mw.fn` sees the right signature; the
8
+ // framework path filters on `runs` before invoking and never executes a
9
+ // wrong-env body.
10
+ const SERVER_BUNDLE_STRIPS = [
11
+ {
12
+ name: 'defineClientMiddleware',
13
+ replacement: `{ __kind: 'middleware', runs: 'client', fn: (_ctx, next) => next() }`,
14
+ },
15
+ ];
16
+ // In the client bundle we strip anything server-only. Stream observers
17
+ // fire on the server-side streaming pipeline (start/chunk/end/error/abort)
18
+ // so they're server-only too.
19
+ const CLIENT_BUNDLE_STRIPS = [
20
+ {
21
+ name: 'defineServerMiddleware',
22
+ replacement: `{ __kind: 'middleware', runs: 'server', fn: (_ctx, next) => next() }`,
23
+ },
24
+ {
25
+ name: 'defineStreamObserver',
26
+ replacement: `{ __kind: 'observer' }`,
27
+ },
28
+ ];
29
+ function collectLocalBindings(ast, strips) {
8
30
  const bindings = new Map();
31
+ const byName = new Map(strips.map((s) => [s.name, s]));
9
32
  for (const node of ast.program.body) {
10
33
  if (node.type !== 'ImportDeclaration')
11
34
  continue;
@@ -17,11 +40,9 @@ function collectLocalBindings(ast, targets) {
17
40
  continue;
18
41
  if (spec.imported.type !== 'Identifier')
19
42
  continue;
20
- const name = spec.imported.name;
21
- if (name === 'defineServerGuard' || name === 'defineClientGuard') {
22
- if (targets.has(name)) {
23
- bindings.set(spec.local.name, name);
24
- }
43
+ const strategy = byName.get(spec.imported.name);
44
+ if (strategy) {
45
+ bindings.set(spec.local.name, strategy);
25
46
  }
26
47
  }
27
48
  }
@@ -38,18 +59,15 @@ function findCallsByLocalName(node, bindings, hits) {
38
59
  const n = node;
39
60
  if (n.type === 'CallExpression' &&
40
61
  n.callee?.type === 'Identifier' &&
41
- n.callee.name &&
42
- bindings.has(n.callee.name) &&
43
- n.arguments &&
44
- n.arguments.length >= 1 &&
45
- n.arguments[0].start !== undefined &&
46
- n.arguments[0].end !== undefined) {
47
- hits.push({
48
- start: n.start,
49
- end: n.end,
50
- argStart: n.arguments[0].start,
51
- argEnd: n.arguments[0].end,
52
- });
62
+ n.callee.name) {
63
+ const strategy = bindings.get(n.callee.name);
64
+ if (strategy && n.start !== undefined && n.end !== undefined) {
65
+ hits.push({
66
+ strategy,
67
+ start: n.start,
68
+ end: n.end,
69
+ });
70
+ }
53
71
  }
54
72
  for (const key of Object.keys(node)) {
55
73
  if (key === 'loc' ||
@@ -66,19 +84,27 @@ export function guardStripPlugin() {
66
84
  transform(code, id, options) {
67
85
  if (!/\.[jt]sx?$/.test(id))
68
86
  return;
87
+ // F7: `.server.*` files are intentionally skipped in both bundles.
88
+ // In the client bundle the server-only stub plugin already rewrites
89
+ // imports of these files; in the server bundle the file's own
90
+ // body stays as-authored. The validation plugin restricts a
91
+ // `.server.*` module's named exports to the allowlist, so a user
92
+ // cannot land a `defineClientMiddleware(...)` value as a recognized
93
+ // export and ship it to the server.
69
94
  if (/\.server\.[jt]sx?$/.test(id))
70
95
  return;
71
- const stripping = options?.ssr
72
- ? 'defineClientGuard'
73
- : 'defineServerGuard';
74
- if (!code.includes(stripping))
96
+ const strips = options?.ssr ? SERVER_BUNDLE_STRIPS : CLIENT_BUNDLE_STRIPS;
97
+ // Cheap pre-filter: only parse files that mention at least one of the
98
+ // symbols we strip. Avoids parsing the entire dep graph just to
99
+ // confirm no strips apply.
100
+ if (!strips.some((s) => code.includes(s.name)))
75
101
  return;
76
102
  const ast = parse(code, {
77
103
  sourceType: 'module',
78
104
  plugins: BABEL_PARSER_PLUGINS,
79
105
  errorRecovery: true,
80
106
  });
81
- const bindings = collectLocalBindings(ast, new Set([stripping]));
107
+ const bindings = collectLocalBindings(ast, strips);
82
108
  if (bindings.size === 0)
83
109
  return;
84
110
  const hits = [];
@@ -87,9 +113,8 @@ export function guardStripPlugin() {
87
113
  return;
88
114
  const s = new MagicString(code);
89
115
  for (const hit of [...hits].reverse()) {
90
- s.overwrite(hit.argStart, hit.argEnd, NOOP_LOCAL_NAME);
116
+ s.overwrite(hit.start, hit.end, hit.strategy.replacement);
91
117
  }
92
- s.prepend(`import { ${NOOP_LOCAL_NAME} } from '${NOOP_IMPORT_SOURCE}';\n`);
93
118
  return { code: s.toString(), map: s.generateMap({ hires: true }) };
94
119
  },
95
120
  };
@@ -1,12 +1,12 @@
1
- import { type BuildEnvironmentOptions, type Plugin } from 'vite';
1
+ import { type Plugin } from 'vite';
2
+ import type { HonoPreactAdapter } from './adapter.js';
2
3
  export interface HonoPreactOptions {
4
+ /** Deployment target. Required. See hono-preact/adapter-cloudflare. */
5
+ adapter: HonoPreactAdapter;
3
6
  layout?: string;
4
7
  routes?: string;
5
8
  api?: string;
9
+ appConfig?: string;
6
10
  clientEntry?: string;
7
- entry?: string;
8
- clientBuild?: BuildEnvironmentOptions;
9
- serverBuild?: BuildEnvironmentOptions;
10
- sharedBuild?: BuildEnvironmentOptions;
11
11
  }
12
- export declare function honoPreact(options?: HonoPreactOptions): Plugin[];
12
+ export declare function honoPreact(options: HonoPreactOptions): Plugin[];