@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
@@ -0,0 +1,235 @@
1
+ import type { MiddlewareFn } from "./middleware.ts"
2
+ import type { LoggerInstance } from "./logger.ts"
3
+
4
+ type BodyOmittedReason =
5
+ | "content-type"
6
+ | "disabled"
7
+ | "missing"
8
+ | "read-error"
9
+ | "too-large"
10
+
11
+ type CurlLogData = {
12
+ bodyIncluded: boolean
13
+ bodyOmittedReason: BodyOmittedReason | null
14
+ curl: string
15
+ duration: number
16
+ method: string
17
+ path: string
18
+ requestId: string | null
19
+ status: number
20
+ }
21
+
22
+ type CurlLoggerBodyOptions = {
23
+ allowContentTypes?: string[]
24
+ maxBytes?: number
25
+ }
26
+
27
+ type CurlLoggerOptions = {
28
+ body?: boolean | CurlLoggerBodyOptions
29
+ instance?: LoggerInstance
30
+ log?: (data: CurlLogData) => void
31
+ redactHeader?: (name: string, value: string) => string | null
32
+ redactQueryParam?: (name: string, value: string) => string | null
33
+ skip?: (data: CurlLogData) => boolean
34
+ }
35
+
36
+ const DEFAULT_BODY_CONTENT_TYPES = [
37
+ "application/json",
38
+ "application/x-www-form-urlencoded",
39
+ "application/xml",
40
+ "text/",
41
+ ]
42
+
43
+ const DEFAULT_MAX_BODY_BYTES = 16_384
44
+
45
+ function shellEscape(value: string): string {
46
+ return value.replace(/'/g, "'\\''")
47
+ }
48
+
49
+ function defaultLog(data: CurlLogData): void {
50
+ console.log(data.curl)
51
+ }
52
+
53
+ function shouldIncludeBody(contentType: string | null, allowContentTypes: string[]): boolean {
54
+ if (contentType === null || contentType.length === 0) return false
55
+ return allowContentTypes.some((allowed) => contentType.startsWith(allowed))
56
+ }
57
+
58
+ function redactUrl(url: URL, redactQueryParam?: (name: string, value: string) => string | null): string {
59
+ if (redactQueryParam === undefined || url.search.length === 0) return url.toString()
60
+
61
+ const redacted = new URL(url.toString())
62
+ redacted.search = ""
63
+
64
+ for (const [name, value] of url.searchParams.entries()) {
65
+ const nextValue = redactQueryParam(name, value)
66
+ if (nextValue === null) continue
67
+ redacted.searchParams.append(name, nextValue)
68
+ }
69
+
70
+ return redacted.toString()
71
+ }
72
+
73
+ async function readBodyWithinLimit(request: Request, maxBytes: number): Promise<{
74
+ body: string | null
75
+ omittedReason: BodyOmittedReason | null
76
+ }> {
77
+ if (request.body === null) {
78
+ return { body: null, omittedReason: "missing" }
79
+ }
80
+
81
+ const contentLength = request.headers.get("content-length")
82
+ if (contentLength !== null) {
83
+ const parsedLength = Number.parseInt(contentLength, 10)
84
+ if (!Number.isNaN(parsedLength) && parsedLength > maxBytes) {
85
+ return { body: null, omittedReason: "too-large" }
86
+ }
87
+ }
88
+
89
+ const clone = request.clone()
90
+ if (clone.body === null) {
91
+ return { body: null, omittedReason: "missing" }
92
+ }
93
+
94
+ const reader = clone.body.getReader()
95
+ const chunks: Uint8Array[] = []
96
+ let totalBytes = 0
97
+
98
+ try {
99
+ while (true) {
100
+ const { done, value } = await reader.read()
101
+ if (done) break
102
+ totalBytes += value.byteLength
103
+ if (totalBytes > maxBytes) {
104
+ await reader.cancel()
105
+ return { body: null, omittedReason: "too-large" }
106
+ }
107
+ chunks.push(value)
108
+ }
109
+ } catch {
110
+ return { body: null, omittedReason: "read-error" }
111
+ }
112
+
113
+ const bytes = new Uint8Array(totalBytes)
114
+ let offset = 0
115
+
116
+ for (const chunk of chunks) {
117
+ bytes.set(chunk, offset)
118
+ offset += chunk.byteLength
119
+ }
120
+
121
+ return {
122
+ body: new TextDecoder().decode(bytes),
123
+ omittedReason: null,
124
+ }
125
+ }
126
+
127
+ async function buildCurlLogData(
128
+ request: Request,
129
+ options?: Pick<CurlLoggerOptions, "body" | "redactHeader" | "redactQueryParam">,
130
+ ): Promise<Pick<CurlLogData, "bodyIncluded" | "bodyOmittedReason" | "curl">> {
131
+ const parts: string[] = [`curl -X ${request.method}`]
132
+
133
+ for (const [name, value] of request.headers.entries()) {
134
+ const redactedValue = options?.redactHeader !== undefined
135
+ ? options.redactHeader(name, value)
136
+ : value
137
+ if (redactedValue === null) continue
138
+ parts.push(`-H '${shellEscape(name)}: ${shellEscape(redactedValue)}'`)
139
+ }
140
+
141
+ let bodyIncluded = false
142
+ let bodyOmittedReason: BodyOmittedReason | null = null
143
+
144
+ if (options?.body === true || typeof options?.body === "object") {
145
+ const bodyOptions = typeof options.body === "object" ? options.body : {}
146
+ const allowContentTypes = bodyOptions.allowContentTypes ?? DEFAULT_BODY_CONTENT_TYPES
147
+ const contentType = request.headers.get("content-type")
148
+
149
+ if (shouldIncludeBody(contentType, allowContentTypes) === false) {
150
+ bodyOmittedReason = request.body === null ? "missing" : "content-type"
151
+ } else {
152
+ const result = await readBodyWithinLimit(
153
+ request,
154
+ bodyOptions.maxBytes ?? DEFAULT_MAX_BODY_BYTES,
155
+ )
156
+
157
+ if (result.body === null) {
158
+ bodyOmittedReason = result.omittedReason
159
+ } else {
160
+ bodyIncluded = true
161
+ parts.push(`--data-raw '${shellEscape(result.body)}'`)
162
+ }
163
+ }
164
+ } else {
165
+ bodyOmittedReason = "disabled"
166
+ }
167
+
168
+ parts.push(`'${shellEscape(redactUrl(new URL(request.url), options?.redactQueryParam))}'`)
169
+
170
+ return {
171
+ bodyIncluded,
172
+ bodyOmittedReason,
173
+ curl: parts.join(" "),
174
+ }
175
+ }
176
+
177
+ function curlLogger(
178
+ options?: CurlLoggerOptions,
179
+ ): MiddlewareFn<{ path: string, req: Request }, {}> {
180
+ const log = options?.log ?? defaultLog
181
+ const skip = options?.skip
182
+
183
+ return async (ctx, next) => {
184
+ const start = performance.now()
185
+ const method = ctx.req.method
186
+ const path = ctx.path
187
+ const rid = (ctx as Record<string, unknown>)["requestId"] as string | null ?? null
188
+ const curlDataPromise = buildCurlLogData(ctx.req, options)
189
+
190
+ const response = await next()
191
+ const duration = performance.now() - start
192
+ const curlData = await curlDataPromise
193
+
194
+ const data: CurlLogData = {
195
+ ...curlData,
196
+ duration,
197
+ method,
198
+ path,
199
+ requestId: rid,
200
+ status: response.status,
201
+ }
202
+
203
+ if (skip?.(data)) {
204
+ return response
205
+ }
206
+
207
+ if (options?.instance) {
208
+ options.instance.info(
209
+ {
210
+ bodyIncluded: data.bodyIncluded,
211
+ bodyOmittedReason: data.bodyOmittedReason,
212
+ curl: data.curl,
213
+ duration: data.duration,
214
+ method: data.method,
215
+ path: data.path,
216
+ requestId: data.requestId,
217
+ status: data.status,
218
+ },
219
+ "request curl",
220
+ )
221
+ } else {
222
+ log(data)
223
+ }
224
+
225
+ return response
226
+ }
227
+ }
228
+
229
+ export { buildCurlLogData, curlLogger }
230
+ export type {
231
+ BodyOmittedReason,
232
+ CurlLogData,
233
+ CurlLoggerBodyOptions,
234
+ CurlLoggerOptions,
235
+ }
@@ -0,0 +1,19 @@
1
+ export type ServeRuntime = "bun" | "cloudflare" | "deno" | "node"
2
+
3
+ type DetectGlobal = {
4
+ Bun?: unknown
5
+ Deno?: unknown
6
+ navigator?: { userAgent?: string }
7
+ process?: { versions?: { node?: string } }
8
+ }
9
+
10
+ export function detectRuntime(g: DetectGlobal = globalThis): ServeRuntime {
11
+ if (typeof g.Bun !== "undefined") return "bun"
12
+ if (typeof g.Deno !== "undefined") return "deno"
13
+ const ua = g.navigator?.userAgent ?? ""
14
+ if (ua.includes("Cloudflare-Workers") || ua.includes("workerd")) return "cloudflare"
15
+ if (g.process?.versions?.node) return "node"
16
+ throw new Error(
17
+ 'Honey.serve() could not detect a runtime. Pass runtime: "bun" | "node" | "deno".',
18
+ )
19
+ }
package/src/error.ts ADDED
@@ -0,0 +1,52 @@
1
+ import type { FieldError, StatusKey } from "./types.ts"
2
+ import { EMPTY_FIELDS, statusKeyToCode } from "./types.ts"
3
+
4
+ export class HoneyError extends Error {
5
+ readonly data?: unknown
6
+ readonly errorKey: string
7
+ readonly fields: Record<string, FieldError[]>
8
+ readonly headers?: Record<string, string>
9
+ readonly status: number
10
+ readonly statusKey: StatusKey
11
+ readonly vars?: Record<string, string | number>
12
+
13
+ constructor(opts: {
14
+ cause?: unknown
15
+ data?: unknown
16
+ errorKey: string
17
+ fields?: Record<string, FieldError[]>
18
+ headers?: Record<string, string>
19
+ status: StatusKey
20
+ vars?: Record<string, string | number>
21
+ }) {
22
+ super(opts.errorKey, opts.cause !== undefined ? { cause: opts.cause } : undefined)
23
+ this.data = opts.data
24
+ this.errorKey = opts.errorKey
25
+ this.headers = opts.headers
26
+ this.statusKey = opts.status
27
+ this.status = statusKeyToCode[opts.status]
28
+ this.fields = opts.fields ?? EMPTY_FIELDS
29
+ this.vars = opts.vars
30
+ }
31
+
32
+ static serialize(e: unknown): Record<string, unknown> {
33
+ if (!(e instanceof Error)) return { message: String(e) }
34
+
35
+ const obj: Record<string, unknown> = {
36
+ message: e.message,
37
+ name: e.name,
38
+ stack: e.stack,
39
+ }
40
+
41
+ if (e instanceof HoneyError) {
42
+ obj["errorKey"] = e.errorKey
43
+ obj["status"] = e.status
44
+ obj["statusKey"] = e.statusKey
45
+ if (Object.keys(e.fields).length > 0) obj["fields"] = e.fields
46
+ }
47
+
48
+ if (e.cause) obj["cause"] = HoneyError.serialize(e.cause)
49
+
50
+ return obj
51
+ }
52
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,87 @@
1
+ import { HoneyError } from "./error.ts"
2
+ import type { ExtractICUVars } from "./i18n.ts"
3
+ import type { FieldError, InferOutput, StandardSchemaLike, StatusKey } from "./types.ts"
4
+
5
+ type ErrorFactoryOpts<TVars> = {
6
+ cause?: unknown
7
+ fields?: Record<string, FieldError[]>
8
+ headers?: Record<string, string>
9
+ vars?: TVars
10
+ }
11
+
12
+ type ErrorDefWithSchema = {
13
+ schema: StandardSchemaLike
14
+ status: StatusKey
15
+ }
16
+
17
+ type ErrorDef = ErrorDefWithSchema | StatusKey
18
+
19
+ export type ErrorMetaEntry = {
20
+ schema: StandardSchemaLike | null
21
+ statusKey: StatusKey
22
+ }
23
+
24
+ /** Per-key meta — keeps schema + statusKey literals for ComputeErrorsByStatus */
25
+ type ErrorMetaForDef<D extends ErrorDef> = D extends ErrorDefWithSchema
26
+ ? { schema: D["schema"]; statusKey: D["status"] }
27
+ : D extends StatusKey
28
+ ? { schema: null; statusKey: D }
29
+ : ErrorMetaEntry
30
+
31
+ export const ERROR_META: unique symbol = Symbol.for("honey.error.meta")
32
+
33
+ type ErrorFactoryResult<TMap extends Record<string, ErrorDef>, TTranslations extends Record<string, string>> = {
34
+ [K in keyof TMap]: TMap[K] extends ErrorDefWithSchema
35
+ ? (data: InferOutput<TMap[K]["schema"]>, opts?: { cause?: unknown; headers?: Record<string, string> }) => HoneyError
36
+ : K extends keyof TTranslations
37
+ ? (opts?: ErrorFactoryOpts<ExtractICUVars<TTranslations[K]>>) => HoneyError
38
+ : (opts?: ErrorFactoryOpts<Record<string, string | number>>) => HoneyError
39
+ } & {
40
+ readonly [ERROR_META]: { [K in keyof TMap]: ErrorMetaForDef<TMap[K]> }
41
+ }
42
+
43
+ export function defineErrors<
44
+ TMap extends Record<string, ErrorDef>,
45
+ TTranslations extends Record<string, string> = Record<string, string>,
46
+ >(
47
+ map: TMap,
48
+ ): ErrorFactoryResult<TMap, TTranslations> {
49
+ const result = Object.create(null) as Record<string, unknown>
50
+ const meta = Object.create(null) as Record<string, ErrorMetaEntry>
51
+
52
+ for (const [key, def] of Object.entries(map)) {
53
+ if (typeof def === "string") {
54
+ /* standard error — string status key */
55
+ meta[key] = { schema: null, statusKey: def }
56
+ result[key] = (opts?: ErrorFactoryOpts<Record<string, string | number>>) =>
57
+ new HoneyError({
58
+ cause: opts?.cause,
59
+ errorKey: key,
60
+ fields: opts?.fields,
61
+ headers: opts?.headers,
62
+ status: def,
63
+ vars: opts?.vars,
64
+ })
65
+ } else {
66
+ /* custom schema error — { status, schema } */
67
+ meta[key] = { schema: def.schema, statusKey: def.status }
68
+ result[key] = (data: unknown, opts?: { cause?: unknown; headers?: Record<string, string> }) =>
69
+ new HoneyError({
70
+ cause: opts?.cause,
71
+ data,
72
+ errorKey: key,
73
+ headers: opts?.headers,
74
+ status: def.status,
75
+ })
76
+ }
77
+ }
78
+
79
+ Object.defineProperty(result, ERROR_META, {
80
+ configurable: false,
81
+ enumerable: false,
82
+ value: meta,
83
+ writable: false,
84
+ })
85
+
86
+ return result as ErrorFactoryResult<TMap, TTranslations>
87
+ }
package/src/etag.ts ADDED
@@ -0,0 +1,74 @@
1
+ import { replaceResponse } from "./honey-response.ts"
2
+ import type { MiddlewareFn } from "./middleware.ts"
3
+
4
+ type ETagOptions = {
5
+ weak?: boolean
6
+ }
7
+
8
+ /**
9
+ * FNV-1a 32-bit hash — fast, non-cryptographic, deterministic.
10
+ * Good enough for ETag fingerprinting (not security-critical).
11
+ */
12
+ function fnv1a(data: Uint8Array): string {
13
+ let hash = 0x811c9dc5
14
+ for (let i = 0; i < data.length; i++) {
15
+ hash ^= data[i]
16
+ hash = Math.imul(hash, 0x01000193)
17
+ }
18
+ return (hash >>> 0).toString(16).padStart(8, "0")
19
+ }
20
+
21
+ export function etag(options?: ETagOptions): MiddlewareFn<{ req: Request }, {}> {
22
+ const weak = options?.weak !== false
23
+
24
+ const mw: MiddlewareFn<{ req: Request }, {}> = async (ctx, next) => {
25
+ const method = ctx.req.method
26
+ if (method !== "GET" && method !== "HEAD") {
27
+ return next()
28
+ }
29
+
30
+ const response = await next()
31
+
32
+ /* skip error responses — caching errors is wasteful and misleading */
33
+ if (response.status >= 400) return response
34
+
35
+ /* skip streaming responses — can't hash without buffering entire stream */
36
+ if (
37
+ response.headers.get("transfer-encoding") === "chunked" ||
38
+ (response.body !== null &&
39
+ !response.headers.has("content-length") &&
40
+ !response.headers.has("content-type"))
41
+ ) {
42
+ return response
43
+ }
44
+
45
+ /* read body to compute hash */
46
+ const body = await response.arrayBuffer()
47
+ if (body.byteLength === 0) return response
48
+
49
+ const hash = fnv1a(new Uint8Array(body))
50
+ const etagValue = weak ? `W/"${hash}"` : `"${hash}"`
51
+
52
+ const headers = new Headers(response.headers)
53
+ headers.set("etag", etagValue)
54
+
55
+ /* check If-None-Match — supports comma-separated list and wildcard per RFC 7232 */
56
+ const ifNoneMatch = ctx.req.headers.get("if-none-match")
57
+ if (ifNoneMatch === "*" || ifNoneMatch?.split(",").some((v) => v.trim() === etagValue)) {
58
+ return replaceResponse(response, {
59
+ body: null,
60
+ headers,
61
+ status: 304,
62
+ })
63
+ }
64
+
65
+ /* return response with ETag header and reconstituted body */
66
+ return replaceResponse(response, {
67
+ body,
68
+ headers,
69
+ status: response.status,
70
+ })
71
+ }
72
+
73
+ return mw
74
+ }
@@ -0,0 +1,23 @@
1
+ import { getI18nRuntime } from "./i18n-slot.ts"
2
+ import { getOpenApiRuntime } from "./openapi/spec-factory.ts"
3
+ import { getServeRuntime } from "./serve-slot.ts"
4
+
5
+ function isReady(name: "i18n" | "openapi" | "serve"): boolean {
6
+ try {
7
+ if (name === "i18n") getI18nRuntime()
8
+ else if (name === "openapi") getOpenApiRuntime()
9
+ else getServeRuntime()
10
+ return true
11
+ } catch {
12
+ return false
13
+ }
14
+ }
15
+
16
+ /** Opaque so bun/esbuild do not follow unused feature entries into a fetch-only bundle. */
17
+ export async function loadHoneyFeature(name: "i18n" | "openapi" | "serve"): Promise<void> {
18
+ if (isReady(name)) return
19
+ const mod = (await import(["@lovrozagar/honey", name].join("/"))) as Record<string, unknown>
20
+ const enable = { i18n: "enableI18n", openapi: "enableOpenApi", serve: "enableServe" }[name]
21
+ const fn = mod[enable]
22
+ if (typeof fn === "function") fn()
23
+ }