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