@lovrozagar/honey 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.
Files changed (131) hide show
  1. package/README.md +51 -0
  2. package/docs/api-ref/go.md +399 -0
  3. package/docs/api-ref/python.md +399 -0
  4. package/docs/api-ref/rust.md +399 -0
  5. package/docs/api-ref/typescript.md +399 -0
  6. package/docs/invalidate-spec.md +303 -0
  7. package/docs/sdk.md +722 -0
  8. package/package.json +120 -0
  9. package/src/accepts.ts +78 -0
  10. package/src/body-limit.ts +100 -0
  11. package/src/build/index.ts +174 -0
  12. package/src/cli-go/auth.go +15 -0
  13. package/src/cli-go/config.go +165 -0
  14. package/src/cli-go/errors.go +57 -0
  15. package/src/cli-go/multipart.go +55 -0
  16. package/src/cli-go/output.go +109 -0
  17. package/src/cli-go/root.go +30 -0
  18. package/src/cli-go/stream.go +88 -0
  19. package/src/cli-go/version.go +5 -0
  20. package/src/cli.ts +204 -0
  21. package/src/client/error.ts +27 -0
  22. package/src/client/http.ts +502 -0
  23. package/src/client/index.ts +127 -0
  24. package/src/client/sdk.ts +265 -0
  25. package/src/client/sse.ts +102 -0
  26. package/src/client/types.ts +211 -0
  27. package/src/client/ws.ts +115 -0
  28. package/src/client-go/errors.go +207 -0
  29. package/src/client-go/invalidation.go +373 -0
  30. package/src/client-go/realtime.go +317 -0
  31. package/src/client-go/result.go +20 -0
  32. package/src/client-go/runtime.go +413 -0
  33. package/src/client-go/sse.go +149 -0
  34. package/src/client-go/transport.go +452 -0
  35. package/src/client-go/ws.go +104 -0
  36. package/src/client-mcp/README.md +10 -0
  37. package/src/client-mcp/server.ts +94 -0
  38. package/src/client-mcp/types.ts +19 -0
  39. package/src/client-python/_errors.py +115 -0
  40. package/src/client-python/_invalidation.py +578 -0
  41. package/src/client-python/_realtime.py +156 -0
  42. package/src/client-python/_runtime.py +469 -0
  43. package/src/client-python/_sse.py +99 -0
  44. package/src/client-python/_transport.py +335 -0
  45. package/src/client-python/_ws.py +131 -0
  46. package/src/client-python/py.typed +0 -0
  47. package/src/client-rust/errors.rs +200 -0
  48. package/src/client-rust/invalidation.rs +441 -0
  49. package/src/client-rust/invalidation_sync.rs +306 -0
  50. package/src/client-rust/realtime.rs +695 -0
  51. package/src/client-rust/result.rs +22 -0
  52. package/src/client-rust/runtime.rs +547 -0
  53. package/src/client-rust/runtime_sync.rs +452 -0
  54. package/src/client-rust/sse.rs +167 -0
  55. package/src/client-rust/ws.rs +143 -0
  56. package/src/cloudflare/realtime.ts +236 -0
  57. package/src/codegen-go-cli.ts +1180 -0
  58. package/src/codegen-go.ts +1010 -0
  59. package/src/codegen-hash.ts +8 -0
  60. package/src/codegen-ir.ts +517 -0
  61. package/src/codegen-loaders.ts +53 -0
  62. package/src/codegen-mcp.ts +364 -0
  63. package/src/codegen-python.ts +1028 -0
  64. package/src/codegen-rust.ts +1652 -0
  65. package/src/codegen-sanitize.ts +67 -0
  66. package/src/codegen-schema-naming.ts +146 -0
  67. package/src/codegen.ts +5015 -0
  68. package/src/context.ts +210 -0
  69. package/src/cookie-sign.ts +58 -0
  70. package/src/cookie.ts +89 -0
  71. package/src/cors.ts +96 -0
  72. package/src/crypto.ts +19 -0
  73. package/src/csrf.ts +54 -0
  74. package/src/curl-logger.ts +235 -0
  75. package/src/detect-runtime.ts +19 -0
  76. package/src/error.ts +52 -0
  77. package/src/errors.ts +87 -0
  78. package/src/etag.ts +74 -0
  79. package/src/feature-load.ts +23 -0
  80. package/src/go-type-emitter.ts +404 -0
  81. package/src/honey-response.ts +354 -0
  82. package/src/i18n-slot.ts +20 -0
  83. package/src/i18n.ts +234 -0
  84. package/src/index.ts +3843 -0
  85. package/src/init.ts +149 -0
  86. package/src/input.ts +8 -0
  87. package/src/ip-restrict.ts +192 -0
  88. package/src/logger.ts +149 -0
  89. package/src/middleware.ts +199 -0
  90. package/src/node-request.ts +240 -0
  91. package/src/node.ts +212 -0
  92. package/src/openapi/register.ts +19 -0
  93. package/src/openapi/scalar.ts +33 -0
  94. package/src/openapi/spec-factory.ts +46 -0
  95. package/src/openapi/spec.ts +31 -0
  96. package/src/openapi/swagger.ts +34 -0
  97. package/src/plugin.ts +771 -0
  98. package/src/powered-by.ts +17 -0
  99. package/src/pretty-json.ts +39 -0
  100. package/src/proxy.ts +155 -0
  101. package/src/python-type-emitter.ts +144 -0
  102. package/src/realtime/buffer.ts +101 -0
  103. package/src/realtime/bus.ts +93 -0
  104. package/src/realtime/longpoll.ts +148 -0
  105. package/src/realtime/protocol.ts +128 -0
  106. package/src/realtime/route.ts +104 -0
  107. package/src/realtime/runtime.ts +430 -0
  108. package/src/request-id.ts +23 -0
  109. package/src/request-to-curl.ts +34 -0
  110. package/src/response.ts +414 -0
  111. package/src/rust-type-emitter.ts +412 -0
  112. package/src/secure-headers.ts +69 -0
  113. package/src/serve-register.ts +8 -0
  114. package/src/serve-slot.ts +20 -0
  115. package/src/serve.ts +141 -0
  116. package/src/server-timing.ts +66 -0
  117. package/src/static.ts +60 -0
  118. package/src/telemetry/otel.ts +139 -0
  119. package/src/testing.ts +114 -0
  120. package/src/timeout.ts +36 -0
  121. package/src/tree.ts +464 -0
  122. package/src/ts-type-emitter.ts +106 -0
  123. package/src/type-emitter.ts +524 -0
  124. package/src/type-extractor.ts +749 -0
  125. package/src/types.ts +638 -0
  126. package/src/validation.ts +300 -0
  127. package/src/ws/bun.ts +89 -0
  128. package/src/ws/cloudflare.ts +133 -0
  129. package/src/ws/deno.ts +109 -0
  130. package/src/ws/node.ts +161 -0
  131. package/src/yaml.ts +80 -0
package/src/plugin.ts ADDED
@@ -0,0 +1,771 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { dirname, extname, resolve } from "node:path";
4
+ import {
5
+ generateManifest,
6
+ generateOpenApi,
7
+ generateRouteTree,
8
+ generateRouteTreeFromApp,
9
+ generateRouteTreeFromRouteTree,
10
+ generateTypes,
11
+ prepareCodegen,
12
+ sanitizeOpenApiSpec,
13
+ } from "./codegen.ts";
14
+ import type { OpenApiRouteInfo, OpenApiSanitizeOptions, OpenApiSpecInput } from "./codegen.ts";
15
+ import type { Honey } from "./index.ts";
16
+ import type { ExtractedChainTypes } from "./type-extractor.ts";
17
+ import { toYaml, yamlSiblingPath } from "./yaml.ts";
18
+
19
+ /* ---- generateFromApp (standalone utility) ---- */
20
+
21
+ type HoneyPluginOptions = {
22
+ manifest?: { output: string };
23
+ openApi?: {
24
+ info: { description?: string; title: string; version: string };
25
+ output: string;
26
+ };
27
+ };
28
+
29
+ type GeneratedArtifacts = {
30
+ manifest?: string;
31
+ openApi?: string;
32
+ openApiYaml?: string;
33
+ routeTree: string;
34
+ };
35
+
36
+ export async function generateFromApp<
37
+ TEnv,
38
+ TCtx,
39
+ TRoutes,
40
+ TMeta,
41
+ TFactory,
42
+ TDefaults extends string,
43
+ TBase extends string,
44
+ >(
45
+ app: Honey<TEnv, TCtx, TRoutes, TMeta, TFactory, TDefaults, TBase>,
46
+ options?: HoneyPluginOptions,
47
+ ): Promise<GeneratedArtifacts> {
48
+ const host = app as unknown as Honey<TEnv, TCtx, unknown, unknown, unknown, string, string>
49
+ const artifacts: GeneratedArtifacts = {
50
+ routeTree: generateRouteTreeFromApp(host),
51
+ };
52
+
53
+ if (options?.manifest) {
54
+ const manifest = generateManifest(host);
55
+ artifacts.manifest = JSON.stringify(manifest, null, 2);
56
+ }
57
+
58
+ if (options?.openApi) {
59
+ const spec = await generateOpenApi(host, { info: options.openApi.info });
60
+ artifacts.openApi = JSON.stringify(spec, null, 2);
61
+ artifacts.openApiYaml = toYaml(spec);
62
+ }
63
+
64
+ return artifacts;
65
+ }
66
+
67
+ /* ---- Config types ---- */
68
+
69
+ export interface HoneyOpenApiOutputConfig {
70
+ description?: string
71
+ filterRoutes?: (route: OpenApiRouteInfo) => boolean
72
+ path?: string
73
+ sanitize?: OpenApiSanitizeOptions
74
+ securitySchemes?: Record<string, unknown>
75
+ title: string
76
+ version: string
77
+ }
78
+
79
+ export interface HoneyGoCliConfig {
80
+ binaryName: string
81
+ configName?: string
82
+ defaultBaseURL?: string
83
+ envPrefix?: string
84
+ modulePath?: string
85
+ out: string
86
+ sdkModulePath?: string
87
+ }
88
+
89
+ export interface HoneySdkPortsConfig {
90
+ go?: { modulePath?: string; outDir: string };
91
+ python?: { outDir: string };
92
+ rust?: { crateName?: string; outDir: string };
93
+ typescript?: boolean | { outDir?: string };
94
+ }
95
+
96
+ export interface HoneyCodegenConfig {
97
+ cli?: boolean | HoneyGoCliConfig;
98
+ manifest?: boolean | string;
99
+ mergeTree?: string;
100
+ openApi?: HoneyOpenApiOutputConfig | HoneyOpenApiOutputConfig[];
101
+ sdk?: boolean | { name?: string; ports?: HoneySdkPortsConfig; specs?: string[] };
102
+ tree?: boolean | string;
103
+ types?: boolean | string | { baseCtxName?: string; path?: string };
104
+ }
105
+
106
+ export interface HoneyVitePluginConfig {
107
+ app?: string;
108
+ codegen?: HoneyCodegenConfig;
109
+ watch?: string[];
110
+ }
111
+
112
+ /* ---- Resolved config ---- */
113
+
114
+ export type ResolvedOpenApiOutput = {
115
+ description?: string
116
+ filterRoutes?: (route: OpenApiRouteInfo) => boolean
117
+ path: string
118
+ sanitize?: OpenApiSanitizeOptions
119
+ securitySchemes?: Record<string, unknown>
120
+ title: string
121
+ version: string
122
+ }
123
+
124
+ export type ResolvedGoCliConfig = {
125
+ binaryName: string
126
+ configName: string | undefined
127
+ defaultBaseURL: string | undefined
128
+ envPrefix: string | undefined
129
+ modulePath: string | undefined
130
+ out: string
131
+ sdkModulePath: string | undefined
132
+ }
133
+
134
+ export type ResolvedSdkPorts = {
135
+ go?: { modulePath: string | undefined; outDir: string };
136
+ python?: { outDir: string };
137
+ rust?: { crateName: string | undefined; outDir: string };
138
+ typescript?: { outDir: string };
139
+ };
140
+
141
+ export interface ResolvedHoneyConfig {
142
+ app?: string;
143
+ codegen: {
144
+ cli: false | ResolvedGoCliConfig;
145
+ manifest: false | string;
146
+ mergeTree: string | undefined;
147
+ openApi: false | ResolvedOpenApiOutput[];
148
+ sdk: false | { name: string; ports: ResolvedSdkPorts; specs?: string[] };
149
+ tree: false | string;
150
+ types: false | { baseCtxName: string | undefined; path: string };
151
+ };
152
+ watch: string[];
153
+ }
154
+
155
+ export function resolveHoneyConfig(
156
+ raw: HoneyVitePluginConfig,
157
+ ): ResolvedHoneyConfig {
158
+ const c = raw.codegen;
159
+
160
+ const resolvePathFlag = (
161
+ val: boolean | string | undefined,
162
+ defaultPath: string,
163
+ defaultEnabled: boolean,
164
+ ): false | string => {
165
+ if (val === false) return false;
166
+ if (val === true || (val === undefined && defaultEnabled))
167
+ return defaultPath;
168
+ if (typeof val === "string") return val;
169
+ return false;
170
+ };
171
+
172
+ let types: false | { baseCtxName: string | undefined; path: string } = false;
173
+ if (c?.types !== undefined && c.types !== false) {
174
+ if (typeof c.types === "string") {
175
+ types = { baseCtxName: undefined, path: c.types };
176
+ } else if (c.types === true) {
177
+ types = { baseCtxName: undefined, path: "src/_gen/types.gen.d.ts" };
178
+ } else {
179
+ types = {
180
+ baseCtxName: c.types.baseCtxName,
181
+ path: c.types.path ?? "src/_gen/types.gen.d.ts",
182
+ };
183
+ }
184
+ }
185
+
186
+ let sdk: false | { name: string; ports: ResolvedSdkPorts; specs?: string[] } = false;
187
+ if (c?.sdk !== undefined && c.sdk !== false) {
188
+ if (typeof c.sdk === "string") {
189
+ throw new Error("codegen.sdk string form removed — pass object { name?, specs?, ports }")
190
+ }
191
+ if (c.sdk === true) {
192
+ sdk = { name: "SDK", ports: { typescript: { outDir: "src/_gen" } } };
193
+ } else {
194
+ const rawPorts = c.sdk.ports ?? { typescript: true };
195
+ const ports: ResolvedSdkPorts = {};
196
+ if (rawPorts.typescript !== undefined && rawPorts.typescript !== false) {
197
+ if (rawPorts.typescript === true) {
198
+ ports.typescript = { outDir: "src/_gen" };
199
+ } else {
200
+ ports.typescript = { outDir: rawPorts.typescript.outDir ?? "src/_gen" };
201
+ }
202
+ }
203
+ if (rawPorts.python) {
204
+ ports.python = { outDir: rawPorts.python.outDir };
205
+ }
206
+ if (rawPorts.go) {
207
+ ports.go = { modulePath: rawPorts.go.modulePath, outDir: rawPorts.go.outDir };
208
+ }
209
+ if (rawPorts.rust) {
210
+ ports.rust = { crateName: rawPorts.rust.crateName, outDir: rawPorts.rust.outDir };
211
+ }
212
+ sdk = {
213
+ name: c.sdk.name ?? "SDK",
214
+ ports,
215
+ specs: c.sdk.specs,
216
+ };
217
+ }
218
+ }
219
+
220
+ let openApi: false | ResolvedOpenApiOutput[] = false
221
+ if (c?.openApi) {
222
+ const entries = Array.isArray(c.openApi) ? c.openApi : [c.openApi]
223
+ openApi = entries.map((entry) => ({
224
+ description: entry.description,
225
+ filterRoutes: entry.filterRoutes,
226
+ path: entry.path ?? "src/_gen/openapi.gen.json",
227
+ sanitize: entry.sanitize,
228
+ securitySchemes: entry.securitySchemes,
229
+ title: entry.title,
230
+ version: entry.version,
231
+ }))
232
+ }
233
+
234
+ let cli: false | ResolvedGoCliConfig = false
235
+ if (c?.cli !== undefined && c.cli !== false) {
236
+ if (c.cli === true) {
237
+ throw new Error(
238
+ "codegen.cli=true requires `out` and `binaryName` — pass an object { out, binaryName }",
239
+ )
240
+ }
241
+ if (!c.cli.out || !c.cli.binaryName) {
242
+ throw new Error("codegen.cli requires `out` and `binaryName`")
243
+ }
244
+ cli = {
245
+ binaryName: c.cli.binaryName,
246
+ configName: c.cli.configName,
247
+ defaultBaseURL: c.cli.defaultBaseURL,
248
+ envPrefix: c.cli.envPrefix,
249
+ modulePath: c.cli.modulePath,
250
+ out: c.cli.out,
251
+ sdkModulePath: c.cli.sdkModulePath,
252
+ }
253
+ }
254
+
255
+ return {
256
+ app: raw.app,
257
+ codegen: {
258
+ cli,
259
+ manifest: resolvePathFlag(
260
+ c?.manifest,
261
+ "src/_gen/manifest.gen.json",
262
+ false,
263
+ ),
264
+ mergeTree: c?.mergeTree,
265
+ openApi,
266
+ sdk,
267
+ tree: resolvePathFlag(c?.tree, "src/_gen/routes.gen.ts", true),
268
+ types,
269
+ },
270
+ watch: raw.watch ?? [],
271
+ };
272
+ }
273
+
274
+ /* ---- Config stash for CLI ---- */
275
+
276
+ let _lastConfig: HoneyVitePluginConfig | undefined;
277
+
278
+ export function getLastHoneyConfig(): HoneyVitePluginConfig | undefined {
279
+ return _lastConfig;
280
+ }
281
+
282
+ /* ---- writeGenFile / writeGenJsonFile ---- */
283
+
284
+ function writeGenFile(filePath: string, body: string, generator: string): boolean {
285
+ const normalized = body.replace(/\n*$/, "\n");
286
+ const hash = createHash("sha256").update(normalized).digest("hex").slice(0, 12);
287
+
288
+ const ext = filePath.endsWith(".d.ts") ? ".d.ts" : extname(filePath);
289
+ const header = ext === ".sql"
290
+ ? `-- @generated by ${generator} — do not edit. checksum: ${hash}`
291
+ : `/* @generated by ${generator} — do not edit. checksum: ${hash} */`;
292
+ const fullContent = `${header}\n${normalized}`;
293
+
294
+ if (existsSync(filePath)) {
295
+ const existing = readFileSync(filePath, "utf-8");
296
+ const firstLine = existing.split("\n")[0] ?? "";
297
+ const match = firstLine.match(/checksum: ([0-9a-f]{12})/);
298
+ if (match?.[1] === hash) return false;
299
+ }
300
+
301
+ mkdirSync(dirname(filePath), { recursive: true });
302
+ writeFileSync(filePath, fullContent, "utf-8");
303
+ return true;
304
+ }
305
+
306
+ function writeGenJsonFile(filePath: string, data: unknown, generator: string): boolean {
307
+ const json = JSON.stringify(data, null, 2);
308
+ const hash = createHash("sha256").update(json).digest("hex").slice(0, 12);
309
+ const marker = `${generator} checksum:${hash}`;
310
+
311
+ if (existsSync(filePath)) {
312
+ try {
313
+ const existing = readFileSync(filePath, "utf-8");
314
+ const parsed = JSON.parse(existing) as Record<string, unknown>;
315
+ if (parsed._generated === marker) return false;
316
+ } catch {
317
+ /* invalid JSON — proceed to write */
318
+ }
319
+ }
320
+
321
+ const output = { _generated: marker, ...(data as Record<string, unknown>) };
322
+ mkdirSync(dirname(filePath), { recursive: true });
323
+ writeFileSync(filePath, `${JSON.stringify(output, null, 2)}\n`, "utf-8");
324
+ return true;
325
+ }
326
+
327
+ function writeGenYamlFile(filePath: string, data: unknown, generator: string): boolean {
328
+ const json = JSON.stringify(data, null, 2)
329
+ const hash = createHash("sha256").update(json).digest("hex").slice(0, 12)
330
+ const marker = `${generator} checksum:${hash}`
331
+ const output = { _generated: marker, ...(data as Record<string, unknown>) }
332
+ const yaml = toYaml(output)
333
+
334
+ if (existsSync(filePath)) {
335
+ const existing = readFileSync(filePath, "utf-8")
336
+ if (existing.includes(marker)) return false
337
+ }
338
+
339
+ mkdirSync(dirname(filePath), { recursive: true })
340
+ writeFileSync(filePath, yaml, "utf-8")
341
+ return true
342
+ }
343
+
344
+ /* ---- Loaders ---- */
345
+
346
+ type HoneyApp = Honey<
347
+ unknown,
348
+ unknown,
349
+ unknown,
350
+ unknown,
351
+ unknown,
352
+ string,
353
+ string
354
+ >;
355
+ type TreeResult = { root: import("./tree.ts").TreeNode };
356
+
357
+ async function loadDefaultWithJiti(entryPath: string): Promise<unknown> {
358
+ const { createJiti } = await import("jiti");
359
+ const jiti = createJiti(entryPath, { fsCache: false, interopDefault: true, moduleCache: false });
360
+ const mod = (await jiti.import(entryPath)) as Record<string, unknown>;
361
+
362
+ /* prefer named exports, then unwrap interop default */
363
+ if (mod.app && isHoneyApp(mod.app)) return mod.app;
364
+ if (mod.tree && isRouteTree(mod.tree)) return mod.tree;
365
+ if (mod.default) {
366
+ const def = mod.default as Record<string, unknown>;
367
+ /* interopDefault may wrap: { default: { app: Honey } } */
368
+ if (isHoneyApp(def)) return def;
369
+ if (isRouteTree(def)) return def;
370
+ if (def.app && isHoneyApp(def.app)) return def.app;
371
+ if (def.tree && isRouteTree(def.tree)) return def.tree;
372
+ }
373
+ return undefined;
374
+ }
375
+
376
+ function isHoneyApp(val: unknown): val is HoneyApp {
377
+ if (val === null || typeof val !== "object") return false;
378
+ return typeof (val as Record<string, unknown>).fetch === "function";
379
+ }
380
+
381
+ function isRouteTree(val: unknown): val is TreeResult {
382
+ if (val === null || typeof val !== "object") return false;
383
+ const obj = val as Record<string, unknown>;
384
+ return obj.root !== undefined && typeof obj.root === "object";
385
+ }
386
+
387
+ /* ---- Generation ---- */
388
+
389
+ export async function generateAndWrite(
390
+ config: ResolvedHoneyConfig,
391
+ root: string,
392
+ ): Promise<void> {
393
+ await prepareCodegen()
394
+ const cg = config.codegen;
395
+
396
+ /* phase 1: route tree — from mergeTree source or app */
397
+ if (cg.tree && (cg.mergeTree || config.app)) {
398
+ const treeSrc = resolve(root, cg.mergeTree ?? config.app ?? "");
399
+ const exported = await loadDefaultWithJiti(treeSrc);
400
+
401
+ let treeCode: string;
402
+ if (isHoneyApp(exported)) {
403
+ treeCode = generateRouteTreeFromApp(exported);
404
+ } else if (isRouteTree(exported)) {
405
+ treeCode = generateRouteTreeFromRouteTree(exported);
406
+ } else {
407
+ throw new Error(
408
+ `Expected Honey app or RouteTree default export in ${treeSrc}`,
409
+ );
410
+ }
411
+
412
+ writeGenFile(resolve(root, cg.tree), treeCode, "honey");
413
+ }
414
+
415
+ /* phase 2: types, manifest, openapi, sdk, cli — app needed unless sdk has specs */
416
+ const sdkHasSpecs = cg.sdk && cg.sdk.specs && cg.sdk.specs.length > 0
417
+ const needsApp = cg.types || cg.manifest || cg.openApi || (cg.sdk && !sdkHasSpecs) || cg.cli;
418
+
419
+ let app: HoneyApp | undefined
420
+ let appPath: string | undefined
421
+ if (needsApp) {
422
+ if (!config.app) {
423
+ throw new Error("No app configured — required for types/manifest/openapi/sdk without specs/cli")
424
+ }
425
+ appPath = resolve(root, config.app);
426
+ const appExported = await loadDefaultWithJiti(appPath);
427
+ if (!isHoneyApp(appExported)) {
428
+ throw new Error(`Expected Honey app default export in ${appPath}`);
429
+ }
430
+ app = appExported;
431
+ }
432
+
433
+ /* manifest */
434
+ if (cg.manifest) {
435
+ if (!app) throw new Error("Manifest generation requires a configured app")
436
+ const manifest = generateManifest(app);
437
+ writeGenJsonFile(resolve(root, cg.manifest), manifest, "honey");
438
+ }
439
+
440
+ /* openapi */
441
+ if (cg.openApi) {
442
+ if (!app) throw new Error("OpenAPI generation requires a configured app")
443
+ for (const entry of cg.openApi) {
444
+ let spec = await generateOpenApi(app, {
445
+ filterRoutes: entry.filterRoutes,
446
+ info: {
447
+ description: entry.description,
448
+ title: entry.title,
449
+ version: entry.version,
450
+ },
451
+ securitySchemes: entry.securitySchemes,
452
+ });
453
+ if (entry.sanitize) {
454
+ spec = sanitizeOpenApiSpec(spec, entry.sanitize);
455
+ }
456
+ const jsonPath = resolve(root, entry.path)
457
+ writeGenJsonFile(jsonPath, spec, "honey")
458
+ writeGenYamlFile(yamlSiblingPath(jsonPath), spec, "honey")
459
+ }
460
+ }
461
+
462
+ /* types */
463
+ if (cg.types) {
464
+ const typesPath = resolve(root, cg.types.path);
465
+ const typesDir = dirname(typesPath);
466
+
467
+ if (!appPath) {
468
+ throw new Error("Type generation requires a configured app")
469
+ }
470
+ const { extractChainTypes } = await import("./type-extractor.ts").catch(
471
+ () => {
472
+ throw new Error(
473
+ 'Type generation requires "ts-morph" — install it as a dev dependency',
474
+ );
475
+ },
476
+ );
477
+
478
+ /* prefer appBase (middleware-only, simpler type) over app (with routes) */
479
+ let extracted: ExtractedChainTypes;
480
+ try {
481
+ extracted = await extractChainTypes({
482
+ entryPath: appPath,
483
+ exportName: "appBase",
484
+ outputDir: typesDir,
485
+ });
486
+ } catch {
487
+ extracted = await extractChainTypes({
488
+ entryPath: appPath,
489
+ exportName: "app",
490
+ outputDir: typesDir,
491
+ });
492
+ }
493
+
494
+ if (!app) throw new Error("Type generation requires a configured app")
495
+ const typesCode = generateTypes(app, {
496
+ baseCtxName: cg.types.baseCtxName,
497
+ inlineEnvType: extracted.base.envType,
498
+ inlineMiddlewareType: extracted.base.middlewareType,
499
+ inlineTapsType: extracted.base.tapsType,
500
+ routeMiddleware: extracted.routeMiddleware,
501
+ routeMiddlewareProps: extracted.routeMiddlewareProps,
502
+ });
503
+ writeGenFile(typesPath, typesCode, "honey");
504
+ }
505
+
506
+ /* sdk */
507
+ if (cg.sdk) {
508
+ const { generateOpenApi: genOA, generateSDK: genSDK, mergeSpecs } = await import(
509
+ "./codegen.ts"
510
+ );
511
+
512
+ let spec: OpenApiSpecInput
513
+ if (cg.sdk.specs && cg.sdk.specs.length > 0) {
514
+ const specs = cg.sdk.specs.map((s) => {
515
+ const abs = resolve(root, s)
516
+ return JSON.parse(readFileSync(abs, "utf-8")) as OpenApiSpecInput
517
+ })
518
+ spec = mergeSpecs(...specs)
519
+ } else {
520
+ if (!app) throw new Error("SDK generation requires a configured app or specs")
521
+ const primaryOA = cg.openApi && cg.openApi.length > 0 ? cg.openApi[0] : null
522
+ const info = primaryOA
523
+ ? { title: primaryOA.title, version: primaryOA.version }
524
+ : { title: "API", version: "1.0.0" };
525
+ spec = await genOA(app, { info });
526
+ }
527
+
528
+ const ports = cg.sdk.ports
529
+
530
+ if (ports.typescript) {
531
+ const tsOutDir = resolve(root, ports.typescript.outDir)
532
+ const { files } = genSDK(spec, { name: cg.sdk.name, stem: "sdk" })
533
+ writeGenFile(resolve(tsOutDir, "sdk.types.gen.ts"), files.types, "honey")
534
+ writeGenFile(resolve(tsOutDir, "sdk.map.gen.ts"), files.map, "honey")
535
+ writeGenFile(resolve(tsOutDir, "sdk.client.gen.ts"), files.client, "honey")
536
+ writeGenFile(resolve(tsOutDir, "sdk.index.gen.ts"), files.index, "honey")
537
+ if (files.runtime) {
538
+ writeGenFile(resolve(tsOutDir, "sdk.runtime.gen.ts"), files.runtime, "honey")
539
+ }
540
+ }
541
+
542
+ if (ports.python) {
543
+ const { generatePythonSDK } = await import("./codegen-python.ts")
544
+ const { files } = generatePythonSDK(spec, { name: cg.sdk.name })
545
+ const outDir = resolve(root, ports.python.outDir)
546
+ for (const [filename, content] of Object.entries(files)) {
547
+ const absPath = resolve(outDir, filename)
548
+ mkdirSync(dirname(absPath), { recursive: true })
549
+ writeFileSync(absPath, content, "utf-8")
550
+ }
551
+ }
552
+
553
+ if (ports.go) {
554
+ const { generateGoSDK } = await import("./codegen-go.ts")
555
+ const { files } = generateGoSDK(spec, { modulePath: ports.go.modulePath })
556
+ const outDir = resolve(root, ports.go.outDir)
557
+ for (const [filename, content] of Object.entries(files)) {
558
+ const absPath = resolve(outDir, filename)
559
+ mkdirSync(dirname(absPath), { recursive: true })
560
+ writeFileSync(absPath, content, "utf-8")
561
+ }
562
+ }
563
+
564
+ if (ports.rust) {
565
+ const { generateRustSDK } = await import("./codegen-rust.ts")
566
+ const { files } = generateRustSDK(spec, { crateName: ports.rust.crateName })
567
+ const outDir = resolve(root, ports.rust.outDir)
568
+ for (const [relPath, content] of Object.entries(files)) {
569
+ const absPath = resolve(outDir, relPath)
570
+ mkdirSync(dirname(absPath), { recursive: true })
571
+ writeFileSync(absPath, content, "utf-8")
572
+ }
573
+ }
574
+ }
575
+
576
+ /* cli */
577
+ if (cg.cli) {
578
+ const { generateGoCLI } = await import("./codegen-go-cli.ts")
579
+
580
+ const primaryOA = cg.openApi && cg.openApi.length > 0 ? cg.openApi[0] : null
581
+ const info = primaryOA
582
+ ? { title: primaryOA.title, version: primaryOA.version }
583
+ : { title: "API", version: "1.0.0" }
584
+ if (!app) throw new Error("CLI generation requires a configured app")
585
+ const spec = await generateOpenApi(app, { info })
586
+
587
+ const { files } = generateGoCLI(spec, {
588
+ binaryName: cg.cli.binaryName,
589
+ configName: cg.cli.configName,
590
+ defaultBaseURL: cg.cli.defaultBaseURL,
591
+ envPrefix: cg.cli.envPrefix,
592
+ modulePath: cg.cli.modulePath,
593
+ sdkModulePath: cg.cli.sdkModulePath,
594
+ })
595
+
596
+ const outDir = resolve(root, cg.cli.out)
597
+ for (const [relPath, content] of Object.entries(files)) {
598
+ const absPath = resolve(outDir, relPath)
599
+ mkdirSync(dirname(absPath), { recursive: true })
600
+ writeFileSync(absPath, content, "utf-8")
601
+ }
602
+ }
603
+ }
604
+
605
+ /* ---- Vite plugin ---- */
606
+
607
+ const VIRTUAL_ROUTES = "virtual:honey/routes";
608
+ const VIRTUAL_MANIFEST = "virtual:honey/manifest";
609
+ const VIRTUAL_OPENAPI = "virtual:honey/openapi";
610
+ const RESOLVED_ROUTES = `\0${VIRTUAL_ROUTES}`;
611
+ const RESOLVED_MANIFEST = `\0${VIRTUAL_MANIFEST}`;
612
+ const RESOLVED_OPENAPI = `\0${VIRTUAL_OPENAPI}`;
613
+
614
+ function matchesGlob(filePath: string, patterns: string[]): boolean {
615
+ for (const pattern of patterns) {
616
+ const re = pattern
617
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
618
+ .replace(/\*\*\//g, "(.+/)?")
619
+ .replace(/\*/g, "[^/]*");
620
+ if (new RegExp(`(^|/)${re}$`).test(filePath)) return true;
621
+ }
622
+ return false;
623
+ }
624
+
625
+ export function honey(config: HoneyVitePluginConfig) {
626
+ _lastConfig = config;
627
+
628
+ const resolved = resolveHoneyConfig(config);
629
+
630
+ let root = "";
631
+
632
+ const plugin: {
633
+ buildStart(): Promise<void>;
634
+ configResolved(cfg: { root: string }): void;
635
+ hotUpdate(ctx: {
636
+ file: string;
637
+ modules: unknown[];
638
+ server: {
639
+ moduleGraph: { getModuleById(id: string): unknown };
640
+ reloadModule(mod: unknown): void;
641
+ };
642
+ }): Promise<unknown[] | undefined>;
643
+ load(id: string): Promise<{ code: string; moduleType: string } | undefined>;
644
+ name: string;
645
+ resolveId(id: string): string | undefined;
646
+ transform(code: string, id: string): { code: string; map: null } | undefined;
647
+ } = {
648
+ async buildStart() {
649
+ await generateAndWrite(resolved, root);
650
+ },
651
+
652
+ configResolved(cfg: { root: string }) {
653
+ root = cfg.root;
654
+ },
655
+
656
+ async hotUpdate(ctx: {
657
+ file: string;
658
+ modules: unknown[];
659
+ server: {
660
+ moduleGraph: { getModuleById(id: string): unknown };
661
+ reloadModule(mod: unknown): void;
662
+ };
663
+ }) {
664
+ const patterns = config.watch;
665
+ if (!patterns || patterns.length === 0) return;
666
+ if (!matchesGlob(ctx.file, patterns)) return;
667
+
668
+ await generateAndWrite(resolved, root);
669
+
670
+ const mod = ctx.server.moduleGraph.getModuleById(RESOLVED_ROUTES);
671
+ if (mod) {
672
+ ctx.server.reloadModule(mod);
673
+ }
674
+ return [];
675
+ },
676
+
677
+ async load(
678
+ id: string,
679
+ ): Promise<{ code: string; moduleType: string } | undefined> {
680
+ if (id === RESOLVED_ROUTES) {
681
+ if (!resolved.codegen.mergeTree && !config.app) {
682
+ throw new Error("Route tree virtual module requires app or mergeTree")
683
+ }
684
+ const treeSrc = resolved.codegen.mergeTree
685
+ ? resolve(root, resolved.codegen.mergeTree)
686
+ : resolve(root, config.app ?? "");
687
+ const exported = await loadDefaultWithJiti(treeSrc);
688
+ let code: string;
689
+ if (isHoneyApp(exported)) {
690
+ code = generateRouteTreeFromApp(exported);
691
+ } else if (isRouteTree(exported)) {
692
+ code = generateRouteTreeFromRouteTree(exported);
693
+ } else {
694
+ throw new Error(`Expected Honey app or RouteTree in ${treeSrc}`);
695
+ }
696
+ return { code, moduleType: "js" };
697
+ }
698
+ if (id === RESOLVED_MANIFEST && resolved.codegen.manifest) {
699
+ if (!config.app) throw new Error("Manifest virtual module requires app")
700
+ const appPath = resolve(root, config.app);
701
+ const exported = await loadDefaultWithJiti(appPath);
702
+ if (isHoneyApp(exported)) {
703
+ const manifest = generateManifest(exported);
704
+ return {
705
+ code: `export default ${JSON.stringify(manifest, null, 2)};`,
706
+ moduleType: "js",
707
+ };
708
+ }
709
+ }
710
+ if (id === RESOLVED_OPENAPI && resolved.codegen.openApi) {
711
+ const primary = resolved.codegen.openApi[0]
712
+ if (!config.app) throw new Error("OpenAPI virtual module requires app")
713
+ const appPath = resolve(root, config.app);
714
+ const exported = await loadDefaultWithJiti(appPath);
715
+ if (isHoneyApp(exported)) {
716
+ let spec = await generateOpenApi(exported, {
717
+ filterRoutes: primary.filterRoutes,
718
+ info: {
719
+ description: primary.description,
720
+ title: primary.title,
721
+ version: primary.version,
722
+ },
723
+ securitySchemes: primary.securitySchemes,
724
+ });
725
+ if (primary.sanitize) {
726
+ spec = sanitizeOpenApiSpec(spec, primary.sanitize);
727
+ }
728
+ return {
729
+ code: `export default ${JSON.stringify(spec, null, 2)};`,
730
+ moduleType: "js",
731
+ };
732
+ }
733
+ }
734
+ return undefined;
735
+ },
736
+
737
+ name: "honey",
738
+
739
+ transform(code: string, id: string): { code: string; map: null } | undefined {
740
+ if (id.includes("\0") || id.includes("node_modules")) return undefined
741
+ const inject: string[] = []
742
+ if (/\.openapi\s*\(|\.manifest\s*\(/.test(code) && !code.includes("@lovrozagar/honey/openapi")) {
743
+ inject.push('import { enableOpenApi } from "@lovrozagar/honey/openapi"', "enableOpenApi()")
744
+ }
745
+ if (
746
+ /(?<!Bun)(?<!Deno)\.serve\s*\(/.test(code) &&
747
+ !code.includes("@lovrozagar/honey/serve")
748
+ ) {
749
+ inject.push('import { enableServe } from "@lovrozagar/honey/serve"', "enableServe()")
750
+ }
751
+ if (/\.errorI18n\s*\(/.test(code) && !code.includes("@lovrozagar/honey/i18n")) {
752
+ inject.push('import { enableI18n } from "@lovrozagar/honey/i18n"', "enableI18n()")
753
+ }
754
+ if (inject.length === 0) return undefined
755
+ return { code: `${inject.join("\n")}\n${code}`, map: null }
756
+ },
757
+
758
+ resolveId(id: string): string | undefined {
759
+ if (id === VIRTUAL_ROUTES) return RESOLVED_ROUTES;
760
+ if (id === VIRTUAL_MANIFEST && resolved.codegen.manifest)
761
+ return RESOLVED_MANIFEST;
762
+ if (id === VIRTUAL_OPENAPI && resolved.codegen.openApi)
763
+ return RESOLVED_OPENAPI;
764
+ return undefined;
765
+ },
766
+ };
767
+
768
+ return [plugin];
769
+ }
770
+
771
+ export { generateRouteTree };