@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,749 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, relative, resolve } from "node:path";
3
+ import type { ts } from "ts-morph";
4
+
5
+ export type ExtractedBaseCtx = {
6
+ envType: string;
7
+ middlewareType: string | null;
8
+ tapsType: string | null;
9
+ };
10
+
11
+ function findTsConfig(fromPath: string): string | undefined {
12
+ let dir = dirname(resolve(fromPath));
13
+ let prev = "";
14
+ while (dir !== prev) {
15
+ const p = resolve(dir, "tsconfig.json");
16
+ if (existsSync(p)) return p;
17
+ prev = dir;
18
+ dir = dirname(dir);
19
+ }
20
+ return undefined;
21
+ }
22
+
23
+ /**
24
+ * Uses ts-morph to extract the middleware context type from a Honey app export.
25
+ * Returns inline type strings so the gen file has zero imports from user code.
26
+ */
27
+ export async function extractBaseCtx(options: {
28
+ entryPath: string;
29
+ exportName: string;
30
+ outputDir?: string;
31
+ tsconfigPath?: string;
32
+ }): Promise<ExtractedBaseCtx> {
33
+ const { Project, ts } = await import("ts-morph");
34
+
35
+ const tsConfigFilePath =
36
+ options.tsconfigPath ?? findTsConfig(options.entryPath);
37
+
38
+ const project = new Project({
39
+ skipAddingFilesFromTsConfig: true,
40
+ tsConfigFilePath,
41
+ });
42
+
43
+ project.addSourceFilesAtPaths(options.entryPath);
44
+ project.resolveSourceFileDependencies();
45
+
46
+ const sourceFile = project.getSourceFileOrThrow(options.entryPath);
47
+ const varDecl = sourceFile.getVariableDeclaration(options.exportName);
48
+ if (!varDecl) {
49
+ throw new Error(
50
+ `Export "${options.exportName}" not found in ${options.entryPath}`,
51
+ );
52
+ }
53
+
54
+ const type = varDecl.getType();
55
+ /* ts-morph bundles different TS version — structurally identical at runtime */
56
+ function bridge<T>(value: unknown): T {
57
+ return value as T;
58
+ }
59
+ const checker: ts.TypeChecker = bridge(
60
+ project.getTypeChecker().compilerObject,
61
+ );
62
+ const node: ts.Node = bridge(varDecl.compilerNode);
63
+ const tsLib: typeof ts = bridge(ts);
64
+
65
+ function getType(
66
+ sym: ReturnType<typeof type.getProperty>,
67
+ ): ts.Type {
68
+ const s: ts.Symbol = bridge(sym?.compilerSymbol);
69
+ return checker.getTypeOfSymbolAtLocation(s, node);
70
+ }
71
+
72
+ /* extract $env */
73
+ let envType = "Record<string, unknown>";
74
+ const envProp = type.getProperty("$env");
75
+ if (envProp) {
76
+ envType = serialize(getType(envProp), checker, node, tsLib);
77
+ }
78
+
79
+ /* extract $ctx and isolate middleware additions */
80
+ const ctxProp = type.getProperty("$ctx");
81
+ if (!ctxProp) {
82
+ throw new Error(
83
+ `Export "${options.exportName}" has no $ctx — not a Honey instance`,
84
+ );
85
+ }
86
+
87
+ const ctxType = getType(ctxProp);
88
+ const middlewareType = extractMiddleware(ctxType, checker, node, tsLib);
89
+
90
+ /* extract $taps — typed tap payloads declared via .taps<T>() */
91
+ let tapsType: string | null = null;
92
+ const tapsProp = type.getProperty("$taps");
93
+ if (tapsProp) {
94
+ const tapsT = getType(tapsProp);
95
+ const tapsStr = serialize(tapsT, checker, node, tsLib);
96
+ if (tapsStr !== "{}" && tapsStr !== "Record<string, unknown>") {
97
+ tapsType = tapsStr;
98
+ }
99
+ }
100
+
101
+ const sanitize = (s: string) => sanitizeImportPaths(s, options.outputDir);
102
+ return {
103
+ envType: sanitize(envType),
104
+ middlewareType: middlewareType ? sanitize(middlewareType) : null,
105
+ tapsType: tapsType ? sanitize(tapsType) : null,
106
+ };
107
+ }
108
+
109
+ /* properties that live on HoneyContext — everything else is middleware-added */
110
+ const HONEY_CTX_PROPS = new Set([
111
+ "background",
112
+ "cookies",
113
+ "env",
114
+ "errors",
115
+ "executionCtx",
116
+ "headers",
117
+ "meta",
118
+ "params",
119
+ "path",
120
+ "req",
121
+ "res",
122
+ "routePattern",
123
+ "search",
124
+ "searchAll",
125
+ "tap",
126
+ ]);
127
+
128
+ /** Resolve an import() reference from a declaration's type annotation */
129
+ function resolveTypeRefFromDecl(
130
+ decl: ts.Declaration | undefined,
131
+ checker: ts.TypeChecker,
132
+ compiler: typeof ts,
133
+ ): string | undefined {
134
+ if (!decl) return undefined
135
+ const typeRef = (decl as { type?: ts.TypeNode }).type
136
+ if (!typeRef || !compiler.isTypeReferenceNode(typeRef)) return undefined
137
+ const typeName = (typeRef as ts.TypeReferenceNode).typeName
138
+
139
+ let refSym = checker.getSymbolAtLocation(typeName)
140
+ while (refSym && (refSym.flags & compiler.SymbolFlags.Alias)) {
141
+ refSym = checker.getAliasedSymbol(refSym)
142
+ }
143
+ if (!refSym) return undefined
144
+
145
+ const refDecls = refSym.declarations
146
+ if (!refDecls?.length) return undefined
147
+
148
+ const srcFile = refDecls[0].getSourceFile()
149
+ if (!isExportedFromFile(refSym, srcFile, checker)) return undefined
150
+
151
+ return `import("${srcFile.fileName}").${refSym.getName()}`
152
+ }
153
+
154
+ type MwPropEntry = { name: string; opt: boolean; type: string }
155
+
156
+ function extractMiddlewareProps(
157
+ ctxType: ts.Type,
158
+ checker: ts.TypeChecker,
159
+ node: ts.Node,
160
+ compiler: typeof ts,
161
+ ): MwPropEntry[] {
162
+ const allProps = ctxType.getProperties();
163
+ const mwProps = allProps.filter((p) => {
164
+ const name = p.getName();
165
+ if (HONEY_CTX_PROPS.has(name)) return false;
166
+ /* skip internal/private properties (underscore-prefixed) */
167
+ if (name.startsWith("_")) return false;
168
+ return true;
169
+ });
170
+
171
+ return mwProps.map((p) => {
172
+ const t = checker.getTypeOfSymbolAtLocation(p, p.valueDeclaration ?? node);
173
+ const opt = (p.flags & compiler.SymbolFlags.Optional) !== 0;
174
+
175
+ /* 1. try declaration-site TypeReference (explicit annotation on property or variable) */
176
+ let typeStr: string | undefined;
177
+ const mwDecl = p.valueDeclaration ?? p.declarations?.[0];
178
+ typeStr = resolveTypeRefFromDecl(mwDecl, checker, compiler);
179
+
180
+ /* 1b. shorthand property — trace to the underlying variable's type annotation */
181
+ if (!typeStr && mwDecl && compiler.isShorthandPropertyAssignment(mwDecl)) {
182
+ const varSym = checker.getShorthandAssignmentValueSymbol(mwDecl);
183
+ if (varSym) {
184
+ const varDecl = varSym.valueDeclaration ?? varSym.declarations?.[0];
185
+ typeStr = resolveTypeRefFromDecl(varDecl, checker, compiler);
186
+
187
+ /* 1c. destructured binding (e.g. const { shardDb } = openShardSession(...))
188
+ * — trace through the function return type to find the original type annotation */
189
+ if (!typeStr && varDecl && compiler.isBindingElement(varDecl)) {
190
+ let parent: ts.Node = varDecl.parent;
191
+ while (parent && !compiler.isVariableDeclaration(parent)) parent = parent.parent;
192
+ if (parent && compiler.isVariableDeclaration(parent)) {
193
+ const initializer = (parent as ts.VariableDeclaration).initializer
194
+ if (initializer) {
195
+ const initType = checker.getTypeAtLocation(initializer);
196
+ const retProp = initType.getProperty(p.getName());
197
+ if (retProp?.valueDeclaration && compiler.isShorthandPropertyAssignment(retProp.valueDeclaration)) {
198
+ const innerSym = checker.getShorthandAssignmentValueSymbol(retProp.valueDeclaration);
199
+ if (innerSym) {
200
+ const innerDecl = innerSym.valueDeclaration ?? innerSym.declarations?.[0];
201
+ typeStr = resolveTypeRefFromDecl(innerDecl, checker, compiler);
202
+ }
203
+ }
204
+ }
205
+ }
206
+ }
207
+ }
208
+ }
209
+
210
+ /* 2. try resolved type as named import reference */
211
+ if (!typeStr) {
212
+ typeStr = serializeAsRef(t, checker, node, compiler) ?? undefined;
213
+ }
214
+
215
+ /* 3. structural fallback */
216
+ if (!typeStr) {
217
+ typeStr = serialize(t, checker, node, compiler);
218
+ }
219
+
220
+ return { name: p.getName(), opt, type: typeStr };
221
+ });
222
+ }
223
+
224
+ function extractMiddleware(
225
+ ctxType: ts.Type,
226
+ checker: ts.TypeChecker,
227
+ node: ts.Node,
228
+ compiler: typeof ts,
229
+ ): string | null {
230
+ const props = extractMiddlewareProps(ctxType, checker, node, compiler);
231
+ if (props.length === 0) return null;
232
+ const entries = props.map((p) => `${p.name}${p.opt ? "?" : ""}: ${p.type}`);
233
+ return `{ ${entries.join("; ")} }`;
234
+ }
235
+
236
+ /** Check if a symbol is exported from its source file */
237
+ function isExportedFromFile(
238
+ sym: ts.Symbol,
239
+ sf: ts.SourceFile,
240
+ checker: ts.TypeChecker,
241
+ ): boolean {
242
+ /* .d.ts module types are always accessible */
243
+ if (sf.isDeclarationFile) return true;
244
+ const moduleSym = checker.getSymbolAtLocation(sf);
245
+ if (!moduleSym) return false;
246
+ const exports = checker.getExportsOfModule(moduleSym);
247
+ return exports.some((e) => e.getName() === sym.getName());
248
+ }
249
+
250
+ /**
251
+ * Attempts to build an `import("path").TypeName` reference for a type.
252
+ * Returns null if the type has no usable named symbol.
253
+ *
254
+ * Prefers compact references over structural expansion:
255
+ * - .d.ts types: import("drizzle-orm/d1").DrizzleD1Database
256
+ * - .ts type aliases: import("./db.orm.gen").CoreDb
257
+ * - globals (Promise, Map, etc.) are skipped — handled by serialize()
258
+ */
259
+ function serializeAsRef(
260
+ type: ts.Type,
261
+ checker: ts.TypeChecker,
262
+ node: ts.Node,
263
+ compiler: typeof ts,
264
+ depth = 0,
265
+ ): string | null {
266
+ if (depth > 6) return null;
267
+
268
+ /* union with never members (from intersection narrowing) — simplify first */
269
+ if (type.isUnion()) {
270
+ const nonNever = (type as ts.UnionType).types.filter(
271
+ (t) => !(t.flags & compiler.TypeFlags.Never),
272
+ );
273
+ if (nonNever.length === 1) {
274
+ return serializeAsRef(nonNever[0], checker, node, compiler, depth);
275
+ }
276
+ }
277
+
278
+ const sym = type.getSymbol() ?? type.aliasSymbol;
279
+ if (!sym) return null;
280
+
281
+ const name = sym.getName();
282
+ if (!name || name.startsWith("__") || name === "Object") return null;
283
+
284
+ const decls = sym.declarations;
285
+ if (!decls?.length) return null;
286
+
287
+ const sf = decls[0].getSourceFile();
288
+ const fileName = sf.fileName;
289
+
290
+ /* skip global/lib types — serialize() handles these with its globals set */
291
+ if (fileName.includes("/lib.") || fileName.includes("/lib/")) return null;
292
+
293
+ const isDts = sf.isDeclarationFile;
294
+ const isTypeAlias =
295
+ (sym.flags & compiler.SymbolFlags.TypeAlias) !== 0 ||
296
+ decls.some((d) => compiler.isTypeAliasDeclaration(d));
297
+
298
+ /* only reference .d.ts symbols or explicit type aliases from .ts files */
299
+ if (!isDts && !isTypeAlias) return null;
300
+
301
+ /* skip ambient .d.ts scripts (no exports) — import() can't resolve them */
302
+ if (isDts) {
303
+ const tsSf = sf as unknown as { externalModuleIndicator?: unknown };
304
+ if (!tsSf.externalModuleIndicator) return null;
305
+ }
306
+
307
+ /* skip non-exported symbols — import() on them resolves to `any` */
308
+ if (!isExportedFromFile(sym, sf, checker)) return null;
309
+
310
+ /* build type arguments if present */
311
+ const ref = type as ts.TypeReference;
312
+ const typeArgs = ref.typeArguments ?? type.aliasTypeArguments;
313
+ if (typeArgs?.length) {
314
+ const args = typeArgs.map((arg) => {
315
+ const argRef = serializeAsRef(arg, checker, node, compiler, depth + 1);
316
+ return argRef ?? serialize(arg, checker, node, compiler);
317
+ });
318
+ return `import("${fileName}").${name}<${args.join(", ")}>`;
319
+ }
320
+
321
+ return `import("${fileName}").${name}`;
322
+ }
323
+
324
+ function serialize(
325
+ type: ts.Type,
326
+ checker: ts.TypeChecker,
327
+ node: ts.Node,
328
+ compiler: typeof ts,
329
+ depth = 0,
330
+ ): string {
331
+ if (depth > 8) return "unknown";
332
+
333
+ const next = (t: ts.Type) =>
334
+ serialize(t, checker, node, compiler, depth + 1);
335
+
336
+ /* primitives */
337
+ if (type.flags & compiler.TypeFlags.String) return "string";
338
+ if (type.flags & compiler.TypeFlags.Number) return "number";
339
+ if (type.flags & compiler.TypeFlags.Boolean) return "boolean";
340
+ if (type.flags & compiler.TypeFlags.BigInt) return "bigint";
341
+ if (type.flags & compiler.TypeFlags.ESSymbol) return "symbol";
342
+ if (type.flags & compiler.TypeFlags.Void) return "void";
343
+ if (type.flags & compiler.TypeFlags.Undefined) return "undefined";
344
+ if (type.flags & compiler.TypeFlags.Null) return "null";
345
+ if (type.flags & compiler.TypeFlags.Never) return "never";
346
+ if (type.flags & compiler.TypeFlags.Unknown) return "unknown";
347
+ if (type.flags & compiler.TypeFlags.Any) return "unknown";
348
+
349
+ /* literals */
350
+ if (type.isStringLiteral())
351
+ return `"${(type as ts.StringLiteralType).value}"`;
352
+ if (type.isNumberLiteral())
353
+ return `${(type as ts.NumberLiteralType).value}`;
354
+
355
+ /* union */
356
+ if (type.isUnion()) {
357
+ return type.types
358
+ .map((t) => {
359
+ const s = next(t);
360
+ /* parenthesize function types inside unions — TS syntax requires it */
361
+ return s.includes("=>") ? `(${s})` : s;
362
+ })
363
+ .join(" | ");
364
+ }
365
+
366
+ /* intersection */
367
+ if (type.isIntersection()) return type.types.map(next).join(" & ");
368
+
369
+ /* array */
370
+ if (checker.isArrayType(type)) {
371
+ const args = checker.getTypeArguments(
372
+ type as ts.TypeReference,
373
+ );
374
+ if (args.length > 0) {
375
+ const el = next(args[0]);
376
+ return el.includes("|") || el.includes("&") ? `(${el})[]` : `${el}[]`;
377
+ }
378
+ return "unknown[]";
379
+ }
380
+
381
+ /* tuple */
382
+ if (checker.isTupleType(type)) {
383
+ const args = checker.getTypeArguments(
384
+ type as ts.TypeReference,
385
+ );
386
+ return `[${args.map(next).join(", ")}]`;
387
+ }
388
+
389
+ /* function (pure callable, no data properties) */
390
+ const sigs = type.getCallSignatures();
391
+ if (sigs.length > 0 && type.getProperties().length === 0) {
392
+ const sig = sigs[0];
393
+ const params = sig.getParameters().map((p) => {
394
+ const pt = checker.getTypeOfSymbolAtLocation(
395
+ p,
396
+ p.valueDeclaration ?? node,
397
+ );
398
+ const decl = p.valueDeclaration;
399
+ const isRest =
400
+ decl !== undefined &&
401
+ compiler.isParameter(decl) &&
402
+ (decl as ts.ParameterDeclaration).dotDotDotToken !== undefined;
403
+ return `${isRest ? "..." : ""}${p.getName()}: ${next(pt)}`;
404
+ });
405
+ const ret = checker.getReturnTypeOfSignature(sig);
406
+ return `(${params.join(", ")}) => ${next(ret)}`;
407
+ }
408
+
409
+ /* well-known globals — keep name, serialize type args */
410
+ const sym = type.getSymbol();
411
+ if (sym) {
412
+ const name = sym.getName();
413
+ const globals = new Set([
414
+ "AbortController",
415
+ "AbortSignal",
416
+ "ArrayBuffer",
417
+ "Blob",
418
+ "DataView",
419
+ "Date",
420
+ "Error",
421
+ "File",
422
+ "FormData",
423
+ "Headers",
424
+ "Map",
425
+ "Promise",
426
+ "ReadableStream",
427
+ "RegExp",
428
+ "Request",
429
+ "Response",
430
+ "Set",
431
+ "SharedArrayBuffer",
432
+ "TransformStream",
433
+ "URL",
434
+ "URLSearchParams",
435
+ "WeakMap",
436
+ "WeakSet",
437
+ "WritableStream",
438
+ ]);
439
+ if (globals.has(name)) {
440
+ const ref = type as ts.TypeReference;
441
+ if (ref.typeArguments?.length) {
442
+ return `${name}<${ref.typeArguments.map(next).join(", ")}>`;
443
+ }
444
+ return name;
445
+ }
446
+
447
+ /* named types — prefer import() reference over structural expansion */
448
+ if (name && !name.startsWith("__") && name !== "Object") {
449
+ const ref = serializeAsRef(type, checker, node, compiler, 0);
450
+ if (ref) return ref;
451
+
452
+ /* fallback: typeToString for .d.ts types without clean ref */
453
+ const decls = sym.declarations;
454
+ if (decls?.length) {
455
+ const sf = decls[0].getSourceFile();
456
+ if (sf.isDeclarationFile) {
457
+ return checker.typeToString(
458
+ type,
459
+ node,
460
+ compiler.TypeFormatFlags.UseFullyQualifiedType |
461
+ compiler.TypeFormatFlags.NoTruncation,
462
+ );
463
+ }
464
+ }
465
+ }
466
+ }
467
+
468
+ /* Record<string, V> — index signature only */
469
+ const stringIdx = type.getStringIndexType();
470
+ if (stringIdx && type.getProperties().length === 0) {
471
+ return `Record<string, ${next(stringIdx)}>`;
472
+ }
473
+
474
+ /* object — expand to structural form */
475
+ const props = type.getProperties();
476
+ if (props.length > 50) {
477
+ return checker.typeToString(type, node, compiler.TypeFormatFlags.NoTruncation);
478
+ }
479
+ if (props.length > 0) {
480
+ const entries: string[] = [];
481
+ for (const p of props) {
482
+ const decls = p.getDeclarations();
483
+ if (decls?.length) {
484
+ const flags = compiler.getCombinedModifierFlags(decls[0]);
485
+ if (flags & (compiler.ModifierFlags.Private | compiler.ModifierFlags.Protected))
486
+ continue;
487
+ }
488
+ const pt = checker.getTypeOfSymbolAtLocation(p, decls?.[0] ?? node);
489
+ const opt = (p.flags & compiler.SymbolFlags.Optional) !== 0;
490
+ const ro =
491
+ decls?.some(
492
+ (d) =>
493
+ (compiler.getCombinedModifierFlags(d) & compiler.ModifierFlags.Readonly) !== 0,
494
+ ) ?? false;
495
+ entries.push(
496
+ `${ro ? "readonly " : ""}${p.getName()}${opt ? "?" : ""}: ${next(pt)}`,
497
+ );
498
+ }
499
+ if (entries.length > 0) return `{ ${entries.join("; ")} }`;
500
+ }
501
+
502
+ /* fallback */
503
+ return checker.typeToString(type, undefined, compiler.TypeFormatFlags.NoTruncation);
504
+ }
505
+
506
+ /**
507
+ * Converts absolute `import()` paths emitted by `typeToString` into
508
+ * resolvable module specifiers:
509
+ * - node_modules paths → bare package specifiers
510
+ * - project-internal paths → relative from outputDir
511
+ */
512
+ function sanitizeImportPaths(str: string, outputDir?: string): string {
513
+ return str.replace(/import\("([^"]+)"\)/g, (_match, filePath: string) => {
514
+ const nmIdx = filePath.lastIndexOf("/node_modules/");
515
+ if (nmIdx !== -1) {
516
+ return `import("${filePath.slice(nmIdx + "/node_modules/".length)}")`;
517
+ }
518
+ if (!outputDir) return `import("${filePath}")`;
519
+ let rel = relative(outputDir, filePath);
520
+ if (!rel.startsWith(".")) rel = `./${rel}`;
521
+ return `import("${rel}")`;
522
+ });
523
+ }
524
+
525
+ export type ExtractedChainTypes = {
526
+ base: ExtractedBaseCtx;
527
+ /** per-route middleware additions keyed by "method /path" */
528
+ routeMiddleware: Record<string, string>;
529
+ /** structured per-property middleware data for sub-type dedup */
530
+ routeMiddlewareProps: Record<string, Array<{ name: string; opt: boolean; type: string }>>;
531
+ };
532
+
533
+ const HTTP_METHODS = new Set([
534
+ "all",
535
+ "delete",
536
+ "get",
537
+ "head",
538
+ "options",
539
+ "patch",
540
+ "post",
541
+ "put",
542
+ ]);
543
+
544
+ /**
545
+ * Extracts base ctx and per-route middleware additions from sub-chains.
546
+ * Traces HTTP method calls (`.get()`, `.post()`, etc.) back to their
547
+ * Honey instance receiver, computes the ctx diff vs the base export,
548
+ * and returns per-route additions keyed by "method /path".
549
+ */
550
+ export async function extractChainTypes(options: {
551
+ entryPath: string;
552
+ exportName: string;
553
+ outputDir?: string;
554
+ tsconfigPath?: string;
555
+ }): Promise<ExtractedChainTypes> {
556
+ const { Project, ts } = await import("ts-morph");
557
+
558
+ const tsConfigFilePath =
559
+ options.tsconfigPath ?? findTsConfig(options.entryPath);
560
+
561
+ const project = new Project({
562
+ skipAddingFilesFromTsConfig: true,
563
+ tsConfigFilePath,
564
+ });
565
+
566
+ project.addSourceFilesAtPaths(options.entryPath);
567
+ project.resolveSourceFileDependencies();
568
+
569
+ const sourceFile = project.getSourceFileOrThrow(options.entryPath);
570
+ const baseVarDecl = sourceFile.getVariableDeclaration(options.exportName);
571
+ if (!baseVarDecl) {
572
+ throw new Error(
573
+ `Export "${options.exportName}" not found in ${options.entryPath}`,
574
+ );
575
+ }
576
+
577
+ function bridge<T>(value: unknown): T {
578
+ return value as T;
579
+ }
580
+ const checker: ts.TypeChecker = bridge(
581
+ project.getTypeChecker().compilerObject,
582
+ );
583
+ const baseNode: ts.Node = bridge(
584
+ baseVarDecl.compilerNode,
585
+ );
586
+ const tsLib: typeof ts = bridge(ts);
587
+
588
+ function getCtxType(
589
+ tsType: ts.Type,
590
+ locationNode?: ts.Node,
591
+ ): ts.Type | null {
592
+ const sym = tsType.getProperty("$ctx");
593
+ if (!sym) return null;
594
+ return checker.getTypeOfSymbolAtLocation(sym, locationNode ?? sym.valueDeclaration ?? baseNode);
595
+ }
596
+
597
+ /* extract base ctx */
598
+ const baseType = bridge<ts.Type>(
599
+ baseVarDecl.getType().compilerType,
600
+ );
601
+ let envType = "Record<string, unknown>";
602
+ const envSym = baseType.getProperty("$env");
603
+ if (envSym) {
604
+ envType = serialize(
605
+ checker.getTypeOfSymbolAtLocation(envSym, baseNode),
606
+ checker,
607
+ baseNode,
608
+ tsLib,
609
+ );
610
+ }
611
+
612
+ const baseCtxType = getCtxType(baseType);
613
+ if (!baseCtxType) {
614
+ throw new Error(
615
+ `Export "${options.exportName}" has no $ctx — not a Honey instance`,
616
+ );
617
+ }
618
+ const baseMwProps = extractMiddlewareProps(baseCtxType, checker, baseNode, tsLib);
619
+ const baseMiddlewareType = baseMwProps.length === 0 ? null
620
+ : `{ ${baseMwProps.map((p) => `${p.name}${p.opt ? "?" : ""}: ${p.type}`).join("; ")} }`;
621
+
622
+ /* build name→type map for reusing base type references in route middleware */
623
+ const basePropTypeByName = new Map<string, string>();
624
+ for (const p of baseMwProps) {
625
+ basePropTypeByName.set(p.name, p.type);
626
+ }
627
+
628
+ /* collect base middleware property names for diffing */
629
+ const baseMwPropNames = new Set<string>();
630
+ for (const p of baseCtxType.getProperties()) {
631
+ const name = p.getName();
632
+ if (!HONEY_CTX_PROPS.has(name) && !name.startsWith("_")) {
633
+ baseMwPropNames.add(name);
634
+ }
635
+ }
636
+
637
+ const sanitize = (s: string) => sanitizeImportPaths(s, options.outputDir);
638
+
639
+ /* scan for route-defining calls: <expr>.<httpMethod>(<pathLiteral>) */
640
+ const routeMiddleware: Record<string, string> = {};
641
+ const routeMiddlewareProps: Record<string, Array<{ name: string; opt: boolean; type: string }>> = {};
642
+ const compilerSourceFile: ts.SourceFile = bridge(
643
+ sourceFile.compilerNode,
644
+ );
645
+
646
+ function visitNode(node: ts.Node): void {
647
+ if (
648
+ tsLib.isCallExpression(node) &&
649
+ tsLib.isPropertyAccessExpression(node.expression)
650
+ ) {
651
+ const methodName = node.expression.name.text;
652
+ if (HTTP_METHODS.has(methodName) && node.arguments.length > 0) {
653
+ const pathArg = node.arguments[0];
654
+ if (tsLib.isStringLiteral(pathArg)) {
655
+ const path = pathArg.text;
656
+ /* get the type of the receiver (the expression before .get/.post/etc.) */
657
+ const receiverExpr = node.expression.expression;
658
+ const receiverType = checker.getTypeAtLocation(receiverExpr);
659
+ const ctxType = getCtxType(receiverType, receiverExpr);
660
+
661
+ if (ctxType) {
662
+ /* extract extra middleware props by iterating intersection MEMBERS individually.
663
+ * This preserves source-level declarations (e.g. ShorthandPropertyAssignment)
664
+ * that get lost when extracting from the flattened intersection. */
665
+ const extraEntries: MwPropEntry[] = [];
666
+ const dupeNames = new Set<string>();
667
+ const members = ctxType.isIntersection()
668
+ ? (ctxType as ts.IntersectionType).types
669
+ : [ctxType];
670
+ for (const member of members) {
671
+ const memberProps = extractMiddlewareProps(member, checker, baseNode, tsLib);
672
+ for (const rp of memberProps) {
673
+ if (baseMwPropNames.has(rp.name)) continue;
674
+ if (extraEntries.some((e) => e.name === rp.name)) {
675
+ dupeNames.add(rp.name);
676
+ continue;
677
+ }
678
+ extraEntries.push(rp);
679
+ }
680
+ }
681
+ /* for properties that appeared in multiple intersection members
682
+ * (e.g. auth narrowed by a later middleware), resolve from the
683
+ * flattened intersection so TypeScript computes the correct
684
+ * intersected type instead of using the first member's type */
685
+ for (const dupeName of dupeNames) {
686
+ const sym = ctxType.getProperty(dupeName);
687
+ if (!sym) continue;
688
+ const t = checker.getTypeOfSymbolAtLocation(sym, sym.valueDeclaration ?? baseNode);
689
+ const typeStr = serializeAsRef(t, checker, baseNode, tsLib) ?? serialize(t, checker, baseNode, tsLib);
690
+ const idx = extraEntries.findIndex((e) => e.name === dupeName);
691
+ if (idx !== -1) {
692
+ extraEntries[idx] = { ...extraEntries[idx], type: typeStr };
693
+ }
694
+ }
695
+
696
+ if (extraEntries.length > 0) {
697
+ /* reuse compact base type references when the route has the same property
698
+ * (e.g. shardDb: ShardDb from base → reuse import ref instead of structural expansion) */
699
+ const entries = extraEntries.map((e) => {
700
+ const baseRef = basePropTypeByName.get(e.name)
701
+ if (baseRef && baseRef.length < e.type.length) {
702
+ return { ...e, type: baseRef }
703
+ }
704
+ return e
705
+ });
706
+
707
+ const key = `${methodName} ${path}`;
708
+ routeMiddleware[key] = sanitize(
709
+ `{ ${entries.map((e) => `${e.name}${e.opt ? "?" : ""}: ${e.type}`).join("; ")} }`,
710
+ );
711
+ routeMiddlewareProps[key] = entries.map((e) => ({
712
+ name: e.name,
713
+ opt: e.opt,
714
+ type: sanitize(e.type),
715
+ }));
716
+ }
717
+ }
718
+ }
719
+ }
720
+ }
721
+
722
+ tsLib.forEachChild(node, visitNode);
723
+ }
724
+
725
+ visitNode(compilerSourceFile);
726
+
727
+ /* extract $taps */
728
+ let tapsType: string | null = null;
729
+ const tapsSym = baseType.getProperty("$taps");
730
+ if (tapsSym) {
731
+ const tapsT = checker.getTypeOfSymbolAtLocation(tapsSym, baseNode);
732
+ const tapsStr = serialize(tapsT, checker, baseNode, tsLib);
733
+ if (tapsStr !== "{}" && tapsStr !== "Record<string, unknown>") {
734
+ tapsType = tapsStr;
735
+ }
736
+ }
737
+
738
+ return {
739
+ base: {
740
+ envType: sanitize(envType),
741
+ middlewareType: baseMiddlewareType
742
+ ? sanitize(baseMiddlewareType)
743
+ : null,
744
+ tapsType: tapsType ? sanitize(tapsType) : null,
745
+ },
746
+ routeMiddleware,
747
+ routeMiddlewareProps,
748
+ };
749
+ }