@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.
- package/README.md +51 -0
- package/docs/api-ref/go.md +399 -0
- package/docs/api-ref/python.md +399 -0
- package/docs/api-ref/rust.md +399 -0
- package/docs/api-ref/typescript.md +399 -0
- package/docs/invalidate-spec.md +303 -0
- package/docs/sdk.md +722 -0
- package/package.json +120 -0
- package/src/accepts.ts +78 -0
- package/src/body-limit.ts +100 -0
- package/src/build/index.ts +174 -0
- package/src/cli-go/auth.go +15 -0
- package/src/cli-go/config.go +165 -0
- package/src/cli-go/errors.go +57 -0
- package/src/cli-go/multipart.go +55 -0
- package/src/cli-go/output.go +109 -0
- package/src/cli-go/root.go +30 -0
- package/src/cli-go/stream.go +88 -0
- package/src/cli-go/version.go +5 -0
- package/src/cli.ts +204 -0
- package/src/client/error.ts +27 -0
- package/src/client/http.ts +502 -0
- package/src/client/index.ts +127 -0
- package/src/client/sdk.ts +265 -0
- package/src/client/sse.ts +102 -0
- package/src/client/types.ts +211 -0
- package/src/client/ws.ts +115 -0
- package/src/client-go/errors.go +207 -0
- package/src/client-go/invalidation.go +373 -0
- package/src/client-go/realtime.go +317 -0
- package/src/client-go/result.go +20 -0
- package/src/client-go/runtime.go +413 -0
- package/src/client-go/sse.go +149 -0
- package/src/client-go/transport.go +452 -0
- package/src/client-go/ws.go +104 -0
- package/src/client-mcp/README.md +10 -0
- package/src/client-mcp/server.ts +94 -0
- package/src/client-mcp/types.ts +19 -0
- package/src/client-python/_errors.py +115 -0
- package/src/client-python/_invalidation.py +578 -0
- package/src/client-python/_realtime.py +156 -0
- package/src/client-python/_runtime.py +469 -0
- package/src/client-python/_sse.py +99 -0
- package/src/client-python/_transport.py +335 -0
- package/src/client-python/_ws.py +131 -0
- package/src/client-python/py.typed +0 -0
- package/src/client-rust/errors.rs +200 -0
- package/src/client-rust/invalidation.rs +441 -0
- package/src/client-rust/invalidation_sync.rs +306 -0
- package/src/client-rust/realtime.rs +695 -0
- package/src/client-rust/result.rs +22 -0
- package/src/client-rust/runtime.rs +547 -0
- package/src/client-rust/runtime_sync.rs +452 -0
- package/src/client-rust/sse.rs +167 -0
- package/src/client-rust/ws.rs +143 -0
- package/src/cloudflare/realtime.ts +236 -0
- package/src/codegen-go-cli.ts +1180 -0
- package/src/codegen-go.ts +1010 -0
- package/src/codegen-hash.ts +8 -0
- package/src/codegen-ir.ts +517 -0
- package/src/codegen-loaders.ts +53 -0
- package/src/codegen-mcp.ts +364 -0
- package/src/codegen-python.ts +1028 -0
- package/src/codegen-rust.ts +1652 -0
- package/src/codegen-sanitize.ts +67 -0
- package/src/codegen-schema-naming.ts +146 -0
- package/src/codegen.ts +5015 -0
- package/src/context.ts +210 -0
- package/src/cookie-sign.ts +58 -0
- package/src/cookie.ts +89 -0
- package/src/cors.ts +96 -0
- package/src/crypto.ts +19 -0
- package/src/csrf.ts +54 -0
- package/src/curl-logger.ts +235 -0
- package/src/detect-runtime.ts +19 -0
- package/src/error.ts +52 -0
- package/src/errors.ts +87 -0
- package/src/etag.ts +74 -0
- package/src/feature-load.ts +23 -0
- package/src/go-type-emitter.ts +404 -0
- package/src/honey-response.ts +354 -0
- package/src/i18n-slot.ts +20 -0
- package/src/i18n.ts +234 -0
- package/src/index.ts +3843 -0
- package/src/init.ts +149 -0
- package/src/input.ts +8 -0
- package/src/ip-restrict.ts +192 -0
- package/src/logger.ts +149 -0
- package/src/middleware.ts +199 -0
- package/src/node-request.ts +240 -0
- package/src/node.ts +212 -0
- package/src/openapi/register.ts +19 -0
- package/src/openapi/scalar.ts +33 -0
- package/src/openapi/spec-factory.ts +46 -0
- package/src/openapi/spec.ts +31 -0
- package/src/openapi/swagger.ts +34 -0
- package/src/plugin.ts +771 -0
- package/src/powered-by.ts +17 -0
- package/src/pretty-json.ts +39 -0
- package/src/proxy.ts +155 -0
- package/src/python-type-emitter.ts +144 -0
- package/src/realtime/buffer.ts +101 -0
- package/src/realtime/bus.ts +93 -0
- package/src/realtime/longpoll.ts +148 -0
- package/src/realtime/protocol.ts +128 -0
- package/src/realtime/route.ts +104 -0
- package/src/realtime/runtime.ts +430 -0
- package/src/request-id.ts +23 -0
- package/src/request-to-curl.ts +34 -0
- package/src/response.ts +414 -0
- package/src/rust-type-emitter.ts +412 -0
- package/src/secure-headers.ts +69 -0
- package/src/serve-register.ts +8 -0
- package/src/serve-slot.ts +20 -0
- package/src/serve.ts +141 -0
- package/src/server-timing.ts +66 -0
- package/src/static.ts +60 -0
- package/src/telemetry/otel.ts +139 -0
- package/src/testing.ts +114 -0
- package/src/timeout.ts +36 -0
- package/src/tree.ts +464 -0
- package/src/ts-type-emitter.ts +106 -0
- package/src/type-emitter.ts +524 -0
- package/src/type-extractor.ts +749 -0
- package/src/types.ts +638 -0
- package/src/validation.ts +300 -0
- package/src/ws/bun.ts +89 -0
- package/src/ws/cloudflare.ts +133 -0
- package/src/ws/deno.ts +109 -0
- package/src/ws/node.ts +161 -0
- package/src/yaml.ts +80 -0
package/package.json
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lovrozagar/honey",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Type-safe API framework built on Web Standards. Builder-pattern DX, precompiled radix tree in production.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"api",
|
|
7
|
+
"framework",
|
|
8
|
+
"honey",
|
|
9
|
+
"radix-tree",
|
|
10
|
+
"router",
|
|
11
|
+
"type-safe",
|
|
12
|
+
"web-standards"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/lovrozagar/honey"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"bin": {
|
|
23
|
+
"honey": "./src/cli.ts"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"docs",
|
|
27
|
+
"src"
|
|
28
|
+
],
|
|
29
|
+
"type": "module",
|
|
30
|
+
"sideEffects": [
|
|
31
|
+
"./src/i18n.ts",
|
|
32
|
+
"./src/openapi/register.ts",
|
|
33
|
+
"./src/serve-register.ts"
|
|
34
|
+
],
|
|
35
|
+
"exports": {
|
|
36
|
+
".": "./src/index.ts",
|
|
37
|
+
"./accepts": "./src/accepts.ts",
|
|
38
|
+
"./body-limit": "./src/body-limit.ts",
|
|
39
|
+
"./build": "./src/build/index.ts",
|
|
40
|
+
"./cli": "./src/cli.ts",
|
|
41
|
+
"./client": "./src/client/index.ts",
|
|
42
|
+
"./client/sdk": "./src/client/sdk.ts",
|
|
43
|
+
"./codegen": "./src/codegen.ts",
|
|
44
|
+
"./codegen-go-cli": "./src/codegen-go-cli.ts",
|
|
45
|
+
"./codegen/extract": "./src/type-extractor.ts",
|
|
46
|
+
"./cookie": "./src/cookie.ts",
|
|
47
|
+
"./cookie-sign": "./src/cookie-sign.ts",
|
|
48
|
+
"./cors": "./src/cors.ts",
|
|
49
|
+
"./crypto": "./src/crypto.ts",
|
|
50
|
+
"./curl-logger": "./src/curl-logger.ts",
|
|
51
|
+
"./csrf": "./src/csrf.ts",
|
|
52
|
+
"./errors": "./src/errors.ts",
|
|
53
|
+
"./etag": "./src/etag.ts",
|
|
54
|
+
"./i18n": "./src/i18n.ts",
|
|
55
|
+
"./input": "./src/input.ts",
|
|
56
|
+
"./ip-restrict": "./src/ip-restrict.ts",
|
|
57
|
+
"./logger": "./src/logger.ts",
|
|
58
|
+
"./node": "./src/node.ts",
|
|
59
|
+
"./openapi": "./src/openapi/register.ts",
|
|
60
|
+
"./openapi/spec": "./src/openapi/spec.ts",
|
|
61
|
+
"./openapi/scalar": "./src/openapi/scalar.ts",
|
|
62
|
+
"./openapi/swagger": "./src/openapi/swagger.ts",
|
|
63
|
+
"./plugin": "./src/plugin.ts",
|
|
64
|
+
"./powered-by": "./src/powered-by.ts",
|
|
65
|
+
"./pretty-json": "./src/pretty-json.ts",
|
|
66
|
+
"./proxy": "./src/proxy.ts",
|
|
67
|
+
"./request-to-curl": "./src/request-to-curl.ts",
|
|
68
|
+
"./request-id": "./src/request-id.ts",
|
|
69
|
+
"./secure-headers": "./src/secure-headers.ts",
|
|
70
|
+
"./serve": "./src/serve-register.ts",
|
|
71
|
+
"./server-timing": "./src/server-timing.ts",
|
|
72
|
+
"./static": "./src/static.ts",
|
|
73
|
+
"./telemetry/otel": "./src/telemetry/otel.ts",
|
|
74
|
+
"./testing": "./src/testing.ts",
|
|
75
|
+
"./timeout": "./src/timeout.ts",
|
|
76
|
+
"./tree": "./src/tree.ts",
|
|
77
|
+
"./ws/bun": "./src/ws/bun.ts",
|
|
78
|
+
"./ws/cloudflare": "./src/ws/cloudflare.ts",
|
|
79
|
+
"./ws/deno": "./src/ws/deno.ts",
|
|
80
|
+
"./ws/node": "./src/ws/node.ts"
|
|
81
|
+
},
|
|
82
|
+
"scripts": {
|
|
83
|
+
"test": "vitest run",
|
|
84
|
+
"test:all": "vitest run --config vitest.all.config.ts",
|
|
85
|
+
"test:harness": "vitest run --config vitest.harness.config.ts",
|
|
86
|
+
"test:harness:rust": "vitest run --config vitest.harness.rust.config.ts",
|
|
87
|
+
"test:watch": "vitest",
|
|
88
|
+
"type:check": "tsc --noEmit -p tsconfig.src.json && tsc --noEmit -p tsconfig.types.json",
|
|
89
|
+
"type:check:tests": "NODE_OPTIONS='--max-old-space-size=4096' tsc --noEmit",
|
|
90
|
+
"type:check:types": "tsc --noEmit -p tsconfig.types.json"
|
|
91
|
+
},
|
|
92
|
+
"dependencies": {
|
|
93
|
+
"jiti": "2.7.0"
|
|
94
|
+
},
|
|
95
|
+
"devDependencies": {
|
|
96
|
+
"@honey/e2e-surface": "workspace:*",
|
|
97
|
+
"@honey/e2e-kitchen": "workspace:*",
|
|
98
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
99
|
+
"@total-typescript/ts-reset": "catalog:",
|
|
100
|
+
"@types/node": "catalog:",
|
|
101
|
+
"@types/ws": "catalog:",
|
|
102
|
+
"arktype": "catalog:",
|
|
103
|
+
"effect": "catalog:",
|
|
104
|
+
"ts-morph": "catalog:",
|
|
105
|
+
"typescript": "catalog:",
|
|
106
|
+
"valibot": "catalog:",
|
|
107
|
+
"vite": "catalog:",
|
|
108
|
+
"vitest": "catalog:",
|
|
109
|
+
"yup": "catalog:",
|
|
110
|
+
"zod": "catalog:"
|
|
111
|
+
},
|
|
112
|
+
"peerDependencies": {
|
|
113
|
+
"ts-morph": ">=25.0.0"
|
|
114
|
+
},
|
|
115
|
+
"peerDependenciesMeta": {
|
|
116
|
+
"ts-morph": {
|
|
117
|
+
"optional": true
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
package/src/accepts.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
type ParsedType = {
|
|
2
|
+
q: number
|
|
3
|
+
subtype: string
|
|
4
|
+
type: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function parseAccept(header: string): ParsedType[] {
|
|
8
|
+
const types: ParsedType[] = []
|
|
9
|
+
for (const part of header.split(",")) {
|
|
10
|
+
const trimmed = part.trim()
|
|
11
|
+
if (trimmed.length === 0) continue
|
|
12
|
+
|
|
13
|
+
const segments = trimmed.split(";")
|
|
14
|
+
const mediaType = segments[0].trim()
|
|
15
|
+
const slash = mediaType.indexOf("/")
|
|
16
|
+
if (slash === -1) continue
|
|
17
|
+
|
|
18
|
+
let q = 1.0
|
|
19
|
+
for (let i = 1; i < segments.length; i++) {
|
|
20
|
+
const param = segments[i].trim()
|
|
21
|
+
if (param.startsWith("q=")) {
|
|
22
|
+
const parsed = Number.parseFloat(param.slice(2))
|
|
23
|
+
if (!Number.isNaN(parsed)) q = parsed
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
types.push({
|
|
28
|
+
q,
|
|
29
|
+
subtype: mediaType.slice(slash + 1).toLowerCase(),
|
|
30
|
+
type: mediaType.slice(0, slash).toLowerCase(),
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
return types.sort((a, b) => b.q - a.q)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function matches(accept: ParsedType, supported: string): boolean {
|
|
37
|
+
const slash = supported.indexOf("/")
|
|
38
|
+
if (slash === -1) return false
|
|
39
|
+
const sType = supported.slice(0, slash)
|
|
40
|
+
const sSub = supported.slice(slash + 1)
|
|
41
|
+
|
|
42
|
+
if (accept.type === "*" && accept.subtype === "*") return true
|
|
43
|
+
if (accept.type === sType && accept.subtype === "*") return true
|
|
44
|
+
return accept.type === sType && accept.subtype === sSub
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Pick the best supported content type based on the request's Accept header.
|
|
49
|
+
* Returns null if no match found. Returns first supported type if no Accept header.
|
|
50
|
+
*/
|
|
51
|
+
export function accepts(req: Request, supported: string[]): string | null {
|
|
52
|
+
const header = req.headers.get("accept")
|
|
53
|
+
if (header === null || header.length === 0) {
|
|
54
|
+
return supported[0] ?? null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const parsed = parseAccept(header)
|
|
58
|
+
if (parsed.length === 0) {
|
|
59
|
+
return supported[0] ?? null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* for each supported type (server preference order), find best q match */
|
|
63
|
+
let bestMatch: string | null = null
|
|
64
|
+
let bestQ = -1
|
|
65
|
+
|
|
66
|
+
for (const s of supported) {
|
|
67
|
+
for (const accept of parsed) {
|
|
68
|
+
if (accept.q === 0) continue
|
|
69
|
+
if (matches(accept, s) && accept.q > bestQ) {
|
|
70
|
+
bestQ = accept.q
|
|
71
|
+
bestMatch = s
|
|
72
|
+
break
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return bestMatch
|
|
78
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { HoneyError } from "./error.ts"
|
|
2
|
+
import type { MiddlewareFn } from "./middleware.ts"
|
|
3
|
+
import { EK, SK } from "./types.ts"
|
|
4
|
+
|
|
5
|
+
type BodyLimitOptions = {
|
|
6
|
+
/** Per content-type size limits (matched via startsWith against request Content-Type header) */
|
|
7
|
+
limits?: Record<string, number>
|
|
8
|
+
/** Default max body size in bytes — used when no content-type match or no limits map */
|
|
9
|
+
maxSize: number
|
|
10
|
+
/**
|
|
11
|
+
* Skip stream byte-counting when Content-Length header is present and within bounds.
|
|
12
|
+
* HTTP/1.1 and HTTP/2 framing guarantees body cannot exceed declared Content-Length
|
|
13
|
+
* regardless of TLS — the protocol enforces it at the transport level.
|
|
14
|
+
* When false, every request body is wrapped in a counting TransformStream even
|
|
15
|
+
* when Content-Length is present. Chunked requests (no Content-Length) are always
|
|
16
|
+
* stream-counted regardless of this setting.
|
|
17
|
+
* Default: false.
|
|
18
|
+
*/
|
|
19
|
+
trustContentLength?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type { BodyLimitOptions }
|
|
23
|
+
|
|
24
|
+
export function bodyLimit(opts: BodyLimitOptions): MiddlewareFn<{ req: Request }, {}> {
|
|
25
|
+
const { limits, maxSize, trustContentLength } = opts
|
|
26
|
+
const limitEntries = limits !== undefined ? Object.entries(limits) : null
|
|
27
|
+
|
|
28
|
+
const mw: MiddlewareFn<{ req: Request }, {}> = (ctx, next) => {
|
|
29
|
+
const req = ctx.req
|
|
30
|
+
const method = req.method
|
|
31
|
+
if (method === "GET" || method === "HEAD" || method === "OPTIONS" || method === "DELETE") {
|
|
32
|
+
return next()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* resolve max size from content-type */
|
|
36
|
+
let resolvedMax = maxSize
|
|
37
|
+
if (limitEntries !== null) {
|
|
38
|
+
const ct = req.headers.get("content-type")
|
|
39
|
+
if (ct !== null) {
|
|
40
|
+
for (const [key, limit] of limitEntries) {
|
|
41
|
+
if (ct.startsWith(key)) {
|
|
42
|
+
resolvedMax = limit
|
|
43
|
+
break
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* fast path: Content-Length header present */
|
|
50
|
+
const contentLength = req.headers.get("content-length")
|
|
51
|
+
if (contentLength !== null) {
|
|
52
|
+
const len = Number.parseInt(contentLength, 10)
|
|
53
|
+
if (!Number.isNaN(len) && len > resolvedMax) {
|
|
54
|
+
throw new HoneyError({
|
|
55
|
+
errorKey: EK.content_too_large,
|
|
56
|
+
status: SK.content_too_large,
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
if (trustContentLength === true) return next()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* slow path: stream-count when no Content-Length (chunked) or trustContentLength disabled */
|
|
63
|
+
if (req.body !== null) {
|
|
64
|
+
let totalBytes = 0
|
|
65
|
+
const limit = resolvedMax
|
|
66
|
+
const transform = new TransformStream<Uint8Array, Uint8Array>({
|
|
67
|
+
transform(chunk, controller) {
|
|
68
|
+
totalBytes += chunk.byteLength
|
|
69
|
+
if (totalBytes > limit) {
|
|
70
|
+
controller.error(
|
|
71
|
+
new HoneyError({
|
|
72
|
+
errorKey: EK.content_too_large,
|
|
73
|
+
status: SK.content_too_large,
|
|
74
|
+
}),
|
|
75
|
+
)
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
controller.enqueue(chunk)
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const limited = req.body.pipeThrough(transform)
|
|
83
|
+
const replaceBody = (req as unknown as Record<symbol, unknown>)[Symbol.for("honey.replaceBody")]
|
|
84
|
+
if (typeof replaceBody === "function") {
|
|
85
|
+
;(replaceBody as (stream: ReadableStream<Uint8Array>) => void).call(req, limited)
|
|
86
|
+
} else {
|
|
87
|
+
/* duplex: "half" required by Node/undici when body is a ReadableStream */
|
|
88
|
+
const newReq = new Request(req, {
|
|
89
|
+
body: limited,
|
|
90
|
+
duplex: "half",
|
|
91
|
+
} as RequestInit)
|
|
92
|
+
Object.defineProperty(ctx, "req", { configurable: true, value: newReq })
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return next()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return mw
|
|
100
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
2
|
+
import { builtinModules } from "node:module"
|
|
3
|
+
import { resolve } from "node:path"
|
|
4
|
+
|
|
5
|
+
/* ---- Public types ---- */
|
|
6
|
+
|
|
7
|
+
export type HoneyBuildConfig = {
|
|
8
|
+
external?: string[]
|
|
9
|
+
minify?: boolean
|
|
10
|
+
outDir?: string
|
|
11
|
+
port?: number
|
|
12
|
+
target: "bun" | "cloudflare" | "deno" | "node"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* ---- Internal types ---- */
|
|
16
|
+
|
|
17
|
+
type ResolvedBuildConfig = {
|
|
18
|
+
entry: string
|
|
19
|
+
export: string
|
|
20
|
+
port: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type BuildAdapterDef = {
|
|
24
|
+
entry(config: ResolvedBuildConfig): string
|
|
25
|
+
ssrTarget: "node" | "webworker"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* ---- Entry helpers ---- */
|
|
29
|
+
|
|
30
|
+
function featurePrelude(appSource: string): string {
|
|
31
|
+
const lines: string[] = []
|
|
32
|
+
if (/\.openapi\s*\(|\.manifest\s*\(/.test(appSource)) {
|
|
33
|
+
lines.push('import { enableOpenApi } from "@lovrozagar/honey/openapi"', "enableOpenApi()")
|
|
34
|
+
}
|
|
35
|
+
if (/\.errorI18n\s*\(/.test(appSource)) {
|
|
36
|
+
lines.push('import { enableI18n } from "@lovrozagar/honey/i18n"', "enableI18n()")
|
|
37
|
+
}
|
|
38
|
+
if (/(?<!Bun)(?<!Deno)\.serve\s*\(/.test(appSource)) {
|
|
39
|
+
lines.push('import { enableServe } from "@lovrozagar/honey/serve"', "enableServe()")
|
|
40
|
+
}
|
|
41
|
+
return lines.length > 0 ? `${lines.join("\n")}\n` : ""
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function importApp(entry: string, exportName: string): string {
|
|
45
|
+
if (exportName === "default") return `import app from "./${entry}"`
|
|
46
|
+
return `import { ${exportName} as app } from "./${entry}"`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* ---- Adapter definitions ---- */
|
|
50
|
+
|
|
51
|
+
const adapters: Record<HoneyBuildConfig["target"], BuildAdapterDef> = {
|
|
52
|
+
bun: {
|
|
53
|
+
entry(config) {
|
|
54
|
+
return [
|
|
55
|
+
importApp(config.entry, config.export),
|
|
56
|
+
"",
|
|
57
|
+
`const port = Number(process.env.PORT ?? ${config.port})`,
|
|
58
|
+
"Bun.serve({",
|
|
59
|
+
" fetch: (req, server) => app.fetch(req, { server }),",
|
|
60
|
+
' hostname: "0.0.0.0",',
|
|
61
|
+
" port,",
|
|
62
|
+
"})",
|
|
63
|
+
].join("\n")
|
|
64
|
+
},
|
|
65
|
+
ssrTarget: "node",
|
|
66
|
+
},
|
|
67
|
+
cloudflare: {
|
|
68
|
+
entry(config) {
|
|
69
|
+
return [
|
|
70
|
+
importApp(config.entry, config.export),
|
|
71
|
+
"",
|
|
72
|
+
"export default {",
|
|
73
|
+
" fetch: (req, env, ctx) => app.fetch(req, env, ctx),",
|
|
74
|
+
"}",
|
|
75
|
+
].join("\n")
|
|
76
|
+
},
|
|
77
|
+
ssrTarget: "webworker",
|
|
78
|
+
},
|
|
79
|
+
deno: {
|
|
80
|
+
entry(config) {
|
|
81
|
+
return [
|
|
82
|
+
importApp(config.entry, config.export),
|
|
83
|
+
"",
|
|
84
|
+
`const port = Number(Deno.env.get("PORT") ?? "${config.port}")`,
|
|
85
|
+
'Deno.serve({ hostname: "0.0.0.0", port }, (req) => app.fetch(req, {}))',
|
|
86
|
+
].join("\n")
|
|
87
|
+
},
|
|
88
|
+
ssrTarget: "webworker",
|
|
89
|
+
},
|
|
90
|
+
node: {
|
|
91
|
+
entry(config) {
|
|
92
|
+
return [
|
|
93
|
+
'import { serve } from "@lovrozagar/honey/node"',
|
|
94
|
+
importApp(config.entry, config.export),
|
|
95
|
+
"",
|
|
96
|
+
`const port = Number(process.env.PORT ?? ${config.port})`,
|
|
97
|
+
'serve(app, { env: process.env, hostname: "0.0.0.0", port })',
|
|
98
|
+
].join("\n")
|
|
99
|
+
},
|
|
100
|
+
ssrTarget: "node",
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* ---- Virtual module constants ---- */
|
|
105
|
+
|
|
106
|
+
const VIRTUAL_BUILD_ENTRY = "virtual:honey-build-entry"
|
|
107
|
+
const RESOLVED_BUILD_ENTRY = `\0${VIRTUAL_BUILD_ENTRY}`
|
|
108
|
+
|
|
109
|
+
/* ---- Build plugin factory ---- */
|
|
110
|
+
|
|
111
|
+
export function createBuildPlugin(
|
|
112
|
+
buildConfig: HoneyBuildConfig,
|
|
113
|
+
shared: { entry: string; export: string },
|
|
114
|
+
) {
|
|
115
|
+
const adapter = adapters[buildConfig.target]
|
|
116
|
+
const resolvedConfig: ResolvedBuildConfig = {
|
|
117
|
+
entry: shared.entry,
|
|
118
|
+
export: shared.export,
|
|
119
|
+
port: buildConfig.port ?? 3000,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let root = ""
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
apply: "build" as const,
|
|
126
|
+
|
|
127
|
+
config() {
|
|
128
|
+
return {
|
|
129
|
+
build: {
|
|
130
|
+
emptyOutDir: false,
|
|
131
|
+
minify: buildConfig.minify ?? true,
|
|
132
|
+
outDir: buildConfig.outDir ?? "./dist",
|
|
133
|
+
rolldownOptions: {
|
|
134
|
+
external: [
|
|
135
|
+
...builtinModules,
|
|
136
|
+
...builtinModules.map((m) => `node:${m}`),
|
|
137
|
+
...(buildConfig.external ?? []),
|
|
138
|
+
],
|
|
139
|
+
input: VIRTUAL_BUILD_ENTRY,
|
|
140
|
+
output: { entryFileNames: "index.js" },
|
|
141
|
+
},
|
|
142
|
+
ssr: true,
|
|
143
|
+
},
|
|
144
|
+
ssr: {
|
|
145
|
+
noExternal: true,
|
|
146
|
+
target: adapter.ssrTarget,
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
configResolved(cfg: { root: string }) {
|
|
152
|
+
root = cfg.root
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
load(id: string): { code: string; moduleType: string } | undefined {
|
|
156
|
+
if (id === RESOLVED_BUILD_ENTRY) {
|
|
157
|
+
const appPath = resolve(root || ".", shared.entry)
|
|
158
|
+
const appSource = existsSync(appPath) ? readFileSync(appPath, "utf-8") : ""
|
|
159
|
+
return {
|
|
160
|
+
code: `${featurePrelude(appSource)}${adapter.entry(resolvedConfig)}`,
|
|
161
|
+
moduleType: "js",
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return undefined
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
name: "honey:build",
|
|
168
|
+
|
|
169
|
+
resolveId(id: string): string | undefined {
|
|
170
|
+
if (id === VIRTUAL_BUILD_ENTRY) return RESOLVED_BUILD_ENTRY
|
|
171
|
+
return undefined
|
|
172
|
+
},
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Code generated by honey. DO NOT EDIT.
|
|
2
|
+
package cli
|
|
3
|
+
|
|
4
|
+
import "errors"
|
|
5
|
+
|
|
6
|
+
// ErrMissingAPIKey is returned when no API key can be found via flags, env, or config file.
|
|
7
|
+
var ErrMissingAPIKey = errors.New("api key required: set --api-key, env var, or config file")
|
|
8
|
+
|
|
9
|
+
// ResolveAPIKey returns the resolved API key or ErrMissingAPIKey if none is available.
|
|
10
|
+
func ResolveAPIKey(cfg Config) (string, error) {
|
|
11
|
+
if cfg.APIKey != "" {
|
|
12
|
+
return cfg.APIKey, nil
|
|
13
|
+
}
|
|
14
|
+
return "", ErrMissingAPIKey
|
|
15
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// Code generated by honey. DO NOT EDIT.
|
|
2
|
+
package cli
|
|
3
|
+
|
|
4
|
+
import (
|
|
5
|
+
"fmt"
|
|
6
|
+
"io"
|
|
7
|
+
"os"
|
|
8
|
+
"path/filepath"
|
|
9
|
+
|
|
10
|
+
"github.com/pelletier/go-toml/v2"
|
|
11
|
+
"github.com/spf13/pflag"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
// Config holds resolved runtime values for the CLI binary.
|
|
15
|
+
//
|
|
16
|
+
// Precedence (high → low) — see LoadConfig below:
|
|
17
|
+
//
|
|
18
|
+
// 1. --api-key / --base-url / --output / … flag set on the command line.
|
|
19
|
+
// 2. Env var: <ENV_PREFIX>_API_KEY / _BASE_URL / _OUTPUT / …
|
|
20
|
+
// 3. Config file from --config, $<ENV_PREFIX>_CONFIG, $XDG_CONFIG_HOME/<name>/config.toml,
|
|
21
|
+
// $HOME/.config/<name>/config.toml, os.UserConfigDir()/<name>/config.toml.
|
|
22
|
+
// 4. Built-in defaults.
|
|
23
|
+
// 5. (missing api-key → ErrMissingAPIKey, mapped to exit 4).
|
|
24
|
+
type Config struct {
|
|
25
|
+
APIKey string
|
|
26
|
+
BaseURL string
|
|
27
|
+
Output string
|
|
28
|
+
Timeout string
|
|
29
|
+
Verbose bool
|
|
30
|
+
ConfigPath string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type tomlConfig struct {
|
|
34
|
+
APIKey string `toml:"api_key"`
|
|
35
|
+
BaseURL string `toml:"base_url"`
|
|
36
|
+
Output string `toml:"output"`
|
|
37
|
+
Timeout string `toml:"timeout"`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// LoadConfig merges flag values, env vars, and a TOML config file into a Config.
|
|
41
|
+
//
|
|
42
|
+
// The resolution order matches the Config doc-comment precedence list.
|
|
43
|
+
func LoadConfig(flags *pflag.FlagSet, envPrefix, configName string) (Config, error) {
|
|
44
|
+
var cfg Config
|
|
45
|
+
|
|
46
|
+
cfgPath := ""
|
|
47
|
+
if flags.Changed("config") {
|
|
48
|
+
v, _ := flags.GetString("config")
|
|
49
|
+
cfgPath = v
|
|
50
|
+
} else if v := os.Getenv(envPrefix + "_CONFIG"); v != "" {
|
|
51
|
+
cfgPath = v
|
|
52
|
+
} else {
|
|
53
|
+
cfgPath = defaultConfigPath(configName)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
var fromFile tomlConfig
|
|
57
|
+
if cfgPath != "" {
|
|
58
|
+
if data, err := os.ReadFile(cfgPath); err == nil {
|
|
59
|
+
if err := toml.Unmarshal(data, &fromFile); err != nil {
|
|
60
|
+
return cfg, fmt.Errorf("parse config %s: %w", cfgPath, err)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
cfg.ConfigPath = cfgPath
|
|
65
|
+
|
|
66
|
+
cfg.APIKey = fromFile.APIKey
|
|
67
|
+
if v := os.Getenv(envPrefix + "_API_KEY"); v != "" {
|
|
68
|
+
cfg.APIKey = v
|
|
69
|
+
}
|
|
70
|
+
if flags.Changed("api-key") {
|
|
71
|
+
v, _ := flags.GetString("api-key")
|
|
72
|
+
cfg.APIKey = v
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
cfg.BaseURL = fromFile.BaseURL
|
|
76
|
+
if v := os.Getenv(envPrefix + "_BASE_URL"); v != "" {
|
|
77
|
+
cfg.BaseURL = v
|
|
78
|
+
}
|
|
79
|
+
if flags.Changed("base-url") {
|
|
80
|
+
v, _ := flags.GetString("base-url")
|
|
81
|
+
cfg.BaseURL = v
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
cfg.Output = fromFile.Output
|
|
85
|
+
if v := os.Getenv(envPrefix + "_OUTPUT"); v != "" {
|
|
86
|
+
cfg.Output = v
|
|
87
|
+
}
|
|
88
|
+
if flags.Changed("output") {
|
|
89
|
+
v, _ := flags.GetString("output")
|
|
90
|
+
cfg.Output = v
|
|
91
|
+
}
|
|
92
|
+
if cfg.Output == "" {
|
|
93
|
+
cfg.Output = "json"
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
cfg.Timeout = fromFile.Timeout
|
|
97
|
+
if v := os.Getenv(envPrefix + "_TIMEOUT"); v != "" {
|
|
98
|
+
cfg.Timeout = v
|
|
99
|
+
}
|
|
100
|
+
if flags.Changed("timeout") {
|
|
101
|
+
v, _ := flags.GetString("timeout")
|
|
102
|
+
cfg.Timeout = v
|
|
103
|
+
}
|
|
104
|
+
if cfg.Timeout == "" {
|
|
105
|
+
cfg.Timeout = "30s"
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if flags.Changed("verbose") {
|
|
109
|
+
v, _ := flags.GetBool("verbose")
|
|
110
|
+
cfg.Verbose = v
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return cfg, nil
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
func defaultConfigPath(configName string) string {
|
|
117
|
+
if configName == "" {
|
|
118
|
+
return ""
|
|
119
|
+
}
|
|
120
|
+
if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" {
|
|
121
|
+
return filepath.Join(xdg, configName, "config.toml")
|
|
122
|
+
}
|
|
123
|
+
if home := os.Getenv("HOME"); home != "" {
|
|
124
|
+
return filepath.Join(home, ".config", configName, "config.toml")
|
|
125
|
+
}
|
|
126
|
+
if dir, err := os.UserConfigDir(); err == nil {
|
|
127
|
+
return filepath.Join(dir, configName, "config.toml")
|
|
128
|
+
}
|
|
129
|
+
return ""
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// readDataFlag resolves the value of a --data flag into raw bytes.
|
|
133
|
+
//
|
|
134
|
+
// Rules:
|
|
135
|
+
// - "@path" → reads file at path.
|
|
136
|
+
// - "-" → reads os.Stdin.
|
|
137
|
+
// - "" → returns nil, nil (caller decides whether required).
|
|
138
|
+
// - anything else → treats val as literal JSON.
|
|
139
|
+
//
|
|
140
|
+
// Exported as ReadDataFlag for use from cmd/*.go.
|
|
141
|
+
func ReadDataFlag(val string) ([]byte, error) {
|
|
142
|
+
return readDataFlag(val)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
func readDataFlag(val string) ([]byte, error) {
|
|
146
|
+
if val == "" {
|
|
147
|
+
return nil, nil
|
|
148
|
+
}
|
|
149
|
+
if val == "-" {
|
|
150
|
+
data, err := io.ReadAll(os.Stdin)
|
|
151
|
+
if err != nil {
|
|
152
|
+
return nil, fmt.Errorf("read stdin: %w", err)
|
|
153
|
+
}
|
|
154
|
+
return data, nil
|
|
155
|
+
}
|
|
156
|
+
if val[0] == '@' {
|
|
157
|
+
path := val[1:]
|
|
158
|
+
data, err := os.ReadFile(path)
|
|
159
|
+
if err != nil {
|
|
160
|
+
return nil, fmt.Errorf("read data file %s: %w", path, err)
|
|
161
|
+
}
|
|
162
|
+
return data, nil
|
|
163
|
+
}
|
|
164
|
+
return []byte(val), nil
|
|
165
|
+
}
|