@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
|
@@ -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
|
+
}
|