hono-preact 0.1.0 → 0.2.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 (88) 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.d.ts +10 -14
  11. package/dist/iso/action.js +57 -21
  12. package/dist/iso/define-app.d.ts +7 -0
  13. package/dist/iso/define-app.js +3 -0
  14. package/dist/iso/define-loader.d.ts +19 -0
  15. package/dist/iso/define-loader.js +4 -0
  16. package/dist/iso/define-middleware.d.ts +43 -0
  17. package/dist/iso/define-middleware.js +6 -0
  18. package/dist/iso/define-page.d.ts +7 -2
  19. package/dist/iso/define-page.js +1 -1
  20. package/dist/iso/define-routes.d.ts +24 -1
  21. package/dist/iso/define-routes.js +34 -0
  22. package/dist/iso/define-stream-observer.d.ts +20 -0
  23. package/dist/iso/define-stream-observer.js +3 -0
  24. package/dist/iso/index.d.ts +10 -5
  25. package/dist/iso/index.js +5 -3
  26. package/dist/iso/internal/contexts.d.ts +0 -2
  27. package/dist/iso/internal/contexts.js +0 -1
  28. package/dist/iso/internal/loader-fetch.js +37 -7
  29. package/dist/iso/internal/loader-runner.js +105 -8
  30. package/dist/iso/internal/middleware-runner.d.ts +22 -0
  31. package/dist/iso/internal/middleware-runner.js +79 -0
  32. package/dist/iso/internal/page-middleware-host.d.ts +13 -0
  33. package/dist/iso/internal/page-middleware-host.js +119 -0
  34. package/dist/iso/internal/route-boundary.d.ts +1 -0
  35. package/dist/iso/internal/route-boundary.js +16 -0
  36. package/dist/iso/internal/stream-observer-runner.d.ts +13 -0
  37. package/dist/iso/internal/stream-observer-runner.js +48 -0
  38. package/dist/iso/internal/use-partitioner.d.ts +9 -0
  39. package/dist/iso/internal/use-partitioner.js +11 -0
  40. package/dist/iso/internal/use-types.d.ts +7 -0
  41. package/dist/iso/internal/use-types.js +1 -0
  42. package/dist/iso/internal.d.ts +5 -4
  43. package/dist/iso/internal.js +8 -6
  44. package/dist/iso/outcomes.d.ts +38 -0
  45. package/dist/iso/outcomes.js +56 -0
  46. package/dist/iso/page-only.d.ts +5 -0
  47. package/dist/iso/page-only.js +20 -0
  48. package/dist/iso/page.d.ts +3 -3
  49. package/dist/iso/page.js +3 -3
  50. package/dist/page.d.ts +1 -0
  51. package/dist/page.d.ts.map +1 -0
  52. package/dist/page.js +8 -0
  53. package/dist/server/actions-handler.d.ts +20 -6
  54. package/dist/server/actions-handler.js +83 -47
  55. package/dist/server/context.js +1 -1
  56. package/dist/server/index.d.ts +1 -1
  57. package/dist/server/index.js +1 -1
  58. package/dist/server/loaders-handler.d.ts +16 -0
  59. package/dist/server/loaders-handler.js +94 -17
  60. package/dist/server/render.d.ts +2 -0
  61. package/dist/server/render.js +104 -33
  62. package/dist/server/route-server-modules.d.ts +42 -1
  63. package/dist/server/route-server-modules.js +184 -0
  64. package/dist/server/sse.d.ts +24 -1
  65. package/dist/server/sse.js +56 -4
  66. package/dist/vite/adapter-cloudflare.d.ts +2 -0
  67. package/dist/vite/adapter-cloudflare.js +25 -0
  68. package/dist/vite/adapter-node.d.ts +2 -0
  69. package/dist/vite/adapter-node.js +49 -0
  70. package/dist/vite/adapter.d.ts +29 -0
  71. package/dist/vite/adapter.js +1 -0
  72. package/dist/vite/client-shim.js +5 -4
  73. package/dist/vite/guard-strip.js +52 -27
  74. package/dist/vite/hono-preact.d.ts +6 -6
  75. package/dist/vite/hono-preact.js +48 -77
  76. package/dist/vite/index.d.ts +2 -1
  77. package/dist/vite/index.js +1 -1
  78. package/dist/vite/node-dev-server.d.ts +4 -0
  79. package/dist/vite/node-dev-server.js +121 -0
  80. package/dist/vite/server-entry.d.ts +30 -7
  81. package/dist/vite/server-entry.js +161 -78
  82. package/dist/vite/server-exports-contract.d.ts +6 -0
  83. package/dist/vite/server-exports-contract.js +43 -0
  84. package/dist/vite/server-loader-validation.js +36 -9
  85. package/dist/vite/server-loaders-parser.d.ts +17 -1
  86. package/dist/vite/server-loaders-parser.js +41 -0
  87. package/dist/vite/server-only.js +20 -2
  88. package/package.json +32 -4
@@ -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[];
@@ -1,6 +1,3 @@
1
- import build from '@hono/vite-build/cloudflare-workers';
2
- import devServer, { defaultOptions } from '@hono/vite-dev-server';
3
- import cloudflareAdapter from '@hono/vite-dev-server/cloudflare';
4
1
  import preact from '@preact/preset-vite';
5
2
  import { clientShimPlugin } from './client-shim.js';
6
3
  import { clientEntryPlugin, VIRTUAL_CLIENT_ENTRY_ID } from './client-entry.js';
@@ -8,68 +5,56 @@ import { serverLoaderValidationPlugin } from './server-loader-validation.js';
8
5
  import { moduleKeyPlugin } from './module-key-plugin.js';
9
6
  import { serverOnlyPlugin } from './server-only.js';
10
7
  import { guardStripPlugin } from './guard-strip.js';
11
- import { GENERATED_SERVER_ENTRY_RELATIVE, generatedServerEntryAbsPath, serverEntryPlugin, } from './server-entry.js';
12
- export function honoPreact(options = {}) {
13
- const { layout = 'src/Layout.tsx', routes = 'src/routes.ts', api = 'src/api.ts', clientEntry = VIRTUAL_CLIENT_ENTRY_ID, entry, clientBuild = {}, serverBuild = {}, sharedBuild = {}, } = options;
14
- const useGeneratedEntry = entry === undefined;
15
- const resolvedEntry = entry ?? GENERATED_SERVER_ENTRY_RELATIVE;
8
+ import { generatedCoreAppAbsPath, generatedEntryWrapperAbsPath, serverEntryPlugin, } from './server-entry.js';
9
+ export function honoPreact(options) {
10
+ // `?? {}` is deliberate: TypeScript types `options` as required, but a
11
+ // zero-arg `honoPreact()` call still reaches here at runtime. Without the
12
+ // fallback, destructuring `undefined` throws a cryptic TypeError; with it,
13
+ // the friendly `adapter`-required guard below fires instead.
14
+ const { adapter, layout = 'src/Layout.tsx', routes = 'src/routes.ts', api = 'src/api.ts', appConfig = 'src/app-config.ts', clientEntry = VIRTUAL_CLIENT_ENTRY_ID, } = options ?? {};
15
+ if (!adapter) {
16
+ throw new Error('[hono-preact] honoPreact() requires an `adapter` option. ' +
17
+ "Import one, e.g. `import { cloudflareAdapter } from 'hono-preact/adapter-cloudflare'`, " +
18
+ 'and pass `honoPreact({ adapter: cloudflareAdapter() })`.');
19
+ }
20
+ const coreAppPath = generatedCoreAppAbsPath();
21
+ const entryWrapperPath = generatedEntryWrapperAbsPath();
22
+ const ctx = {
23
+ root: process.cwd(),
24
+ coreAppModuleId: coreAppPath,
25
+ entryWrapperId: entryWrapperPath,
26
+ };
27
+ // Shared config plus the `client` build environment's input. The worker
28
+ // environment is configured by the adapter's plugins; the `client`
29
+ // environment's entry is framework-owned (every adapter needs the same
30
+ // browser bundle) so it lives here. Without it, the client environment has
31
+ // no input and `vite build` emits no client JavaScript. The
32
+ // `static/client.js` entry name is the URL the SSR layer references and
33
+ // must stay stable.
16
34
  const configPlugin = {
17
35
  name: 'hono-preact:config',
18
- config(_, { mode }) {
19
- const shared = {
36
+ config() {
37
+ return {
20
38
  resolve: {
21
39
  dedupe: ['preact', 'preact/compat', 'preact/hooks', 'preact-iso'],
22
40
  },
23
41
  build: {
24
42
  target: 'esnext',
25
43
  assetsDir: 'static',
26
- ssrEmitAssets: true,
27
- minify: true,
28
- ...sharedBuild,
29
44
  },
30
- };
31
- if (mode === 'client') {
32
- const { rollupOptions: userRollup, ...restClientBuild } = clientBuild;
33
- return {
34
- ...shared,
35
- build: {
36
- ...shared.build,
37
- sourcemap: true,
38
- cssCodeSplit: true,
39
- copyPublicDir: false,
40
- ...restClientBuild,
41
- rollupOptions: {
42
- input: userRollup?.input ?? [clientEntry],
43
- output: {
44
- entryFileNames: 'static/client.js',
45
- chunkFileNames: 'static/[name]-[hash].js',
46
- assetFileNames: 'static/[name]-[hash].[ext]',
47
- // Array-form output is not supported; use an OutputOptions object to
48
- // override individual fields (entryFileNames, chunkFileNames, etc.).
49
- ...(userRollup?.output && !Array.isArray(userRollup.output)
50
- ? userRollup.output
51
- : {}),
45
+ environments: {
46
+ client: {
47
+ build: {
48
+ rollupOptions: {
49
+ input: [clientEntry],
50
+ output: {
51
+ entryFileNames: 'static/client.js',
52
+ chunkFileNames: 'static/[name]-[hash].js',
53
+ assetFileNames: 'static/[name]-[hash].[ext]',
54
+ },
52
55
  },
53
56
  },
54
57
  },
55
- };
56
- }
57
- return {
58
- ...shared,
59
- ssr: {
60
- noExternal: ['preact-render-to-string', 'preact-iso', 'hono-preact'],
61
- },
62
- build: {
63
- ...shared.build,
64
- // Inline sourcemaps so SSR error stacks point at user source
65
- // (e.g. `pages/issue.server.ts:42`) instead of the rolled-up
66
- // Worker chunk. We pick `inline` over a separate .map file because
67
- // SSR is a single bundle and Workers tooling will not look at a
68
- // sibling .map. Bundle size grows; for typical app server bundles
69
- // this is a few hundred KB at most and is worth the debuggability.
70
- // Users can override by passing `serverBuild.sourcemap: false`.
71
- sourcemap: 'inline',
72
- ...serverBuild,
73
58
  },
74
59
  };
75
60
  },
@@ -78,34 +63,20 @@ export function honoPreact(options = {}) {
78
63
  configPlugin,
79
64
  clientShimPlugin(clientEntry),
80
65
  clientEntryPlugin({ routes }),
81
- ...(useGeneratedEntry
82
- ? [
83
- serverEntryPlugin({
84
- layout,
85
- routes,
86
- api,
87
- outputPath: generatedServerEntryAbsPath(),
88
- }),
89
- ]
90
- : []),
66
+ serverEntryPlugin({
67
+ layout,
68
+ routes,
69
+ api,
70
+ appConfig,
71
+ adapter,
72
+ coreAppPath,
73
+ entryWrapperPath,
74
+ }),
91
75
  serverLoaderValidationPlugin(),
92
76
  moduleKeyPlugin(),
93
77
  serverOnlyPlugin(),
94
78
  guardStripPlugin(),
95
- Object.assign(build({ entry: resolvedEntry }), {
96
- apply: (_, { command, mode }) => command === 'build' && mode !== 'client',
97
- }),
98
- Object.assign(devServer({
99
- entry: resolvedEntry,
100
- exclude: [
101
- ...defaultOptions.exclude,
102
- /\.scss/,
103
- /\.css/,
104
- /\?url/,
105
- /\?inline/,
106
- ],
107
- adapter: cloudflareAdapter,
108
- }), { apply: 'serve' }),
79
+ ...adapter.vitePlugins(ctx),
109
80
  ...preact(),
110
81
  ];
111
82
  }
@@ -2,6 +2,7 @@ export { honoPreact } from './hono-preact.js';
2
2
  export { serverLoaderValidationPlugin } from './server-loader-validation.js';
3
3
  export { serverOnlyPlugin, VITE_ROOT_ACCESSOR } from './server-only.js';
4
4
  export { moduleKeyPlugin } from './module-key-plugin.js';
5
- export { GENERATED_SERVER_ENTRY_RELATIVE, generatedServerEntryAbsPath, serverEntryPlugin, } from './server-entry.js';
5
+ export { GENERATED_CORE_APP_RELATIVE, GENERATED_ENTRY_WRAPPER_RELATIVE, generatedCoreAppAbsPath, generatedEntryWrapperAbsPath, serverEntryPlugin, } from './server-entry.js';
6
+ export type { HonoPreactAdapter, HonoPreactAdapterContext } from './adapter.js';
6
7
  export { clientEntryPlugin, VIRTUAL_CLIENT_ENTRY_ID } from './client-entry.js';
7
8
  export { guardStripPlugin } from './guard-strip.js';
@@ -2,6 +2,6 @@ export { honoPreact } from './hono-preact.js';
2
2
  export { serverLoaderValidationPlugin } from './server-loader-validation.js';
3
3
  export { serverOnlyPlugin, VITE_ROOT_ACCESSOR } from './server-only.js';
4
4
  export { moduleKeyPlugin } from './module-key-plugin.js';
5
- export { GENERATED_SERVER_ENTRY_RELATIVE, generatedServerEntryAbsPath, serverEntryPlugin, } from './server-entry.js';
5
+ export { GENERATED_CORE_APP_RELATIVE, GENERATED_ENTRY_WRAPPER_RELATIVE, generatedCoreAppAbsPath, generatedEntryWrapperAbsPath, serverEntryPlugin, } from './server-entry.js';
6
6
  export { clientEntryPlugin, VIRTUAL_CLIENT_ENTRY_ID } from './client-entry.js';
7
7
  export { guardStripPlugin } from './guard-strip.js';
@@ -0,0 +1,4 @@
1
+ import type { Plugin } from 'vite';
2
+ import type { HonoPreactAdapterContext } from './adapter.js';
3
+ export declare function nodeBuildPlugin(ctx: HonoPreactAdapterContext): Plugin;
4
+ export declare function nodeDevServerPlugin(ctx: HonoPreactAdapterContext): Plugin;
@@ -0,0 +1,121 @@
1
+ import { createServerModuleRunner } from 'vite';
2
+ export function nodeBuildPlugin(ctx) {
3
+ return {
4
+ name: 'hono-preact:node-build',
5
+ config() {
6
+ return {
7
+ environments: {
8
+ // The Node target has no Cloudflare-style plugin to set the client
9
+ // outDir, so set it here. wrapEntry()'s serveStatic expects the
10
+ // client bundle at dist/client.
11
+ client: {
12
+ build: { outDir: 'dist/client' },
13
+ },
14
+ ssr: {
15
+ build: {
16
+ outDir: 'dist/server',
17
+ ssr: true,
18
+ rollupOptions: {
19
+ input: [ctx.entryWrapperId],
20
+ },
21
+ },
22
+ },
23
+ },
24
+ builder: {
25
+ async buildApp(builder) {
26
+ await builder.build(builder.environments.client);
27
+ await builder.build(builder.environments.ssr);
28
+ },
29
+ },
30
+ };
31
+ },
32
+ };
33
+ }
34
+ export function nodeDevServerPlugin(ctx) {
35
+ return {
36
+ name: 'hono-preact:node-dev-server',
37
+ apply: 'serve',
38
+ configureServer(server) {
39
+ const runner = createServerModuleRunner(server.environments.ssr);
40
+ // Wire the WebSocket upgrade. @hono/node-ws's injectWebSocket(target)
41
+ // just calls target.on('upgrade', fn); we pass a shim that captures
42
+ // that handler so we can invoke it with Node's real upgrade args.
43
+ // Multiple 'upgrade' listeners coexist fine with Vite's own HMR one.
44
+ server.httpServer?.on('upgrade', async (req, socket, head) => {
45
+ try {
46
+ const { injectWebSocket } = await runner.import(ctx.entryWrapperId);
47
+ if (!injectWebSocket)
48
+ return;
49
+ let handler;
50
+ injectWebSocket({
51
+ on(event, fn) {
52
+ if (event === 'upgrade')
53
+ handler = fn;
54
+ },
55
+ });
56
+ handler?.(req, socket, head);
57
+ }
58
+ catch (err) {
59
+ console.error('[hono-preact] dev ws upgrade error', err);
60
+ socket.destroy();
61
+ }
62
+ });
63
+ // Register the SSR middleware synchronously (not via the returned post
64
+ // hook). The post hook runs after Vite's spaFallbackMiddleware, which
65
+ // rewrites req.url to /index.html and makes the SSR app 404. Synchronous
66
+ // registration puts this ahead of Vite's HTML/SPA middlewares.
67
+ server.middlewares.use(async (req, res, next) => {
68
+ try {
69
+ // Vite-internal requests (its HMR client, source modules under
70
+ // /@fs and /@id, optimized deps) must reach Vite's later
71
+ // middlewares, or client hydration and HMR break. The SSR app only
72
+ // owns application routes, so pass these through. Same model as
73
+ // @hono/vite-dev-server's `exclude` option.
74
+ const path = (req.url ?? '').split('?')[0];
75
+ if (path.startsWith('/@') || path.startsWith('/node_modules/')) {
76
+ return next();
77
+ }
78
+ const { default: app } = (await runner.import(ctx.entryWrapperId));
79
+ const url = `http://${req.headers.host}${req.url}`;
80
+ const method = req.method ?? 'GET';
81
+ const headers = new Headers();
82
+ for (const [k, v] of Object.entries(req.headers)) {
83
+ if (typeof v === 'string')
84
+ headers.set(k, v);
85
+ else if (Array.isArray(v))
86
+ v.forEach((vv) => headers.append(k, vv));
87
+ }
88
+ let body;
89
+ if (method !== 'GET' && method !== 'HEAD') {
90
+ const chunks = [];
91
+ for await (const c of req)
92
+ chunks.push(c);
93
+ if (chunks.length) {
94
+ const buf = Buffer.concat(chunks);
95
+ // Copy into a fresh ArrayBuffer: BodyInit accepts ArrayBuffer,
96
+ // and this sidesteps Buffer<ArrayBufferLike> typing friction.
97
+ body = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
98
+ }
99
+ }
100
+ const request = new Request(url, { method, headers, body });
101
+ const response = await app.fetch(request);
102
+ res.statusCode = response.status;
103
+ response.headers.forEach((value, key) => res.setHeader(key, value));
104
+ if (response.body) {
105
+ const reader = response.body.getReader();
106
+ for (;;) {
107
+ const { done, value } = await reader.read();
108
+ if (done)
109
+ break;
110
+ res.write(value);
111
+ }
112
+ }
113
+ res.end();
114
+ }
115
+ catch (err) {
116
+ next(err);
117
+ }
118
+ });
119
+ },
120
+ };
121
+ }
@@ -1,26 +1,49 @@
1
1
  import type { Plugin } from 'vite';
2
- export interface GenerateServerEntrySourceOptions {
2
+ import type { HonoPreactAdapter } from './adapter.js';
3
+ export interface GenerateCoreAppModuleOptions {
3
4
  layoutAbsPath: string;
4
5
  routesAbsPath: string;
5
6
  apiAbsPath: string | undefined;
7
+ appConfigAbsPath: string | undefined;
6
8
  }
7
- export declare function generateServerEntrySource(opts: GenerateServerEntrySourceOptions): string;
8
- export type CatchAllWarning = {
9
+ export declare function generateCoreAppModule(opts: GenerateCoreAppModuleOptions): string;
10
+ export type ApiShadowingRoute = {
9
11
  kind: 'wildcard';
10
12
  method: string;
11
13
  pattern: string;
12
14
  line: number | undefined;
15
+ severity: 'error';
16
+ } | {
17
+ kind: 'reserved';
18
+ method: string;
19
+ pattern: string;
20
+ line: number | undefined;
21
+ severity: 'error';
13
22
  } | {
14
23
  kind: 'notFound';
15
24
  line: number | undefined;
25
+ severity: 'warning';
16
26
  };
17
- export declare function findApiCatchAllRoutes(source: string): CatchAllWarning[];
18
- export declare const GENERATED_SERVER_ENTRY_RELATIVE = "node_modules/.vite/hono-preact/server-entry.tsx";
19
- export declare function generatedServerEntryAbsPath(cwd?: string): string;
27
+ export declare function findApiShadowingRoutes(source: string): ApiShadowingRoute[];
28
+ export declare const GENERATED_CORE_APP_RELATIVE = "node_modules/.vite/hono-preact/core-app.tsx";
29
+ export declare const GENERATED_ENTRY_WRAPPER_RELATIVE = "node_modules/.vite/hono-preact/server-entry.tsx";
30
+ export declare function generatedCoreAppAbsPath(cwd?: string): string;
31
+ export declare function generatedEntryWrapperAbsPath(cwd?: string): string;
20
32
  export interface ServerEntryPluginOptions {
21
33
  layout: string;
22
34
  routes: string;
23
35
  api: string;
24
- outputPath: string;
36
+ /**
37
+ * Project-relative or absolute path to the user's app-config file. The
38
+ * `hono-preact` umbrella plugin always supplies a default
39
+ * (`src/app-config.ts`), so this is required here even though it's
40
+ * optional from the user's perspective: missing the file on disk is
41
+ * allowed (an inline `{ use: [] }` falls back into the generated core
42
+ * app), but the option name itself must be supplied.
43
+ */
44
+ appConfig: string;
45
+ adapter: HonoPreactAdapter;
46
+ coreAppPath: string;
47
+ entryWrapperPath: string;
25
48
  }
26
49
  export declare function serverEntryPlugin(opts: ServerEntryPluginOptions): Plugin;