@openmirai/typeforge 0.1.7

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.
@@ -0,0 +1,2029 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
2
+ import { dirname, join, parse, relative, resolve } from "node:path";
3
+ import chalk from "chalk";
4
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
5
+ //#region src/json/types.ts
6
+ function isJsonPrimitive(value) {
7
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
8
+ }
9
+ function isJsonValue(value) {
10
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return true;
11
+ if (Array.isArray(value)) return value.every(isJsonValue);
12
+ if (typeof value === "object" && value !== null) return Object.values(value).every(isJsonValue);
13
+ return false;
14
+ }
15
+ function isJsonObject(value) {
16
+ return isJsonValue(value) && typeof value === "object" && value !== null && !Array.isArray(value);
17
+ }
18
+ function isJsonArray(value) {
19
+ return Array.isArray(value) && value.every(isJsonValue);
20
+ }
21
+ function parseJson(text) {
22
+ const parsed = JSON.parse(text);
23
+ if (!isJsonValue(parsed)) throw new TypeError("JSON text did not parse to a valid JSON value");
24
+ return parsed;
25
+ }
26
+ function readJsonObject(text) {
27
+ const parsed = parseJson(text);
28
+ if (!isJsonObject(parsed)) throw new TypeError("JSON text did not parse to a JSON object");
29
+ return parsed;
30
+ }
31
+ function readJsonPrimitives(values) {
32
+ if (!isJsonArray(values)) return [];
33
+ return values.filter(isJsonPrimitive);
34
+ }
35
+ //#endregion
36
+ //#region src/config/load.ts
37
+ function readOptionalJson(path) {
38
+ if (!existsSync(path)) return {};
39
+ try {
40
+ const raw = readJsonObject(readFileSync(path, "utf8"));
41
+ const config = {};
42
+ if (typeof raw["apiRoot"] === "string") config.apiRoot = raw["apiRoot"];
43
+ return config;
44
+ } catch {
45
+ return {};
46
+ }
47
+ }
48
+ function readPackageConfig(path) {
49
+ if (!existsSync(path)) return {};
50
+ try {
51
+ const raw = readJsonObject(readFileSync(path, "utf8"));
52
+ const typeforge = raw["typeforge"] ?? raw["openapiCodegen"];
53
+ if (typeof typeforge !== "object" || typeforge === null || Array.isArray(typeforge)) return {};
54
+ const config = {};
55
+ if (typeof typeforge["apiRoot"] === "string") config.apiRoot = typeforge["apiRoot"];
56
+ return config;
57
+ } catch {
58
+ return {};
59
+ }
60
+ }
61
+ function unwrapDefineSourceConfig(content) {
62
+ const match = content.match(/defineSourceConfig\s*(?:<[^>]*>)?\s*\(\s*\{([\s\S]*)\}\s*\)/);
63
+ if (match?.[1] !== void 0) return `{${match[1]}}`;
64
+ return content;
65
+ }
66
+ function parseSourceConfigContent(content) {
67
+ const normalized = unwrapDefineSourceConfig(content);
68
+ const config = {};
69
+ const pathPrefix = normalized.match(/pathPrefix:\s*["'`]([^"'`]+)["'`]/);
70
+ if (pathPrefix?.[1] !== void 0) config.pathPrefix = pathPrefix[1];
71
+ const functionsDir = normalized.match(/functionsDir:\s*["'`]([^"'`]+)["'`]/);
72
+ if (functionsDir?.[1] !== void 0) config.functionsDir = functionsDir[1];
73
+ const typesDir = normalized.match(/typesDir:\s*["'`]([^"'`]+)["'`]/);
74
+ if (typesDir?.[1] !== void 0) config.typesDir = typesDir[1];
75
+ const ignoreMatch = normalized.match(/ignorePaths:\s*\[([\s\S]*?)\]/);
76
+ if (ignoreMatch?.[1] !== void 0) {
77
+ const paths = [...ignoreMatch[1].matchAll(/["'`]([^"'`]+)["'`]/g)].map((match) => match[1]).filter((path) => path !== void 0);
78
+ if (paths.length > 0) config.ignorePaths = paths;
79
+ }
80
+ if (/stripApiPrefix:\s*true/.test(normalized)) config.stripApiPrefix = true;
81
+ const routeEnumName = normalized.match(/routeEnumName:\s*["'`]([^"'`]+)["'`]/);
82
+ if (routeEnumName?.[1] !== void 0) config.routeEnumName = routeEnumName[1];
83
+ const generationMode = normalized.match(/generationMode:\s*["'`](authoritative|merge)["'`]/);
84
+ if (generationMode?.[1] === "authoritative" || generationMode?.[1] === "merge") config.generationMode = generationMode[1];
85
+ const naming = normalized.match(/naming:\s*["'`](path|operationId)["'`]/);
86
+ if (naming?.[1] === "path" || naming?.[1] === "operationId") config.naming = naming[1];
87
+ if (/resolveMapKeyRefs:\s*false/.test(normalized)) config.resolveMapKeyRefs = false;
88
+ if (/unwrapResponseData:\s*true/.test(normalized)) config.unwrapResponseData = true;
89
+ if (/tanstackQuery:\s*true/.test(normalized)) config.tanstackQuery = true;
90
+ const importBase = normalized.match(/importBase:\s*["'`]([^"'`]+)["'`]/);
91
+ if (importBase?.[1] !== void 0) config.importBase = importBase[1];
92
+ const maxRenderDepth = normalized.match(/maxRenderDepth:\s*(\d+)/)?.[1];
93
+ if (maxRenderDepth !== void 0) config.maxRenderDepth = Number.parseInt(maxRenderDepth, 10);
94
+ const queryExtends = parseQueryExtends(normalized);
95
+ if (queryExtends !== void 0) config.queryExtends = queryExtends;
96
+ const spec = normalized.match(/spec:\s*["'`]([^"'`]+)["'`]/);
97
+ if (spec?.[1] !== void 0) config.spec = spec[1];
98
+ return config;
99
+ }
100
+ function parseQueryExtends(content) {
101
+ const block = content.match(/queryExtends:\s*\{([\s\S]*?)\}/)?.[1];
102
+ if (block === void 0) return;
103
+ const config = {};
104
+ const read = (key) => block.match(new RegExp(`${key}:\\s*["'\`]([^"'\`]+)["'\`]`))?.[1];
105
+ const page = read("page");
106
+ const limit = read("limit");
107
+ const sortBy = read("sortBy");
108
+ const sortOrder = read("sortOrder");
109
+ const paginationTypeName = read("paginationTypeName");
110
+ const paginationImportPath = read("paginationImportPath");
111
+ const sortTypeName = read("sortTypeName");
112
+ const sortImportPath = read("sortImportPath");
113
+ if (page !== void 0) config.page = page;
114
+ if (limit !== void 0) config.limit = limit;
115
+ if (sortBy !== void 0) config.sortBy = sortBy;
116
+ if (sortOrder !== void 0) config.sortOrder = sortOrder;
117
+ if (paginationTypeName !== void 0) config.paginationTypeName = paginationTypeName;
118
+ if (paginationImportPath !== void 0) config.paginationImportPath = paginationImportPath;
119
+ if (sortTypeName !== void 0) config.sortTypeName = sortTypeName;
120
+ if (sortImportPath !== void 0) config.sortImportPath = sortImportPath;
121
+ return Object.keys(config).length > 0 ? config : void 0;
122
+ }
123
+ function loadProjectConfig(cwd) {
124
+ const fromJson = readOptionalJson(resolve(cwd, "typeforge.json"));
125
+ const fromLegacyJson = readOptionalJson(resolve(cwd, "openapi-codegen.json"));
126
+ const fromPackage = readPackageConfig(resolve(cwd, "package.json"));
127
+ return { apiRoot: fromJson.apiRoot ?? fromLegacyJson.apiRoot ?? fromPackage.apiRoot ?? "src/api" };
128
+ }
129
+ function loadSourceConfig(cwd, apiRoot, sourceKey) {
130
+ const sourcePath = resolve(cwd, apiRoot, sourceKey, "source.ts");
131
+ if (!existsSync(sourcePath)) return {};
132
+ return parseSourceConfigContent(readFileSync(sourcePath, "utf8"));
133
+ }
134
+ function readModelsFile(cwd, apiRoot) {
135
+ const modelsPath = resolve(cwd, apiRoot, "models.ts");
136
+ if (!existsSync(modelsPath)) return;
137
+ return readFileSync(modelsPath, "utf8");
138
+ }
139
+ function hasQueryScopeFile(cwd, apiRoot) {
140
+ return existsSync(resolve(cwd, apiRoot, "query-scope.ts"));
141
+ }
142
+ function detectHttpMode(cwd, apiRoot) {
143
+ const httpPath = resolve(cwd, apiRoot, "http.ts");
144
+ if (!existsSync(httpPath)) return "injected";
145
+ const content = readFileSync(httpPath, "utf8");
146
+ if (/export\s+(const|function)\s+httpFetch\b/.test(content)) return "singleton";
147
+ if (/export\s*\{[^}]*\bhttpFetch\b/.test(content)) return "singleton";
148
+ return "injected";
149
+ }
150
+ function listSourceKeys(cwd, apiRoot) {
151
+ const apiRootPath = resolve(cwd, apiRoot);
152
+ if (!existsSync(apiRootPath)) return [];
153
+ return readdirSync(apiRootPath).filter((entry) => {
154
+ const entryPath = join(apiRootPath, entry);
155
+ if (!statSync(entryPath).isDirectory()) return false;
156
+ return existsSync(join(entryPath, "source.ts"));
157
+ });
158
+ }
159
+ //#endregion
160
+ //#region src/utils/tsconfig-paths.ts
161
+ /**
162
+ * Strip line comments (//) and block comments from a JSON string, and
163
+ * remove trailing commas before closing braces/brackets.
164
+ * Handles tsconfig.json / jsonc format.
165
+ */
166
+ function stripJsonComments(text) {
167
+ let result = "";
168
+ let i = 0;
169
+ const len = text.length;
170
+ let inString = false;
171
+ while (i < len) {
172
+ const ch = text[i];
173
+ if (inString) {
174
+ result += ch;
175
+ if (ch === "\\") {
176
+ i++;
177
+ if (i < len) result += text[i];
178
+ } else if (ch === "\"") inString = false;
179
+ i++;
180
+ continue;
181
+ }
182
+ if (ch === "\"") {
183
+ inString = true;
184
+ result += ch;
185
+ i++;
186
+ continue;
187
+ }
188
+ if (ch === "/" && text[i + 1] === "/") {
189
+ while (i < len && text[i] !== "\n") i++;
190
+ continue;
191
+ }
192
+ if (ch === "/" && text[i + 1] === "*") {
193
+ i += 2;
194
+ while (i < len && !(text[i] === "*" && text[i + 1] === "/")) i++;
195
+ i += 2;
196
+ continue;
197
+ }
198
+ result += ch;
199
+ i++;
200
+ }
201
+ return result.replace(/,(\s*[}\]])/g, "$1");
202
+ }
203
+ /**
204
+ * Load `compilerOptions.paths` and `baseUrl` from the nearest `tsconfig.json`
205
+ * found at or above `startDir`. Returns `undefined` when no tsconfig has paths.
206
+ */
207
+ function loadTsconfigPaths(startDir) {
208
+ let currentDir = resolve(startDir);
209
+ const rootDir = parse(currentDir).root;
210
+ while (true) {
211
+ const config = readTsconfigPaths(currentDir);
212
+ if (config !== void 0) return config;
213
+ if (currentDir === rootDir) return;
214
+ currentDir = dirname(currentDir);
215
+ }
216
+ }
217
+ function readTsconfigPaths(configDir) {
218
+ const tsconfigPath = resolve(configDir, "tsconfig.json");
219
+ if (!existsSync(tsconfigPath)) return;
220
+ try {
221
+ const raw = JSON.parse(stripJsonComments(readFileSync(tsconfigPath, "utf8")));
222
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return;
223
+ const opts = raw["compilerOptions"];
224
+ if (typeof opts !== "object" || opts === null || Array.isArray(opts)) return;
225
+ const compilerOptions = opts;
226
+ const rawPaths = compilerOptions["paths"];
227
+ if (typeof rawPaths !== "object" || rawPaths === null || Array.isArray(rawPaths)) return;
228
+ const baseUrl = typeof compilerOptions["baseUrl"] === "string" ? compilerOptions["baseUrl"] : ".";
229
+ const normalizedPaths = {};
230
+ for (const [key, value] of Object.entries(rawPaths)) if (Array.isArray(value) && value.every((v) => typeof v === "string")) normalizedPaths[key] = value;
231
+ if (Object.keys(normalizedPaths).length === 0) return;
232
+ return {
233
+ baseDir: configDir,
234
+ paths: normalizedPaths,
235
+ resolvedBaseUrl: resolve(configDir, baseUrl)
236
+ };
237
+ } catch {
238
+ return;
239
+ }
240
+ }
241
+ /**
242
+ * Try to find a tsconfig path alias for `absoluteTarget`.
243
+ *
244
+ * Returns the alias import string (e.g. `@mirai/utils/api/v2/generated/runtime`)
245
+ * when a match is found, or `undefined` when no alias covers the target.
246
+ *
247
+ * Supports:
248
+ * - Wildcard patterns: `"@foo/*"` → `["packages/foo/src/*"]`
249
+ * - Exact patterns: `"@foo/bar"` → `["packages/foo/src/bar"]`
250
+ */
251
+ function resolveAliasImport(absoluteTarget, config) {
252
+ for (const [alias, mappings] of Object.entries(config.paths)) for (const mapping of mappings) if (alias.endsWith("/*") && mapping.endsWith("/*")) {
253
+ const aliasBase = alias.slice(0, -2);
254
+ const mappingBase = mapping.slice(0, -2);
255
+ const resolvedBase = resolve(config.resolvedBaseUrl, mappingBase);
256
+ if (absoluteTarget.startsWith(`${resolvedBase}/`)) return `${aliasBase}/${absoluteTarget.slice(resolvedBase.length + 1)}`;
257
+ if (absoluteTarget === resolvedBase) return aliasBase;
258
+ } else if (!alias.includes("*") && !mapping.includes("*")) {
259
+ const resolvedMapping = resolve(config.resolvedBaseUrl, mapping);
260
+ if (stripTsExtension(absoluteTarget) === stripTsExtension(resolvedMapping)) return alias;
261
+ }
262
+ }
263
+ function stripTsExtension(p) {
264
+ return p.replace(/\.(d\.ts|ts|js)$/, "");
265
+ }
266
+ //#endregion
267
+ //#region src/utils/imports.ts
268
+ function stripTypeScriptExtension(filePath) {
269
+ return filePath.replace(/\.d\.ts$/, "").replace(/\.ts$/, "");
270
+ }
271
+ function relativeImportPath(fromFilePath, toPathWithoutExtension) {
272
+ const fromDir = dirname(stripTypeScriptExtension(fromFilePath));
273
+ const toPath = stripTypeScriptExtension(toPathWithoutExtension);
274
+ const rel = relative(fromDir, toPath).replace(/\\/g, "/");
275
+ if (rel.startsWith(".")) return rel;
276
+ return `./${rel}`;
277
+ }
278
+ /**
279
+ * Minimum number of `../` segments before we prefer an alias import over a
280
+ * relative one. Paths with fewer segments are already concise.
281
+ */
282
+ const MIN_DOTDOT_FOR_ALIAS = 3;
283
+ /**
284
+ * Resolve the best import path from `fromAbsolutePath` to `toAbsolutePath`.
285
+ *
286
+ * Resolution priority:
287
+ * 1. `importBase` explicit override (e.g. `@mirai/utils/src/api/v2/generated`)
288
+ * 2. tsconfig path alias auto-detection (when relative has ≥3 `../` segments)
289
+ * 3. Relative fallback
290
+ */
291
+ function resolveAliasAwareImport(options) {
292
+ const { fromAbsolutePath, toAbsolutePath, importBase, generatedDir, tsconfigPaths } = options;
293
+ if (importBase !== void 0 && generatedDir !== void 0) {
294
+ const strippedGenDir = generatedDir.replace(/\/$/, "");
295
+ const strippedTarget = stripTypeScriptExtension(toAbsolutePath);
296
+ if (strippedTarget.startsWith(`${strippedGenDir}/`)) return `${importBase}/${strippedTarget.slice(strippedGenDir.length + 1)}`;
297
+ if (strippedTarget === strippedGenDir) return importBase;
298
+ }
299
+ const rel = relativeImportPath(fromAbsolutePath, toAbsolutePath);
300
+ if (tsconfigPaths !== void 0) {
301
+ if ((rel.match(/\.\.\//g) ?? []).length >= MIN_DOTDOT_FOR_ALIAS) {
302
+ const aliasImport = resolveAliasImport(toAbsolutePath, tsconfigPaths);
303
+ if (aliasImport !== void 0) return aliasImport;
304
+ }
305
+ }
306
+ return rel;
307
+ }
308
+ /**
309
+ * Compute the absolute path of a generated function file.
310
+ *
311
+ * @param functionsDir - absolute path to the `functions/` directory
312
+ * @param cleanPath - path segment like `api/v2/auth/email/validate`
313
+ * @param method - HTTP method in uppercase, e.g. `GET`
314
+ */
315
+ function functionFileAbsPath(functionsDir, cleanPath, method) {
316
+ return join(functionsDir, cleanPath, `${method}.ts`);
317
+ }
318
+ //#endregion
319
+ //#region src/utils/naming.ts
320
+ function pathToEnumName(routePath) {
321
+ return routePath.split("/").filter(Boolean).map((segment) => {
322
+ return segment.replace(/[{}]/g, "").replace(/[^a-zA-Z0-9]/g, "_").toUpperCase();
323
+ }).join("_");
324
+ }
325
+ function pathToRouteValue(routePath, stripApiPrefix = false) {
326
+ return (stripApiPrefix ? routePath.replace(/^\/api\//, "/") : routePath).replace(/\{(\w+)\}/g, ":$1");
327
+ }
328
+ function pathToFunctionName(cleanPath, method) {
329
+ const resourceName = cleanPath.split("/").filter(Boolean).map((segment) => segment.replace(/[{[\]}/]/g, "").split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("")).join("");
330
+ return `${method === "get" ? "get" : method}${resourceName}`;
331
+ }
332
+ function schemaKindLabel(schema) {
333
+ if (schema.kind === "array") return "array";
334
+ if (schema.kind === "object") return "object";
335
+ if (schema.kind === "ref") return `ref:${schema.ref ?? "unknown"}`;
336
+ if (schema.kind === "oneOf" || schema.kind === "anyOf" || schema.kind === "allOf") return schema.kind;
337
+ if (schema.enum !== void 0 && schema.enum.length > 0) return "enum";
338
+ return schema.kind;
339
+ }
340
+ function isSuccessStatusCode(statusCode) {
341
+ const code = Number.parseInt(statusCode, 10);
342
+ return !Number.isNaN(code) && code >= 200 && code < 300;
343
+ }
344
+ //#endregion
345
+ //#region src/utils/path-params.ts
346
+ function renderPathParamTypeFromSchema(schema) {
347
+ if (schema.enum !== void 0 && schema.enum.length > 0) return schema.enum.map((value) => JSON.stringify(value)).join(" | ");
348
+ if (schema.kind === "number") return "number";
349
+ if (schema.kind === "boolean") return "boolean";
350
+ return "string";
351
+ }
352
+ function resolvePathParamSchemas(pathItem) {
353
+ const schemas = /* @__PURE__ */ new Map();
354
+ for (const operation of pathItem.operations) for (const param of operation.pathParams) if (!schemas.has(param.name)) schemas.set(param.name, param.schema);
355
+ return schemas;
356
+ }
357
+ function renderPathParamType(schemas, param) {
358
+ const schema = schemas.get(param);
359
+ if (schema === void 0) return "string";
360
+ return renderPathParamTypeFromSchema(schema);
361
+ }
362
+ //#endregion
363
+ //#region src/utils/type-names.ts
364
+ function getFunctionTypeName(cleanPath, method) {
365
+ const parts = cleanPath.split("/").filter(Boolean).map((segment) => segment.replace(/[{[\]}/]/g, "").split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("")).join("");
366
+ return `${method.toUpperCase()}${parts}`;
367
+ }
368
+ function hasMeaningfulRequestBody(operation) {
369
+ const schema = operation.requestBody?.schema;
370
+ if (schema === void 0) return false;
371
+ return !isEmptySchema(schema);
372
+ }
373
+ function isEmptySchema(schema) {
374
+ if (schema.kind === "unknown") return true;
375
+ if (schema.kind === "object") {
376
+ if (schema.properties === void 0) return true;
377
+ return Object.keys(schema.properties).length === 0;
378
+ }
379
+ return false;
380
+ }
381
+ function getSuccessResponseSchema(operation) {
382
+ return operation.responses.find((response) => isSuccessStatusCode(response.statusCode) && response.schema !== void 0)?.schema;
383
+ }
384
+ //#endregion
385
+ //#region src/emitters/functions/index.ts
386
+ function extractPathParams(routePath) {
387
+ return (routePath.match(/\{([^}]+)\}/g) ?? []).map((match) => match.slice(1, -1));
388
+ }
389
+ function getTypeImportPath(cleanPath, method, options) {
390
+ const normalizedPath = cleanPath.replace(/\{([^}]+)\}/g, "[$1]").split("/").filter(Boolean).join("/");
391
+ const fromAbs = functionFileAbsPath(options.functionsDir, cleanPath, method.toUpperCase());
392
+ const toAbs = join(options.typesDir, normalizedPath, method.toUpperCase());
393
+ return resolveAliasAwareImport({
394
+ fromAbsolutePath: fromAbs,
395
+ generatedDir: options.generatedDir,
396
+ toAbsolutePath: toAbs,
397
+ ...options.importBase !== void 0 ? { importBase: options.importBase } : {},
398
+ ...options.tsconfigPaths !== void 0 ? { tsconfigPaths: options.tsconfigPaths } : {}
399
+ });
400
+ }
401
+ function getRuntimeImportPath(cleanPath, options, method) {
402
+ const fromAbs = functionFileAbsPath(options.functionsDir, cleanPath, method.toUpperCase());
403
+ const toAbs = join(options.generatedDir, "runtime");
404
+ return resolveAliasAwareImport({
405
+ fromAbsolutePath: fromAbs,
406
+ generatedDir: options.generatedDir,
407
+ toAbsolutePath: toAbs,
408
+ ...options.importBase !== void 0 ? { importBase: options.importBase } : {},
409
+ ...options.tsconfigPaths !== void 0 ? { tsconfigPaths: options.tsconfigPaths } : {}
410
+ });
411
+ }
412
+ function renderOperationPathParamType(operation, schemas, param) {
413
+ const pathParam = operation.pathParams.find((entry) => entry.name === param);
414
+ if (pathParam !== void 0) return renderPathParamType(/* @__PURE__ */ new Map([[param, pathParam.schema]]), param);
415
+ return renderPathParamType(schemas, param);
416
+ }
417
+ function emitFunctionFiles(options) {
418
+ const files = [];
419
+ for (const pathItem of options.paths) for (const operation of pathItem.operations) files.push({
420
+ content: renderFunctionFile(pathItem, operation, options),
421
+ relativePath: `${pathItem.cleanPath}/${operation.method.toUpperCase()}.ts`
422
+ });
423
+ return files;
424
+ }
425
+ function renderFunctionFile(pathItem, operation, options) {
426
+ const method = operation.method;
427
+ const functionName = pathToFunctionName(pathItem.cleanPath, method);
428
+ const enumName = pathToEnumName(pathItem.path);
429
+ const typeName = getFunctionTypeName(pathItem.cleanPath, method);
430
+ const pathParams = extractPathParams(pathItem.path);
431
+ const pathParamSchemas = resolvePathParamSchemas(pathItem);
432
+ const propsTypeName = `${capitalize(functionName)}Props`;
433
+ const queryOptionsPropsTypeName = `${capitalize(functionName)}QueryOptionsProps`;
434
+ const hasRequestBody = method !== "get" && hasMeaningfulRequestBody(operation);
435
+ const queryParamsPresent = operation.queryParams.length > 0;
436
+ const typeImportPath = getTypeImportPath(pathItem.cleanPath, method, options);
437
+ const runtimeImportPath = getRuntimeImportPath(pathItem.cleanPath, options, method);
438
+ const lines = [
439
+ "// Auto-generated from OpenAPI spec",
440
+ `// Path: ${method.toUpperCase()} ${pathItem.path}`,
441
+ "// DO NOT EDIT - This file is automatically generated",
442
+ ""
443
+ ];
444
+ lines.push("import type {");
445
+ lines.push(` ${typeName}Response,`);
446
+ if (queryParamsPresent) lines.push(` ${typeName}Params,`);
447
+ if (hasRequestBody) lines.push(` ${typeName}Body,`);
448
+ lines.push(`} from "${typeImportPath}";`);
449
+ lines.push("");
450
+ const routeTargetsImport = options.hasQueryScope ? ", RouteTargets" : "";
451
+ if (options.httpMode === "singleton") lines.push(`import { httpFetch, Routes${routeTargetsImport}${options.hasQueryScope ? ", getQueryScopeKey, queryOptions" : ""} } from "${runtimeImportPath}";`);
452
+ else lines.push(`import { Routes${routeTargetsImport}${options.hasQueryScope ? ", getQueryScopeKey, queryOptions" : ""} } from "${runtimeImportPath}";`);
453
+ const httpFetchTypeImport = options.httpMode === "injected" ? "HTTPFetch, " : "";
454
+ const queryParamsTypeImport = queryParamsPresent ? "" : ", QueryParams";
455
+ lines.push(`import type { ${httpFetchTypeImport}HTTPFetchConfig${queryParamsTypeImport}${options.hasQueryScope ? ", QueryScope" : ""} } from "${runtimeImportPath}";`);
456
+ lines.push("");
457
+ lines.push(`export interface ${propsTypeName} {`);
458
+ if (options.httpMode === "injected") lines.push(" http: HTTPFetch;");
459
+ for (const param of pathParams) lines.push(` ${param}: ${renderOperationPathParamType(operation, pathParamSchemas, param)};`);
460
+ if (queryParamsPresent) {
461
+ const optional = operation.queryParams.some((param) => param.required) ? "" : "?";
462
+ lines.push(` params${optional}: ${typeName}Params;`);
463
+ }
464
+ if (hasRequestBody) lines.push(` body: ${typeName}Body;`);
465
+ const configType = queryParamsPresent ? `Omit<HTTPFetchConfig<${typeName}Params, ${typeName}Response>, "params" | "signal">` : `Omit<HTTPFetchConfig<QueryParams, ${typeName}Response>, "params" | "signal">`;
466
+ lines.push(` config?: ${configType};`);
467
+ lines.push(" signal?: AbortSignal;");
468
+ lines.push("}");
469
+ lines.push("");
470
+ if (method === "get" && options.hasQueryScope) {
471
+ lines.push(`export interface ${queryOptionsPropsTypeName} extends ${propsTypeName} {`);
472
+ lines.push(" queryScope: QueryScope;");
473
+ lines.push("}");
474
+ lines.push("");
475
+ }
476
+ const httpClient = options.httpMode === "singleton" ? "httpFetch" : "props.http";
477
+ const destructuredProps = [
478
+ ...options.httpMode === "injected" ? ["http"] : [],
479
+ ...pathParams.map((param) => param),
480
+ ...queryParamsPresent ? ["params"] : [],
481
+ ...hasRequestBody ? ["body"] : [],
482
+ "config",
483
+ "signal"
484
+ ];
485
+ lines.push(`export async function ${functionName}(props: ${propsTypeName}): Promise<${typeName}Response> {`);
486
+ if (destructuredProps.length > 0) {
487
+ lines.push(` const { ${destructuredProps.join(", ")} } = props;`);
488
+ lines.push("");
489
+ }
490
+ let routeCall = `Routes.${enumName}`;
491
+ if (pathParams.length > 0) routeCall = `Routes.${enumName}({ ${pathParams.map((param) => `${param}`).join(", ")} })`;
492
+ const fetchConfig = queryParamsPresent ? `{ ...config, params, signal }` : "{ ...config, signal }";
493
+ const getGeneric = queryParamsPresent ? `<${typeName}Response, ${typeName}Params>` : `<${typeName}Response>`;
494
+ const mutationGeneric = `<${typeName}Response, ${hasRequestBody ? `${typeName}Body` : "undefined"}${queryParamsPresent ? `, ${typeName}Params` : ""}>`;
495
+ const mutationBody = hasRequestBody ? "body" : "undefined";
496
+ const mutationConfig = queryParamsPresent ? "{ ...config, params, signal }" : "{ ...config, signal }";
497
+ switch (method) {
498
+ case "get":
499
+ lines.push(` const { data } = await ${httpClient}.get${getGeneric}(${routeCall}, ${fetchConfig});`);
500
+ break;
501
+ case "post":
502
+ lines.push(` const { data } = await ${httpClient}.post${mutationGeneric}(${routeCall}, ${mutationBody}, ${mutationConfig});`);
503
+ break;
504
+ case "put":
505
+ lines.push(` const { data } = await ${httpClient}.put${mutationGeneric}(${routeCall}, ${mutationBody}, ${mutationConfig});`);
506
+ break;
507
+ case "patch":
508
+ lines.push(` const { data } = await ${httpClient}.patch${mutationGeneric}(${routeCall}, ${mutationBody}, ${mutationConfig});`);
509
+ break;
510
+ case "delete": {
511
+ let deleteConfig = fetchConfig;
512
+ if (hasRequestBody) deleteConfig = queryParamsPresent ? "{ ...config, data: body, params, signal }" : "{ ...config, data: body, signal }";
513
+ lines.push(` const { data } = await ${httpClient}.delete${getGeneric}(${routeCall}, ${deleteConfig});`);
514
+ break;
515
+ }
516
+ }
517
+ lines.push(" return data;");
518
+ lines.push("}");
519
+ if (method === "get" && options.hasQueryScope) {
520
+ const queryOptionsName = `${functionName}QueryOptions`;
521
+ lines.push("");
522
+ lines.push(`export function ${queryOptionsName}(props: ${queryOptionsPropsTypeName}) {`);
523
+ lines.push(` const { ${queryParamsPresent ? "params, " : ""}queryScope } = props;`);
524
+ lines.push(" return queryOptions({");
525
+ lines.push(` queryKey: [RouteTargets.${enumName}, ...getQueryScopeKey(queryScope)${pathParams.length > 0 ? `, ${pathParams.map((param) => `props.${param}`).join(", ")}` : ""}${queryParamsPresent ? ", params" : ""}],`);
526
+ lines.push(` queryFn: ({ signal }) => ${functionName}({ ...props, signal }).then((data) => data),`);
527
+ if (pathParams.length > 0) lines.push(` enabled: [${pathParams.map((param) => `props.${param}`).join(", ")}].every(Boolean),`);
528
+ lines.push(" });");
529
+ lines.push("}");
530
+ }
531
+ lines.push("");
532
+ return lines.join("\n");
533
+ }
534
+ function capitalize(value) {
535
+ return value.charAt(0).toUpperCase() + value.slice(1);
536
+ }
537
+ //#endregion
538
+ //#region src/emitters/runtime/index.ts
539
+ function emitRuntimeFile(options) {
540
+ const lines = [
541
+ "// Auto-generated from OpenAPI spec",
542
+ "// DO NOT EDIT - This file is automatically generated",
543
+ "",
544
+ `export type { HTTPFetch, HTTPFetchConfig, QueryParams } from "../../http";`
545
+ ];
546
+ if (options.httpMode === "singleton") lines.push(`export { httpFetch } from "../../http";`);
547
+ lines.push(`export { Routes, RouteTargets, buildRoute } from "./routes";`);
548
+ if (options.hasQueryScope) {
549
+ lines.push(`export type { QueryScope } from "../../query-scope";`);
550
+ lines.push(`export { getQueryScopeKey } from "../../query-scope";`);
551
+ lines.push(`export { queryOptions } from "@tanstack/react-query";`);
552
+ }
553
+ lines.push("");
554
+ return lines.join("\n");
555
+ }
556
+ //#endregion
557
+ //#region src/emitters/routes/index.ts
558
+ function extractPathParamNames(routePath) {
559
+ return (routePath.match(/\{([^}]+)\}/g) ?? []).map((match) => match.slice(1, -1));
560
+ }
561
+ function collectRouteEntries(options) {
562
+ const seen = /* @__PURE__ */ new Set();
563
+ const entries = [];
564
+ for (const pathItem of options.paths) {
565
+ const enumName = pathToEnumName(pathItem.path);
566
+ if (seen.has(enumName)) continue;
567
+ seen.add(enumName);
568
+ entries.push({
569
+ enumName,
570
+ pathParamNames: extractPathParamNames(pathItem.path),
571
+ pathParamSchemas: resolvePathParamSchemas(pathItem),
572
+ routeValue: pathToRouteValue(pathItem.path, options.stripApiPrefix === true)
573
+ });
574
+ }
575
+ return entries;
576
+ }
577
+ function renderRouteParamsType(entries) {
578
+ const lines = ["export type RouteParams = {"];
579
+ for (const entry of entries) {
580
+ if (entry.pathParamNames.length === 0) {
581
+ lines.push(` ${entry.enumName}: undefined;`);
582
+ continue;
583
+ }
584
+ lines.push(` ${entry.enumName}: {`);
585
+ for (const param of entry.pathParamNames) lines.push(` ${param}: ${renderPathParamType(entry.pathParamSchemas, param)};`);
586
+ lines.push(" };");
587
+ }
588
+ lines.push("};", "export type RouteKey = keyof RouteParams;", "");
589
+ return lines;
590
+ }
591
+ function emitRoutesFile(options) {
592
+ const entries = collectRouteEntries(options);
593
+ const lines = [
594
+ "// Auto-generated from OpenAPI spec",
595
+ "// DO NOT EDIT - This file is automatically generated",
596
+ "",
597
+ "import {",
598
+ " buildRouteFromHandlers,",
599
+ " createRouteHandlers,",
600
+ "} from \"@openmirai/typeforge/routes\";",
601
+ "",
602
+ ...renderRouteParamsType(entries),
603
+ `export enum ${options.routeEnumName} {`
604
+ ];
605
+ for (const entry of entries) lines.push(` ${entry.enumName} = "${entry.routeValue}",`);
606
+ lines.push("}", "");
607
+ if (options.routeEnumName !== "RouteTargets") {
608
+ lines.push(`export { ${options.routeEnumName} as RouteTargets };`);
609
+ lines.push("");
610
+ }
611
+ lines.push(`export const Routes = createRouteHandlers<RouteParams>(${options.routeEnumName});`);
612
+ lines.push("export const buildRoute = buildRouteFromHandlers<RouteParams>(Routes);", "");
613
+ return lines.join("\n");
614
+ }
615
+ function mergeRoutesFile(existingContent, newContent, routeEnumName, pathPrefix) {
616
+ const existingEntries = parseEnumEntries(existingContent, routeEnumName);
617
+ const newEntries = parseEnumEntries(newContent, routeEnumName);
618
+ if (pathPrefix !== void 0) {
619
+ for (const [name, value] of existingEntries.entries()) if (value.startsWith(pathPrefix) && !newEntries.has(name)) existingEntries.delete(name);
620
+ }
621
+ return emitRoutesFile({
622
+ paths: [...new Map([...existingEntries, ...newEntries]).entries()].map(([, value]) => ({
623
+ cleanPath: value.replace(/:[^/]+/g, (match) => `{${match.slice(1)}}`),
624
+ operations: [],
625
+ path: value.includes(":") ? value.replace(/:([^/]+)/g, "{$1}") : value
626
+ })),
627
+ routeEnumName,
628
+ stripApiPrefix: false
629
+ });
630
+ }
631
+ function parseEnumEntries(content, enumName) {
632
+ const entries = /* @__PURE__ */ new Map();
633
+ const enumStart = content.indexOf(`export enum ${enumName} {`);
634
+ if (enumStart === -1) return entries;
635
+ const enumBody = content.slice(enumStart);
636
+ const enumEnd = enumBody.indexOf("}");
637
+ if (enumEnd === -1) return entries;
638
+ const enumContent = enumBody.slice(0, enumEnd);
639
+ const staticRegex = /(\w+)\s*=\s*"([^"]+)"/g;
640
+ let match;
641
+ while ((match = staticRegex.exec(enumContent)) !== null) {
642
+ const name = match[1];
643
+ const value = match[2];
644
+ if (name !== void 0 && value !== void 0) entries.set(name, value);
645
+ }
646
+ return entries;
647
+ }
648
+ //#endregion
649
+ //#region src/emitters/resolve-schema.ts
650
+ function resolveRef(schema, components) {
651
+ if (schema.kind !== "ref" || schema.ref === void 0) return schema;
652
+ const refName = schema.ref.split("/").pop();
653
+ if (refName === void 0 || components[refName] === void 0) return { kind: "unknown" };
654
+ return components[refName];
655
+ }
656
+ /**
657
+ * Resolve an object schema through component references and `allOf` composition.
658
+ * The returned object is a new flattened view; component schemas are never mutated.
659
+ */
660
+ function resolveObjectSchema$1(schema, components, visitedRefs = /* @__PURE__ */ new Set()) {
661
+ if (schema.kind === "object") return schema;
662
+ if (schema.kind === "ref") {
663
+ const refName = refNameFromSchema(schema);
664
+ if (refName === void 0 || visitedRefs.has(refName)) return;
665
+ const resolved = components[refName];
666
+ if (resolved === void 0) return;
667
+ return resolveObjectSchema$1(resolved, components, /* @__PURE__ */ new Set([...visitedRefs, refName]));
668
+ }
669
+ if (schema.kind !== "allOf" || schema.allOf === void 0) return;
670
+ const properties = {};
671
+ const required = /* @__PURE__ */ new Set();
672
+ for (const member of schema.allOf) {
673
+ const resolved = resolveObjectSchema$1(member, components, visitedRefs);
674
+ if (resolved?.properties === void 0) return;
675
+ for (const [name, property] of Object.entries(resolved.properties)) {
676
+ const existing = properties[name];
677
+ properties[name] = existing === void 0 ? { ...property } : {
678
+ required: existing.required || property.required,
679
+ schema: JSON.stringify(existing.schema) === JSON.stringify(property.schema) ? existing.schema : {
680
+ allOf: [existing.schema, property.schema],
681
+ kind: "allOf"
682
+ }
683
+ };
684
+ if (property.required) required.add(name);
685
+ }
686
+ }
687
+ return {
688
+ kind: "object",
689
+ properties,
690
+ required: [...required]
691
+ };
692
+ }
693
+ function refNameFromSchema(schema) {
694
+ if (schema.kind !== "ref" || schema.ref === void 0) return;
695
+ return schema.ref.split("/").pop();
696
+ }
697
+ //#endregion
698
+ //#region src/envelope-guard/index.ts
699
+ function resolveSchema(schema, components) {
700
+ if (schema.kind === "ref") return resolveRef(schema, components);
701
+ return schema;
702
+ }
703
+ function extractEnvelopeShape(schema, components) {
704
+ if (schema === void 0) return;
705
+ const resolved = resolveObjectSchema$1(schema, components);
706
+ if (resolved?.properties === void 0) return;
707
+ const fields = [];
708
+ for (const [name, property] of Object.entries(resolved.properties)) fields.push({
709
+ kind: name === "data" ? "generic" : schemaKindLabel(resolveSchema(property.schema, components)),
710
+ name,
711
+ required: property.required
712
+ });
713
+ fields.sort((a, b) => a.name.localeCompare(b.name));
714
+ return {
715
+ fields,
716
+ schema: resolved
717
+ };
718
+ }
719
+ function fingerprint(shape) {
720
+ return JSON.stringify(shape.fields.map((field) => ({
721
+ kind: field.kind,
722
+ name: field.name,
723
+ required: field.required
724
+ })));
725
+ }
726
+ function matchesEnvelopeShape(schema, components, expected) {
727
+ const actual = extractEnvelopeShape(schema, components);
728
+ return actual !== void 0 && fingerprint(actual) === fingerprint(expected);
729
+ }
730
+ const ENVELOPE_METADATA_FIELDS = /* @__PURE__ */ new Set([
731
+ "error",
732
+ "message",
733
+ "requestId",
734
+ "success",
735
+ "timestamp"
736
+ ]);
737
+ function looksLikeEnvelope(shape) {
738
+ const names = new Set(shape.fields.map((field) => field.name));
739
+ if (names.has("data")) return true;
740
+ if (!names.has("success")) return false;
741
+ return [...names].every((name) => ENVELOPE_METADATA_FIELDS.has(name));
742
+ }
743
+ function isEnvelopeSchema(schema, components) {
744
+ const shape = extractEnvelopeShape(schema, components);
745
+ return shape !== void 0 && looksLikeEnvelope(shape);
746
+ }
747
+ function collectOperationEnvelopes(source) {
748
+ const envelopes = [];
749
+ for (const pathItem of source.paths) for (const operation of pathItem.operations) {
750
+ const success = operation.responses.find((response) => isSuccessStatusCode(response.statusCode) && response.schema !== void 0);
751
+ if (success?.schema === void 0) continue;
752
+ const shape = extractEnvelopeShape(success.schema, source.components.schemas);
753
+ if (shape === void 0) continue;
754
+ envelopes.push({
755
+ method: operation.method.toUpperCase(),
756
+ path: pathItem.path,
757
+ shape
758
+ });
759
+ }
760
+ return envelopes;
761
+ }
762
+ function analyzeEnvelope(source) {
763
+ const operations = collectOperationEnvelopes(source);
764
+ const groups = /* @__PURE__ */ new Map();
765
+ for (const operation of operations) {
766
+ const key = fingerprint(operation.shape);
767
+ const existing = groups.get(key) ?? [];
768
+ existing.push(operation);
769
+ groups.set(key, existing);
770
+ }
771
+ if (operations.length === 0) return {
772
+ groups,
773
+ mode: "raw",
774
+ operations
775
+ };
776
+ if (groups.size === 1) {
777
+ const shared = operations[0]?.shape;
778
+ if (shared !== void 0 && looksLikeEnvelope(shared)) return {
779
+ groups,
780
+ mode: "shared",
781
+ operations,
782
+ shared
783
+ };
784
+ return {
785
+ groups,
786
+ mode: "raw",
787
+ operations
788
+ };
789
+ }
790
+ const envelopeGroups = [...groups.entries()].filter(([, items]) => {
791
+ const first = items[0];
792
+ return first !== void 0 && looksLikeEnvelope(first.shape);
793
+ });
794
+ if (envelopeGroups.length === 0) return {
795
+ groups,
796
+ mode: "raw",
797
+ operations
798
+ };
799
+ if (envelopeGroups.length === 1) {
800
+ const group = envelopeGroups[0];
801
+ const firstOperation = group?.[1][0];
802
+ if (group !== void 0 && group[1].length === operations.length && firstOperation !== void 0) return {
803
+ groups,
804
+ mode: "shared",
805
+ operations,
806
+ shared: firstOperation.shape
807
+ };
808
+ }
809
+ return {
810
+ groups,
811
+ mode: "mixed",
812
+ operations
813
+ };
814
+ }
815
+ /** Largest envelope group that includes a `data` field; used for mixed-mode base.ts. */
816
+ function getPrimaryEnvelopeShape(analysis) {
817
+ if (analysis.shared !== void 0) return analysis.shared;
818
+ if (analysis.mode !== "mixed") return;
819
+ let best;
820
+ let bestCount = 0;
821
+ for (const items of analysis.groups.values()) {
822
+ const first = items[0];
823
+ if (first === void 0 || !looksLikeEnvelope(first.shape)) continue;
824
+ if (!first.shape.fields.some((field) => field.name === "data")) continue;
825
+ if (items.length > bestCount) {
826
+ bestCount = items.length;
827
+ best = first.shape;
828
+ }
829
+ }
830
+ return best;
831
+ }
832
+ function parseUserBaseResponse(content) {
833
+ const match = content.match(/export\s+interface\s+BaseResponse\s*<[^>]*>\s*\{([\s\S]*?)\}/);
834
+ if (match === null) {
835
+ const plain = content.match(/export\s+interface\s+BaseResponse\s*\{([\s\S]*?)\}/);
836
+ if (plain === null) return;
837
+ return parseBaseResponseBody(plain[1]);
838
+ }
839
+ return parseBaseResponseBody(match[1]);
840
+ }
841
+ function parseBaseResponseBody(body) {
842
+ const fields = [];
843
+ const lineRegex = /^\s*(\w+)(\?)?:\s*([^;]+);/gm;
844
+ let match;
845
+ while ((match = lineRegex.exec(body)) !== null) {
846
+ const [, name, optional, rawType] = match;
847
+ if (name === void 0 || rawType === void 0) continue;
848
+ fields.push({
849
+ kind: rawType.trim().replace(/\s+/g, " "),
850
+ name,
851
+ required: optional === void 0
852
+ });
853
+ }
854
+ fields.sort((a, b) => a.name.localeCompare(b.name));
855
+ return {
856
+ fields,
857
+ sourcePath: "models.ts"
858
+ };
859
+ }
860
+ function diffEnvelopeFields(spec, user) {
861
+ const diffs = [];
862
+ const specMap = new Map(spec.fields.filter((field) => field.name !== "data").map((field) => [field.name, field]));
863
+ const userMap = new Map(user.fields.filter((field) => field.name !== "data" && field.name !== "T").map((field) => [field.name, field]));
864
+ for (const [name, specField] of specMap.entries()) {
865
+ const userField = userMap.get(name);
866
+ if (userField === void 0) {
867
+ diffs.push({
868
+ field: name,
869
+ issue: "missing",
870
+ spec: specField
871
+ });
872
+ continue;
873
+ }
874
+ if (specField.required !== userField.required) diffs.push({
875
+ field: name,
876
+ issue: "required-changed",
877
+ spec: specField,
878
+ user: userField
879
+ });
880
+ if (specField.kind !== userField.kind && name !== "data") diffs.push({
881
+ field: name,
882
+ issue: "type-changed",
883
+ spec: specField,
884
+ user: userField
885
+ });
886
+ }
887
+ for (const [name, userField] of userMap.entries()) if (!specMap.has(name)) diffs.push({
888
+ field: name,
889
+ issue: "extra",
890
+ user: userField
891
+ });
892
+ return diffs;
893
+ }
894
+ function formatEnvelopeFieldType(field, property, genericName) {
895
+ if (property === void 0) return "unknown";
896
+ if (field.kind === "generic") return genericName;
897
+ return field.kind;
898
+ }
899
+ function buildBaseResponseInterface(shape, genericName = "T") {
900
+ const lines = [`export interface BaseResponse<${genericName}> {`];
901
+ for (const field of shape.fields) {
902
+ if (field.name === "data") {
903
+ lines.push(` data?: ${genericName};`);
904
+ continue;
905
+ }
906
+ const optional = field.required ? "" : "?";
907
+ const property = shape.schema.properties?.[field.name];
908
+ const type = formatEnvelopeFieldType(field, property, genericName);
909
+ lines.push(` ${field.name}${optional}: ${type};`);
910
+ }
911
+ lines.push("}");
912
+ return lines.join("\n");
913
+ }
914
+ //#endregion
915
+ //#region src/parser/index.ts
916
+ const HTTP_METHODS = [
917
+ "get",
918
+ "post",
919
+ "put",
920
+ "patch",
921
+ "delete"
922
+ ];
923
+ function readObjectArray(value) {
924
+ if (!isJsonArray(value)) return [];
925
+ return value.filter(isJsonObject);
926
+ }
927
+ function extractRefName(ref) {
928
+ return ref.split("/").at(-1) ?? ref;
929
+ }
930
+ function toCleanPath(pathStr) {
931
+ return pathStr.replace(/^\//, "").replace(/\{([^}]+)\}/g, "[$1]");
932
+ }
933
+ function parseSchema(raw) {
934
+ if (!isJsonObject(raw)) return { kind: "unknown" };
935
+ if (typeof raw["$ref"] === "string") return {
936
+ kind: "ref",
937
+ ref: extractRefName(raw["$ref"])
938
+ };
939
+ if (isJsonArray(raw["oneOf"]) && raw["oneOf"].length > 0) return {
940
+ kind: "oneOf",
941
+ oneOf: raw["oneOf"].map(parseSchema)
942
+ };
943
+ if (isJsonArray(raw["anyOf"]) && raw["anyOf"].length > 0) return {
944
+ anyOf: raw["anyOf"].map(parseSchema),
945
+ kind: "anyOf"
946
+ };
947
+ if (isJsonArray(raw["allOf"]) && raw["allOf"].length > 0) return {
948
+ allOf: raw["allOf"].map(parseSchema),
949
+ kind: "allOf"
950
+ };
951
+ const rawType = typeof raw["type"] === "string" ? raw["type"] : void 0, nullable = raw["nullable"] === true;
952
+ if (rawType === "array") {
953
+ const schema = { kind: "array" };
954
+ if (raw["items"] !== void 0) schema.items = parseSchema(raw["items"]);
955
+ if (nullable) schema.nullable = true;
956
+ return schema;
957
+ }
958
+ if (rawType === "object" || rawType === void 0 && (isJsonObject(raw["properties"]) || raw["additionalProperties"] !== void 0)) return buildObjectSchema(raw, nullable);
959
+ if (rawType === "integer" || rawType === "number") {
960
+ const schema = { kind: "number" };
961
+ if (typeof raw["format"] === "string") schema.format = raw["format"];
962
+ if (nullable) schema.nullable = true;
963
+ if (Array.isArray(raw["enum"])) schema.enum = readJsonPrimitives(raw["enum"]);
964
+ if (typeof raw["x-map-key-ref"] === "string") schema["x-map-key-ref"] = raw["x-map-key-ref"];
965
+ return schema;
966
+ }
967
+ if (rawType === "string") {
968
+ const schema = { kind: "string" };
969
+ if (typeof raw["format"] === "string") schema.format = raw["format"];
970
+ if (nullable) schema.nullable = true;
971
+ if (Array.isArray(raw["enum"])) schema.enum = readJsonPrimitives(raw["enum"]);
972
+ if (typeof raw["x-map-key-ref"] === "string") schema["x-map-key-ref"] = raw["x-map-key-ref"];
973
+ return schema;
974
+ }
975
+ if (rawType === "boolean") {
976
+ const schema = { kind: "boolean" };
977
+ if (nullable) schema.nullable = true;
978
+ return schema;
979
+ }
980
+ if (rawType === "null") return { kind: "null" };
981
+ return { kind: "unknown" };
982
+ }
983
+ function buildObjectSchema(raw, nullable) {
984
+ const schema = { kind: "object" };
985
+ if (nullable) schema.nullable = true;
986
+ if (isJsonObject(raw["properties"])) {
987
+ const requiredList = isJsonArray(raw["required"]) ? raw["required"].filter((entry) => typeof entry === "string") : [], properties = {};
988
+ for (const [key, propRaw] of Object.entries(raw["properties"])) properties[key] = {
989
+ required: requiredList.includes(key),
990
+ schema: parseSchema(propRaw)
991
+ };
992
+ schema.properties = properties;
993
+ if (requiredList.length > 0) schema.required = requiredList;
994
+ }
995
+ if (raw["additionalProperties"] !== void 0) {
996
+ if (typeof raw["additionalProperties"] === "boolean") schema.additionalProperties = raw["additionalProperties"];
997
+ else schema.additionalProperties = parseSchema(raw["additionalProperties"]);
998
+ }
999
+ if (typeof raw["x-map-key-ref"] === "string") schema["x-map-key-ref"] = raw["x-map-key-ref"];
1000
+ return schema;
1001
+ }
1002
+ /**
1003
+ * Convert a Swagger 2 parameter's inline type attributes to an IRSchema.
1004
+ * Swagger 2 parameters carry `type`, `format`, `enum` directly (no `schema` sub-object
1005
+ * unless `in: body`).
1006
+ */
1007
+ function swaggerParamToSchema(param) {
1008
+ if (isJsonObject(param["schema"])) return parseSchema(param["schema"]);
1009
+ const paramSchema = {};
1010
+ if (param["enum"] !== void 0) paramSchema.enum = param["enum"];
1011
+ if (typeof param["format"] === "string") paramSchema.format = param["format"];
1012
+ if (typeof param["type"] === "string") paramSchema.type = param["type"];
1013
+ return parseSchema(paramSchema);
1014
+ }
1015
+ function parseSwagger2(raw, opts) {
1016
+ const schemas = {}, definitions = isJsonObject(raw["definitions"]) ? raw["definitions"] : {};
1017
+ for (const [name, def] of Object.entries(definitions)) schemas[name] = parseSchema(def);
1018
+ const paths = parsePaths(raw, "swagger2", opts);
1019
+ return {
1020
+ components: { schemas },
1021
+ key: "",
1022
+ paths
1023
+ };
1024
+ }
1025
+ function parseOpenAPI3(raw, opts) {
1026
+ const schemas = {}, components = isJsonObject(raw["components"]) ? raw["components"] : {}, compSchemas = isJsonObject(components["schemas"]) ? components["schemas"] : {};
1027
+ for (const [name, def] of Object.entries(compSchemas)) schemas[name] = parseSchema(def);
1028
+ const paths = parsePaths(raw, "openapi3", opts);
1029
+ return {
1030
+ components: { schemas },
1031
+ key: "",
1032
+ paths
1033
+ };
1034
+ }
1035
+ function parsePaths(raw, version, opts) {
1036
+ const result = [], rawPaths = raw["paths"];
1037
+ if (!isJsonObject(rawPaths)) return result;
1038
+ for (const [pathStr, pathItemRaw] of Object.entries(rawPaths)) {
1039
+ if (opts.pathPrefix !== void 0 && !pathStr.startsWith(opts.pathPrefix)) continue;
1040
+ if (opts.ignorePaths?.includes(pathStr)) continue;
1041
+ if (!isJsonObject(pathItemRaw)) continue;
1042
+ const pathLevelParams = readObjectArray(pathItemRaw["parameters"]), operations = [];
1043
+ for (const method of HTTP_METHODS) {
1044
+ const opRaw = pathItemRaw[method];
1045
+ if (!isJsonObject(opRaw)) continue;
1046
+ const op = parseOperation(method, opRaw, pathLevelParams, version);
1047
+ operations.push(op);
1048
+ }
1049
+ if (operations.length > 0) result.push({
1050
+ cleanPath: toCleanPath(pathStr),
1051
+ operations,
1052
+ path: pathStr
1053
+ });
1054
+ }
1055
+ return result;
1056
+ }
1057
+ function mergeParams(pathLevel, opLevel) {
1058
+ const map = /* @__PURE__ */ new Map();
1059
+ for (const p of pathLevel) if (typeof p["name"] === "string") map.set(p["name"], p);
1060
+ for (const p of opLevel) if (typeof p["name"] === "string") map.set(p["name"], p);
1061
+ return [...map.values()];
1062
+ }
1063
+ function resolveOpenAPI3ParamSchema(param) {
1064
+ if (isJsonObject(param["schema"])) return parseSchema(param["schema"]);
1065
+ return { kind: "unknown" };
1066
+ }
1067
+ function resolveParamSchema(param, version) {
1068
+ if (version === "openapi3") return resolveOpenAPI3ParamSchema(param);
1069
+ return swaggerParamToSchema(param);
1070
+ }
1071
+ function parseOperation(method, opRaw, pathLevelParams, version) {
1072
+ const params = mergeParams(pathLevelParams, readObjectArray(opRaw["parameters"])), pathParams = [], queryParams = [];
1073
+ for (const param of params) {
1074
+ if (typeof param["name"] !== "string") continue;
1075
+ if (param["in"] === "path") pathParams.push({
1076
+ name: param["name"],
1077
+ schema: resolveParamSchema(param, version)
1078
+ });
1079
+ else if (param["in"] === "query") queryParams.push({
1080
+ name: param["name"],
1081
+ required: param["required"] === true,
1082
+ schema: resolveParamSchema(param, version)
1083
+ });
1084
+ }
1085
+ const requestBody = version === "swagger2" ? parseSwagger2Body(params) : parseOpenAPI3Body(opRaw), operation = {
1086
+ method,
1087
+ pathParams,
1088
+ queryParams,
1089
+ responses: parseResponses(opRaw, version)
1090
+ };
1091
+ if (typeof opRaw["operationId"] === "string") operation.operationId = opRaw["operationId"];
1092
+ if (requestBody !== void 0) operation.requestBody = requestBody;
1093
+ return operation;
1094
+ }
1095
+ function parseSwagger2Body(params) {
1096
+ const bodyParam = params.find((p) => p["in"] === "body");
1097
+ if (bodyParam === void 0) return;
1098
+ const schema = isJsonObject(bodyParam["schema"]) ? parseSchema(bodyParam["schema"]) : { kind: "unknown" };
1099
+ return {
1100
+ required: bodyParam["required"] === true,
1101
+ schema
1102
+ };
1103
+ }
1104
+ function parseOpenAPI3Body(opRaw) {
1105
+ if (!isJsonObject(opRaw["requestBody"])) return;
1106
+ const reqBodyRaw = opRaw["requestBody"], content = isJsonObject(reqBodyRaw["content"]) ? reqBodyRaw["content"] : {}, jsonContent = isJsonObject(content["application/json"]) ? content["application/json"] : {}, schema = isJsonObject(jsonContent["schema"]) ? parseSchema(jsonContent["schema"]) : { kind: "unknown" };
1107
+ return {
1108
+ required: reqBodyRaw["required"] === true,
1109
+ schema
1110
+ };
1111
+ }
1112
+ function parseResponses(opRaw, version) {
1113
+ const result = [];
1114
+ if (!isJsonObject(opRaw["responses"])) return result;
1115
+ for (const [statusCode, respRaw] of Object.entries(opRaw["responses"])) {
1116
+ if (!isJsonObject(respRaw)) {
1117
+ result.push({ statusCode });
1118
+ continue;
1119
+ }
1120
+ const irResp = { statusCode };
1121
+ if (version === "swagger2") {
1122
+ if (isJsonObject(respRaw["schema"])) irResp.schema = parseSchema(respRaw["schema"]);
1123
+ } else {
1124
+ const content = isJsonObject(respRaw["content"]) ? respRaw["content"] : {}, jsonContent = isJsonObject(content["application/json"]) ? content["application/json"] : {};
1125
+ if (isJsonObject(jsonContent["schema"])) irResp.schema = parseSchema(jsonContent["schema"]);
1126
+ }
1127
+ result.push(irResp);
1128
+ }
1129
+ return result;
1130
+ }
1131
+ function parseSpec(raw, opts) {
1132
+ if (!isJsonObject(raw)) return {
1133
+ components: { schemas: {} },
1134
+ key: "",
1135
+ paths: []
1136
+ };
1137
+ const parseOpts = {};
1138
+ if (opts.pathPrefix !== void 0) parseOpts.pathPrefix = opts.pathPrefix;
1139
+ if (opts.ignorePaths !== void 0) parseOpts.ignorePaths = opts.ignorePaths;
1140
+ if (raw["swagger"] === "2.0") return parseSwagger2(raw, parseOpts);
1141
+ if (typeof raw["openapi"] === "string" && raw["openapi"].startsWith("3.")) return parseOpenAPI3(raw, parseOpts);
1142
+ return {
1143
+ components: { schemas: {} },
1144
+ key: "",
1145
+ paths: []
1146
+ };
1147
+ }
1148
+ //#endregion
1149
+ //#region src/color/index.ts
1150
+ function isColorEnabled() {
1151
+ if (process.env["NO_COLOR"] !== void 0) return false;
1152
+ return process.stdout.isTTY === true;
1153
+ }
1154
+ function paint(formatter, text) {
1155
+ return isColorEnabled() ? formatter(text) : text;
1156
+ }
1157
+ function bold(text) {
1158
+ return paint(chalk.bold, text);
1159
+ }
1160
+ function red(text) {
1161
+ return paint(chalk.red, text);
1162
+ }
1163
+ function dim(text) {
1164
+ return paint(chalk.dim, text);
1165
+ }
1166
+ function cyan(text) {
1167
+ return paint(chalk.cyan, text);
1168
+ }
1169
+ function yellow(text) {
1170
+ return paint(chalk.yellow, text);
1171
+ }
1172
+ function white(text) {
1173
+ return paint(chalk.white, text);
1174
+ }
1175
+ //#endregion
1176
+ //#region src/emitters/recursive-ref-error.ts
1177
+ var RecursiveRefError = class extends Error {
1178
+ cycle;
1179
+ schemaPath;
1180
+ sourceKey;
1181
+ constructor(sourceKey, cycle, schemaPath) {
1182
+ super(formatRecursiveRefError(sourceKey, cycle, schemaPath));
1183
+ this.name = "RecursiveRefError";
1184
+ this.cycle = cycle;
1185
+ this.schemaPath = schemaPath;
1186
+ this.sourceKey = sourceKey;
1187
+ }
1188
+ };
1189
+ function formatRecursiveRefError(sourceKey, cycle, schemaPath) {
1190
+ return [
1191
+ bold(red(`typeforge: recursive schema reference in source "${sourceKey}"`)),
1192
+ "",
1193
+ bold("Cycle:"),
1194
+ ` ${cycle.join(" → ")}`,
1195
+ "",
1196
+ bold("At:"),
1197
+ dim(` ${schemaPath}`),
1198
+ "",
1199
+ bold("Fix:"),
1200
+ " • Add a known-type override in known-types.ts for this shape, or",
1201
+ " • Simplify the OpenAPI schema to remove the circular reference",
1202
+ cyan(` • typeforge generate --source ${sourceKey} --spec <path>`)
1203
+ ].join("\n");
1204
+ }
1205
+ //#endregion
1206
+ //#region src/plugins/known-types/matchers.ts
1207
+ function resolveObjectSchema(schema, components) {
1208
+ const resolved = schema.kind === "ref" ? resolveRef(schema, components) : schema;
1209
+ return resolved.kind === "object" ? resolved : void 0;
1210
+ }
1211
+ function sortedStrings(values) {
1212
+ return values.slice().toSorted((left, right) => left.localeCompare(right));
1213
+ }
1214
+ function matchesExactProperties(schema, components, exactProperties) {
1215
+ const objectSchema = resolveObjectSchema(schema, components);
1216
+ if (objectSchema?.properties === void 0) return false;
1217
+ const keys = sortedStrings(Object.keys(objectSchema.properties));
1218
+ const expected = sortedStrings(exactProperties);
1219
+ return keys.length === expected.length && keys.every((key, index) => key === expected[index]);
1220
+ }
1221
+ function matchesDeclarativeRule(schema, components, rule) {
1222
+ const objectSchema = resolveObjectSchema(schema, components);
1223
+ if (objectSchema?.properties === void 0) return false;
1224
+ const keys = Object.keys(objectSchema.properties);
1225
+ if (rule.maxPropertyCount !== void 0 && keys.length > rule.maxPropertyCount) return false;
1226
+ if (rule.exactProperties !== void 0 && !matchesExactProperties(schema, components, rule.exactProperties)) return false;
1227
+ if (rule.requireProperties !== void 0) {
1228
+ for (const required of rule.requireProperties) if (!(required in objectSchema.properties)) return false;
1229
+ }
1230
+ if (rule.excludeProperties !== void 0) {
1231
+ for (const excluded of rule.excludeProperties) if (excluded in objectSchema.properties) return false;
1232
+ }
1233
+ return rule.exactProperties !== void 0 || rule.requireProperties !== void 0;
1234
+ }
1235
+ //#endregion
1236
+ //#region src/plugins/known-types/load.ts
1237
+ function parseStringArray(block) {
1238
+ if (block === void 0) return;
1239
+ const values = [...block.matchAll(/["'`]([^"'`]+)["'`]/g)].map((match) => match[1]);
1240
+ return values.length > 0 ? values : void 0;
1241
+ }
1242
+ function parseDeclarativeRules(content) {
1243
+ const rules = [];
1244
+ const objectBlocks = content.matchAll(/\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}/g);
1245
+ for (const block of objectBlocks) {
1246
+ const body = block[1];
1247
+ if (body === void 0 || !body.includes("typeName")) continue;
1248
+ const name = body.match(/name:\s*["'`]([^"'`]+)["'`]/)?.[1];
1249
+ const typeName = body.match(/typeName:\s*["'`]([^"'`]+)["'`]/)?.[1];
1250
+ if (name === void 0 || typeName === void 0) continue;
1251
+ const rule = {
1252
+ name,
1253
+ typeName
1254
+ };
1255
+ const importPath = body.match(/importPath:\s*(null|["'`]([^"'`]*)["'`])/)?.[2];
1256
+ if (body.includes("importPath: null")) rule.importPath = null;
1257
+ else if (importPath !== void 0) rule.importPath = importPath;
1258
+ const exactProperties = parseStringArray(body.match(/exactProperties:\s*\[([\s\S]*?)\]/)?.[1]);
1259
+ if (exactProperties !== void 0) rule.exactProperties = exactProperties;
1260
+ const requireProperties = parseStringArray(body.match(/requireProperties:\s*\[([\s\S]*?)\]/)?.[1]);
1261
+ if (requireProperties !== void 0) rule.requireProperties = requireProperties;
1262
+ const excludeProperties = parseStringArray(body.match(/excludeProperties:\s*\[([\s\S]*?)\]/)?.[1]);
1263
+ if (excludeProperties !== void 0) rule.excludeProperties = excludeProperties;
1264
+ const maxPropertyCount = body.match(/maxPropertyCount:\s*(\d+)/)?.[1];
1265
+ if (maxPropertyCount !== void 0) rule.maxPropertyCount = Number.parseInt(maxPropertyCount, 10);
1266
+ rules.push(rule);
1267
+ }
1268
+ return rules;
1269
+ }
1270
+ function loadUserKnownTypes(cwd, apiRoot) {
1271
+ const knownTypesPath = resolve(cwd, apiRoot, "known-types.ts");
1272
+ if (!existsSync(knownTypesPath)) return [];
1273
+ return parseDeclarativeRules(readFileSync(knownTypesPath, "utf8")).map((rule) => ({
1274
+ importPath: rule.importPath ?? null,
1275
+ matcher: (schema, components) => matchesDeclarativeRule(schema, components, rule),
1276
+ name: rule.name,
1277
+ typeName: rule.typeName
1278
+ }));
1279
+ }
1280
+ //#endregion
1281
+ //#region src/plugins/known-types/index.ts
1282
+ function loadKnownTypeRules(cwd, apiRoot) {
1283
+ return loadUserKnownTypes(cwd, apiRoot);
1284
+ }
1285
+ function matchKnownType(schema, components, rules) {
1286
+ for (const rule of rules) if (rule.matcher(schema, components)) return {
1287
+ importPath: rule.importPath,
1288
+ rule,
1289
+ typeName: rule.typeName
1290
+ };
1291
+ }
1292
+ //#endregion
1293
+ //#region src/emitters/schema-renderer.ts
1294
+ const DEFAULT_MAX_DEPTH = 50;
1295
+ function indent(level) {
1296
+ return " ".repeat(level);
1297
+ }
1298
+ function formatEnumLiteral(value) {
1299
+ if (typeof value === "string") return JSON.stringify(value);
1300
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
1301
+ if (value === null) return "null";
1302
+ return "unknown";
1303
+ }
1304
+ function formatEnumUnion(enumValues) {
1305
+ return [...new Set(enumValues.map(formatEnumLiteral))].join(" | ");
1306
+ }
1307
+ function withNullable(type, schema) {
1308
+ if (schema.nullable === true) return `${type} | null`;
1309
+ return type;
1310
+ }
1311
+ function decodeJsonPointerSegment(segment) {
1312
+ return decodeURIComponent(segment.replace(/~1/g, "/").replace(/~0/g, "~"));
1313
+ }
1314
+ function readJsonPointerValue(value, segment) {
1315
+ if (Array.isArray(value)) {
1316
+ const index = Number(segment);
1317
+ if (!Number.isInteger(index) || index < 0 || index >= value.length) return;
1318
+ return value[index];
1319
+ }
1320
+ if (isJsonObject(value) && segment in value) return value[segment];
1321
+ }
1322
+ function resolveOpenApiPointer(spec, pointer) {
1323
+ if (!pointer.startsWith("#/")) return;
1324
+ let value = spec;
1325
+ for (const part of pointer.slice(2).split("/").map(decodeJsonPointerSegment)) {
1326
+ value = value === void 0 ? void 0 : readJsonPointerValue(value, part);
1327
+ if (value === void 0) return;
1328
+ }
1329
+ return parseSchema(value);
1330
+ }
1331
+ function getMapKeyType(schema, ctx) {
1332
+ const keyRef = schema["x-map-key-ref"];
1333
+ if (keyRef === void 0) return "string";
1334
+ if (ctx.rawSpec !== void 0) {
1335
+ const referenced = resolveOpenApiPointer(ctx.rawSpec, keyRef);
1336
+ if (referenced !== void 0) return renderSchemaType(referenced, {
1337
+ ...ctx,
1338
+ depth: (ctx.depth ?? 0) + 1
1339
+ });
1340
+ }
1341
+ const refName = keyRef.split("/").pop();
1342
+ if (refName !== void 0 && ctx.components[refName] !== void 0) return renderSchemaType(ctx.components[refName], {
1343
+ ...ctx,
1344
+ depth: (ctx.depth ?? 0) + 1
1345
+ });
1346
+ return "string";
1347
+ }
1348
+ function throwRecursiveError(ctx, refName) {
1349
+ const cycle = [...ctx.refStack ?? [], refName];
1350
+ throw new RecursiveRefError(ctx.sourceKey ?? "unknown", cycle, ctx.schemaPath ?? refName);
1351
+ }
1352
+ function childContext(ctx, segment) {
1353
+ const basePath = ctx.schemaPath ?? "schema";
1354
+ return {
1355
+ ...ctx,
1356
+ schemaPath: `${basePath}.${segment}`
1357
+ };
1358
+ }
1359
+ function renderSchemaType(schema, ctx) {
1360
+ const depth = ctx.depth ?? 0;
1361
+ const maxDepth = ctx.maxDepth ?? DEFAULT_MAX_DEPTH;
1362
+ if (depth > maxDepth) throw new RecursiveRefError(ctx.sourceKey ?? "unknown", ctx.refStack ?? [], `${ctx.schemaPath ?? "schema"} (max depth ${maxDepth} exceeded)`);
1363
+ const nextCtx = {
1364
+ ...ctx,
1365
+ depth: depth + 1
1366
+ };
1367
+ const knownRules = ctx.knownTypes ?? [];
1368
+ if (knownRules.length > 0) {
1369
+ const known = matchKnownType(schema, ctx.components, knownRules);
1370
+ if (known !== void 0) {
1371
+ if (ctx.knownTypeImports !== void 0 && !ctx.knownTypeImports.has(known.typeName)) ctx.knownTypeImports.set(known.typeName, known.importPath);
1372
+ return withNullable(known.typeName, schema);
1373
+ }
1374
+ }
1375
+ if (schema.enum !== void 0 && schema.enum.length > 0) return withNullable(formatEnumUnion(schema.enum), schema);
1376
+ if (schema.kind === "ref") {
1377
+ const refName = refNameFromSchema(schema);
1378
+ if (refName === void 0) return "unknown";
1379
+ const visited = ctx.visitedRefs ?? /* @__PURE__ */ new Set();
1380
+ const refStack = ctx.refStack ?? [];
1381
+ if (visited.has(refName)) throwRecursiveError(ctx, refName);
1382
+ return renderSchemaType(resolveRef(schema, ctx.components), {
1383
+ ...nextCtx,
1384
+ refStack: [...refStack, refName],
1385
+ visitedRefs: /* @__PURE__ */ new Set([...visited, refName])
1386
+ });
1387
+ }
1388
+ switch (schema.kind) {
1389
+ case "string": return withNullable("string", schema);
1390
+ case "number": return withNullable("number", schema);
1391
+ case "boolean": return withNullable("boolean", schema);
1392
+ case "null": return "null";
1393
+ case "unknown": return "unknown";
1394
+ case "array": {
1395
+ const itemType = schema.items === void 0 ? "unknown" : renderSchemaType(schema.items, childContext(nextCtx, "[]"));
1396
+ if (itemType === "TiptapDocument") return withNullable("TiptapDocument", schema);
1397
+ return withNullable(`${itemType.includes(" | ") ? `(${itemType})` : itemType}[]`, schema);
1398
+ }
1399
+ case "object": {
1400
+ if (schema.properties === void 0) {
1401
+ if (schema.additionalProperties === true) return withNullable("Record<string, unknown>", schema);
1402
+ if (typeof schema.additionalProperties === "object" && schema.additionalProperties !== null) return withNullable(`Record<${ctx.resolveMapKeyRefs !== false && schema["x-map-key-ref"] !== void 0 ? getMapKeyType(schema, ctx) : "string"}, ${renderSchemaType(schema.additionalProperties, childContext(nextCtx, "value"))}>`, schema);
1403
+ return withNullable("Record<string, unknown>", schema);
1404
+ }
1405
+ const lines = ["{"];
1406
+ for (const [name, property] of Object.entries(schema.properties)) {
1407
+ const optional = property.required ? "" : "?";
1408
+ const type = renderSchemaType(property.schema, childContext(nextCtx, name));
1409
+ lines.push(`${indent(depth + 1)}${name}${optional}: ${type};`);
1410
+ }
1411
+ lines.push(`${indent(depth)}}`);
1412
+ return withNullable(lines.join("\n"), schema);
1413
+ }
1414
+ case "oneOf":
1415
+ case "anyOf": {
1416
+ const variants = schema[schema.kind];
1417
+ if (variants === void 0 || variants.length === 0) return "unknown";
1418
+ const renderedVariants = variants.map((variant) => renderSchemaType(variant, nextCtx));
1419
+ const specificVariants = renderedVariants.filter((variant) => variant !== "Record<string, unknown>");
1420
+ return withNullable((specificVariants.length > 0 ? specificVariants : renderedVariants).join(" | "), schema);
1421
+ }
1422
+ case "allOf": {
1423
+ const parts = schema.allOf;
1424
+ if (parts === void 0 || parts.length === 0) return "unknown";
1425
+ return withNullable(parts.map((part) => renderSchemaType(part, nextCtx)).join(" & "), schema);
1426
+ }
1427
+ default: return "unknown";
1428
+ }
1429
+ }
1430
+ //#endregion
1431
+ //#region src/emitters/types/index.ts
1432
+ function createRenderContext(options, schemaPath, knownTypeImports) {
1433
+ const ctx = {
1434
+ components: options.source.components.schemas,
1435
+ knownTypeImports,
1436
+ knownTypes: options.knownTypes ?? [],
1437
+ maxDepth: options.maxRenderDepth ?? 50,
1438
+ resolveMapKeyRefs: options.resolveMapKeyRefs !== false,
1439
+ schemaPath
1440
+ };
1441
+ if (options.sourceKey !== void 0) ctx.sourceKey = options.sourceKey;
1442
+ if (options.rawSpec !== void 0) ctx.rawSpec = options.rawSpec;
1443
+ return ctx;
1444
+ }
1445
+ function formatImportLines(knownTypeImports) {
1446
+ const lines = [];
1447
+ const byPath = /* @__PURE__ */ new Map();
1448
+ for (const [typeName, importPath] of knownTypeImports.entries()) {
1449
+ const existing = byPath.get(importPath) ?? [];
1450
+ existing.push(typeName);
1451
+ byPath.set(importPath, existing);
1452
+ }
1453
+ for (const [importPath, typeNames] of byPath.entries()) {
1454
+ if (importPath === null) continue;
1455
+ lines.push(`import type { ${typeNames.join(", ")} } from "${importPath}";`);
1456
+ }
1457
+ return lines;
1458
+ }
1459
+ function renderParamsInterface(typeName, operation, options, knownTypeImports) {
1460
+ if (operation.queryParams.length === 0) return;
1461
+ const queryExtends = options.queryExtends;
1462
+ const pageName = queryExtends?.page ?? "page";
1463
+ const limitName = queryExtends?.limit ?? "limit";
1464
+ const sortByName = queryExtends?.sortBy ?? "sortBy";
1465
+ const sortOrderName = queryExtends?.sortOrder ?? "sortOrder";
1466
+ const hasPage = operation.queryParams.some((param) => param.name === pageName);
1467
+ const hasLimit = operation.queryParams.some((param) => param.name === limitName);
1468
+ const sortByParam = operation.queryParams.find((param) => param.name === sortByName);
1469
+ const hasSortOrder = operation.queryParams.some((param) => param.name === sortOrderName);
1470
+ const hasOffsetLimitQuery = hasPage && hasLimit;
1471
+ const hasSortParams = sortByParam !== void 0 && hasSortOrder;
1472
+ const commonParamNames = new Set([
1473
+ hasOffsetLimitQuery ? pageName : void 0,
1474
+ hasOffsetLimitQuery ? limitName : void 0,
1475
+ hasSortParams ? sortByName : void 0,
1476
+ hasSortParams ? sortOrderName : void 0
1477
+ ].filter((name) => name !== void 0));
1478
+ const nonCommonParams = operation.queryParams.filter((param) => !commonParamNames.has(param.name));
1479
+ const extendsParts = [];
1480
+ if (hasSortParams && sortByParam !== void 0 && sortByParam.schema.enum !== void 0 && sortByParam.schema.enum.length > 0 && queryExtends?.sortTypeName !== void 0) {
1481
+ const sortOptions = [...new Set(sortByParam.schema.enum.map((value) => JSON.stringify(value)))].join(" | ");
1482
+ extendsParts.push(`${queryExtends.sortTypeName}<${sortOptions}>`);
1483
+ if (queryExtends.sortImportPath !== void 0) knownTypeImports.set(queryExtends.sortTypeName, queryExtends.sortImportPath);
1484
+ }
1485
+ if (hasOffsetLimitQuery && queryExtends?.paginationTypeName !== void 0) {
1486
+ extendsParts.push(queryExtends.paginationTypeName);
1487
+ if (queryExtends.paginationImportPath !== void 0) knownTypeImports.set(queryExtends.paginationTypeName, queryExtends.paginationImportPath);
1488
+ }
1489
+ if (extendsParts.length > 0 && nonCommonParams.length === 0) return `export type ${typeName}Params = ${extendsParts.join(" & ")};`;
1490
+ const lines = [];
1491
+ if (extendsParts.length > 0) lines.push(`export interface ${typeName}Params extends ${extendsParts.join(", ")} {`);
1492
+ else lines.push(`export interface ${typeName}Params {`);
1493
+ for (const param of nonCommonParams) appendQueryParam(param, lines, options, knownTypeImports, typeName);
1494
+ lines.push("}");
1495
+ return lines.join("\n");
1496
+ }
1497
+ function appendQueryParam(param, lines, options, knownTypeImports, typeName) {
1498
+ const optional = param.required ? "" : "?";
1499
+ const type = renderSchemaType(param.schema, createRenderContext(options, `${typeName}Params.${param.name}`, knownTypeImports));
1500
+ lines.push(` ${param.name}${optional}: ${type};`);
1501
+ }
1502
+ function renderBodyInterface(typeName, operation, options, knownTypeImports) {
1503
+ if (!hasMeaningfulRequestBody(operation) || operation.requestBody === void 0) return;
1504
+ const bodyType = renderSchemaType(operation.requestBody.schema, createRenderContext(options, `${typeName}Body`, knownTypeImports));
1505
+ if (bodyType.startsWith("{")) return `export interface ${typeName}Body ${bodyType}`;
1506
+ return `export type ${typeName}Body = ${bodyType};`;
1507
+ }
1508
+ function resolveSuccessResponseSchema(schema, components) {
1509
+ return resolveObjectSchema$1(schema, components) ?? schema;
1510
+ }
1511
+ function renderResponseType(typeName, operation, options, _envelopeMode, knownTypeImports, baseImportPath) {
1512
+ const schema = getSuccessResponseSchema(operation);
1513
+ if (schema === void 0) return `export type ${typeName}Response = unknown;`;
1514
+ const resolved = resolveSuccessResponseSchema(schema, options.source.components.schemas);
1515
+ const dataSchema = resolved.kind === "object" && resolved.properties?.data !== void 0 ? resolved.properties.data.schema : void 0;
1516
+ const isSuccessEnvelope = resolved.kind === "object" && resolved.properties?.success !== void 0 && isEnvelopeSchema(schema, options.source.components.schemas);
1517
+ if (options.unwrapResponseData === true && isSuccessEnvelope) {
1518
+ if (dataSchema === void 0) return `export type ${typeName}Response = null;`;
1519
+ return `export type ${typeName}Response = ${renderSchemaType(dataSchema, createRenderContext(options, `${typeName}Response.data`, knownTypeImports))};`;
1520
+ }
1521
+ if (dataSchema !== void 0) {
1522
+ if (!(_envelopeMode === "shared" || _envelopeMode === "mixed" && options.sharedEnvelope !== void 0 && matchesEnvelopeShape(schema, options.source.components.schemas, options.sharedEnvelope))) return `export type ${typeName}Response = ${renderSchemaType(schema, createRenderContext(options, `${typeName}Response`, knownTypeImports))};`;
1523
+ return `export type ${typeName}Response = import("${baseImportPath}").BaseResponse<${renderSchemaType(dataSchema, createRenderContext(options, `${typeName}Response.data`, knownTypeImports))}> & Omit<${renderSchemaType(schema, createRenderContext(options, `${typeName}Response`, knownTypeImports))}, "data">;`;
1524
+ }
1525
+ return `export type ${typeName}Response = ${renderSchemaType(schema, createRenderContext(options, `${typeName}Response`, knownTypeImports))};`;
1526
+ }
1527
+ function emitTypeFiles(options) {
1528
+ const files = [];
1529
+ for (const pathItem of options.source.paths) for (const operation of pathItem.operations) {
1530
+ const typeName = getFunctionTypeName(pathItem.cleanPath, operation.method);
1531
+ const knownTypeImports = /* @__PURE__ */ new Map();
1532
+ const blocks = [
1533
+ "// Auto-generated from OpenAPI spec",
1534
+ `// Path: ${operation.method.toUpperCase()} ${pathItem.path}`,
1535
+ "// DO NOT EDIT - This file is automatically generated",
1536
+ ""
1537
+ ];
1538
+ const paramsBlock = renderParamsInterface(typeName, operation, options, knownTypeImports);
1539
+ if (paramsBlock !== void 0) blocks.push(paramsBlock, "");
1540
+ const bodyBlock = renderBodyInterface(typeName, operation, options, knownTypeImports);
1541
+ if (bodyBlock !== void 0) blocks.push(bodyBlock, "");
1542
+ const baseImportPath = resolveAliasAwareImport({
1543
+ fromAbsolutePath: `${options.typesDir}/${pathItem.cleanPath}/${operation.method.toUpperCase()}.d.ts`,
1544
+ toAbsolutePath: options.baseFile.replace(/\.d\.ts$/, "").replace(/\.ts$/, ""),
1545
+ ...options.tsconfigPaths === void 0 ? {} : { tsconfigPaths: options.tsconfigPaths }
1546
+ });
1547
+ blocks.push(renderResponseType(typeName, operation, options, options.envelopeMode, knownTypeImports, baseImportPath));
1548
+ const importLines = formatImportLines(knownTypeImports);
1549
+ const content = [
1550
+ ...importLines,
1551
+ ...importLines.length > 0 ? [""] : [],
1552
+ ...blocks
1553
+ ].join("\n").trimEnd();
1554
+ files.push({
1555
+ content: `${content}\n`,
1556
+ relativePath: `${pathItem.cleanPath}/${operation.method.toUpperCase()}.d.ts`
1557
+ });
1558
+ }
1559
+ return files;
1560
+ }
1561
+ function emitBaseFile(sharedEnvelope) {
1562
+ return [
1563
+ "// Auto-generated from OpenAPI spec",
1564
+ "// DO NOT EDIT - This file is automatically generated",
1565
+ "",
1566
+ buildBaseResponseInterface(sharedEnvelope),
1567
+ ""
1568
+ ].join("\n");
1569
+ }
1570
+ //#endregion
1571
+ //#region src/envelope-guard/diagnostic.ts
1572
+ function formatDriftError(sourceKey, spec, userSource, userBlock, diffs, outliers = []) {
1573
+ const lines = [bold(red(`typeforge: base response mismatch in source "${sourceKey}"`)), ""];
1574
+ lines.push(bold("Spec envelope:"));
1575
+ for (const field of spec.fields) lines.push(` ${field.name}${field.required ? "" : "?"}: ${field.kind}`);
1576
+ lines.push("");
1577
+ lines.push(bold(`Your ${userSource} BaseResponse:`));
1578
+ lines.push(userBlock);
1579
+ lines.push("");
1580
+ lines.push(bold("Conflicts:"));
1581
+ for (const diff of diffs) if (diff.issue === "missing") lines.push(yellow(` • ${diff.field}: present in spec, missing in your type`));
1582
+ else if (diff.issue === "extra") lines.push(yellow(` • ${diff.field}: extra field in your type`));
1583
+ else if (diff.issue === "type-changed") lines.push(yellow(` • ${diff.field}: spec is ${diff.spec?.kind}, your type is ${diff.user?.kind}`));
1584
+ else lines.push(yellow(` • ${diff.field}: required/optional mismatch`));
1585
+ if (outliers.length > 0) {
1586
+ lines.push("");
1587
+ lines.push(dim("Also not matching the spec envelope:"));
1588
+ for (const outlier of outliers.slice(0, 3)) lines.push(dim(` ${outlier.method} ${outlier.path}`));
1589
+ }
1590
+ lines.push("");
1591
+ lines.push(bold("Fix:"));
1592
+ lines.push(" • Update models.ts to match the spec, or");
1593
+ lines.push(cyan(` • typeforge generate --source ${sourceKey} --spec <path> --accept-base`));
1594
+ return lines.join("\n");
1595
+ }
1596
+ //#endregion
1597
+ //#region src/color/diagnostic.ts
1598
+ function formatCaretLine(column, highlightStart, highlightEnd, label) {
1599
+ const caret = `${" ".repeat(column + 1)}:${`${" ".repeat(Math.max(highlightEnd - highlightStart, 1))}|`}`;
1600
+ if (label === void 0) return caret;
1601
+ return `${caret}\n${" ".repeat(column + highlightEnd + 3)}\`${dim(`-- ${label}`)}`;
1602
+ }
1603
+ function formatSnippet(snippet) {
1604
+ return [
1605
+ dim(` ,-[${snippet.file}:${snippet.line}:${snippet.column}]`),
1606
+ white(`${String(snippet.line).padStart(4, " ")} | ${snippet.source}`),
1607
+ formatCaretLine(7 + snippet.highlightStart, snippet.highlightStart, snippet.highlightEnd, snippet.label),
1608
+ dim(" `----")
1609
+ ].join("\n");
1610
+ }
1611
+ function formatDiagnostic(options) {
1612
+ const lines = [` ${(options.severity ?? "error") === "error" ? red("×") : yellow("!")} ${bold(`${options.code}`)}: ${options.message}`];
1613
+ if (options.snippet !== void 0) lines.push(formatSnippet(options.snippet));
1614
+ if (options.help !== void 0) lines.push(` ${dim("help:")} ${options.help}`);
1615
+ return lines.join("\n");
1616
+ }
1617
+ function formatHelpList(title, items) {
1618
+ return [bold(title), ...items.map((item) => ` ${cyan(item)}`)].join("\n");
1619
+ }
1620
+ //#endregion
1621
+ //#region src/parser/loader.ts
1622
+ async function loadSpec(source) {
1623
+ let filePath;
1624
+ switch (source.kind) {
1625
+ case "file":
1626
+ filePath = resolve(source.path);
1627
+ break;
1628
+ case "local-override":
1629
+ filePath = resolve(source.path);
1630
+ break;
1631
+ case "env": {
1632
+ const envValue = process.env[source.varName];
1633
+ if (envValue === void 0) throw new Error(`Environment variable ${source.varName} is not set`);
1634
+ filePath = resolve(envValue);
1635
+ break;
1636
+ }
1637
+ }
1638
+ return parseJson(await readFile(filePath, "utf8"));
1639
+ }
1640
+ /**
1641
+ * Resolution priority:
1642
+ * 1. --spec CLI flag
1643
+ * 2. OPENAPI_SPEC_<UPPER_KEY> env var
1644
+ * 3. source.ts `spec` property (project-relative path from config)
1645
+ * 4. typeforge.local.json (gitignored per-machine override)
1646
+ * 5. committed snapshot at snapshotPath
1647
+ *
1648
+ * Throws a human-readable error (no stack trace as first line) when nothing is found.
1649
+ */
1650
+ function resolveSpecSource(sourceKey, opts) {
1651
+ if (opts.specFlag !== void 0) return {
1652
+ kind: "file",
1653
+ path: opts.specFlag
1654
+ };
1655
+ const envVarName = `OPENAPI_SPEC_${sourceKey.toUpperCase().replace(/-/g, "_")}`;
1656
+ if (process.env[envVarName] !== void 0) return {
1657
+ kind: "env",
1658
+ varName: envVarName
1659
+ };
1660
+ if (opts.sourceConfigSpec !== void 0) return {
1661
+ kind: "file",
1662
+ path: opts.sourceConfigSpec
1663
+ };
1664
+ const defaultLocalPath = "./typeforge.local.json";
1665
+ const legacyLocalPath = "./openapi-codegen.local.json";
1666
+ const localPath = opts.localOverridePath ?? (existsSync(defaultLocalPath) || !existsSync(legacyLocalPath) ? defaultLocalPath : legacyLocalPath);
1667
+ if (existsSync(localPath)) try {
1668
+ const specPath = readJsonObject(readFileSync(localPath, "utf8"))[sourceKey];
1669
+ if (typeof specPath === "string") return {
1670
+ kind: "local-override",
1671
+ path: specPath
1672
+ };
1673
+ } catch {}
1674
+ if (opts.snapshotPath !== void 0 && existsSync(opts.snapshotPath)) return {
1675
+ kind: "file",
1676
+ path: opts.snapshotPath
1677
+ };
1678
+ throw new Error(buildNotFoundMessage(sourceKey, envVarName, opts, localPath));
1679
+ }
1680
+ function formatSnapshotNote(snapshotPath) {
1681
+ if (snapshotPath === void 0) return dim("not configured");
1682
+ if (existsSync(snapshotPath)) return dim(`found at ${snapshotPath}`);
1683
+ return dim(`not found at ${snapshotPath}`);
1684
+ }
1685
+ function buildNotFoundMessage(sourceKey, envVarName, opts, localPath) {
1686
+ const specFlagNote = opts.specFlag !== void 0 ? opts.specFlag : dim("not provided"), envNote = dim("not set"), sourceConfigNote = opts.sourceConfigSpec !== void 0 ? opts.sourceConfigSpec : dim("not set in source.ts"), localNote = existsSync(localPath) ? dim(`found at ${localPath} (no entry for "${sourceKey}")`) : dim(`not found at ${localPath}`), { snapshotPath } = opts;
1687
+ const snapshotNote = formatSnapshotNote(snapshotPath), tried = [
1688
+ ` --spec flag: ${specFlagNote}`,
1689
+ ` ${envVarName} env: ${envNote}`,
1690
+ ` source.ts spec: ${sourceConfigNote}`,
1691
+ ` local override: ${localNote}`,
1692
+ ` committed snapshot: ${snapshotNote}`
1693
+ ].join("\n"), fixCommands = [`typeforge generate --source ${sourceKey} --spec ./path/to/swagger.json`, `export ${envVarName}=./path/to/swagger.json`];
1694
+ return [
1695
+ formatDiagnostic({
1696
+ code: "typeforge/spec-not-found",
1697
+ help: "Provide one of the resolution paths above, for example with --spec or an env var.",
1698
+ message: `No OpenAPI spec found for source "${sourceKey}"`,
1699
+ severity: "error"
1700
+ }),
1701
+ "",
1702
+ bold("Tried:"),
1703
+ tried,
1704
+ "",
1705
+ formatHelpList("Fix:", fixCommands)
1706
+ ].join("\n");
1707
+ }
1708
+ //#endregion
1709
+ //#region src/utils/output.ts
1710
+ async function writeOutputFiles(files, check = false) {
1711
+ const changed = [];
1712
+ for (const file of files) {
1713
+ await mkdir(dirname(file.path), { recursive: true });
1714
+ let existing;
1715
+ try {
1716
+ existing = await readFile(file.path, "utf8");
1717
+ } catch {
1718
+ existing = void 0;
1719
+ }
1720
+ if (existing === file.content) continue;
1721
+ changed.push(file.path);
1722
+ if (!check) await writeFile(file.path, file.content, "utf8");
1723
+ }
1724
+ return {
1725
+ changed,
1726
+ written: check ? 0 : changed.length
1727
+ };
1728
+ }
1729
+ //#endregion
1730
+ //#region src/generate/index.ts
1731
+ function buildGenerateContext(cwd, sourceKey) {
1732
+ const apiRoot = loadProjectConfig(cwd).apiRoot ?? "src/api";
1733
+ const sourceConfig = loadSourceConfig(cwd, apiRoot, sourceKey);
1734
+ const sourceDir = resolve(cwd, apiRoot, sourceKey);
1735
+ const generatedDir = join(sourceDir, "generated");
1736
+ const functionsDir = sourceConfig.functionsDir === void 0 ? join(generatedDir, "functions") : resolve(cwd, sourceConfig.functionsDir);
1737
+ const typesDir = sourceConfig.typesDir === void 0 ? join(generatedDir, "types") : resolve(cwd, sourceConfig.typesDir);
1738
+ return {
1739
+ apiRoot,
1740
+ baseFile: sourceConfig.typesDir === void 0 ? join(generatedDir, "base.ts") : join(dirname(typesDir), "base.d.ts"),
1741
+ cwd,
1742
+ functionsDir,
1743
+ generatedDir,
1744
+ hasQueryScope: sourceConfig.tanstackQuery === true && hasQueryScopeFile(cwd, apiRoot),
1745
+ httpMode: detectHttpMode(cwd, apiRoot),
1746
+ routesFile: join(generatedDir, "routes.ts"),
1747
+ snapshotPath: join(sourceDir, "spec.json"),
1748
+ sourceConfig,
1749
+ sourceDir,
1750
+ sourceKey,
1751
+ typesDir
1752
+ };
1753
+ }
1754
+ function validateEnvelope(context, source, acceptBase) {
1755
+ const analysis = analyzeEnvelope(source);
1756
+ if (analysis.mode !== "shared" || analysis.shared === void 0) return { mode: analysis.mode };
1757
+ const modelsContent = readModelsFile(context.cwd, context.apiRoot);
1758
+ if (modelsContent === void 0) return { mode: analysis.mode };
1759
+ const userBase = parseUserBaseResponse(modelsContent);
1760
+ if (userBase === void 0) return { mode: analysis.mode };
1761
+ const diffs = diffEnvelopeFields(analysis.shared, userBase);
1762
+ if (diffs.length === 0) return { mode: analysis.mode };
1763
+ if (acceptBase) return { mode: analysis.mode };
1764
+ const userBlock = userBase.fields.map((field) => ` ${field.name}${field.required ? "" : "?"}: ${field.kind};`).join("\n");
1765
+ return {
1766
+ error: formatDriftError(context.sourceKey, analysis.shared, userBase.sourcePath, userBlock, diffs),
1767
+ mode: analysis.mode
1768
+ };
1769
+ }
1770
+ function patchModelsBaseResponse(cwd, apiRoot, newInterface) {
1771
+ const modelsPath = resolve(cwd, apiRoot, "models.ts");
1772
+ if (!existsSync(modelsPath)) return;
1773
+ const patched = readFileSync(modelsPath, "utf8").replace(/export\s+interface\s+BaseResponse\s*<[^>]*>\s*\{[\s\S]*?\}/, newInterface);
1774
+ writeFileSync(modelsPath, patched, "utf8");
1775
+ }
1776
+ async function generateForSource(options) {
1777
+ const cwd = options.cwd ?? process.cwd();
1778
+ const context = buildGenerateContext(cwd, options.sourceKey);
1779
+ const specResolveOptions = { snapshotPath: context.snapshotPath };
1780
+ if (options.specFlag !== void 0) specResolveOptions.specFlag = options.specFlag;
1781
+ if (context.sourceConfig.spec !== void 0) specResolveOptions.sourceConfigSpec = context.sourceConfig.spec;
1782
+ const rawSpec = await loadSpec(resolveSpecSource(options.sourceKey, specResolveOptions));
1783
+ const parseOptions = {};
1784
+ if (context.sourceConfig.ignorePaths !== void 0) parseOptions.ignorePaths = context.sourceConfig.ignorePaths;
1785
+ if (context.sourceConfig.pathPrefix !== void 0) parseOptions.pathPrefix = context.sourceConfig.pathPrefix;
1786
+ const source = parseSpec(rawSpec, parseOptions);
1787
+ source.key = options.sourceKey;
1788
+ const envelopeCheck = validateEnvelope(context, source, options.acceptBase === true);
1789
+ if (envelopeCheck.error !== void 0) throw new Error(envelopeCheck.error);
1790
+ const analysis = analyzeEnvelope(source);
1791
+ const outputFiles = [];
1792
+ const primaryEnvelope = getPrimaryEnvelopeShape(analysis);
1793
+ if (primaryEnvelope !== void 0) {
1794
+ outputFiles.push({
1795
+ content: emitBaseFile(primaryEnvelope),
1796
+ path: context.baseFile
1797
+ });
1798
+ if (options.acceptBase === true) patchModelsBaseResponse(cwd, context.apiRoot, buildBaseResponseInterface(primaryEnvelope));
1799
+ }
1800
+ const typeEmitterOptions = {
1801
+ baseFile: context.baseFile,
1802
+ envelopeMode: analysis.mode,
1803
+ knownTypes: loadKnownTypeRules(cwd, context.apiRoot),
1804
+ source,
1805
+ sourceKey: options.sourceKey,
1806
+ typesDir: context.typesDir
1807
+ };
1808
+ if (context.sourceConfig.resolveMapKeyRefs !== void 0) typeEmitterOptions.resolveMapKeyRefs = context.sourceConfig.resolveMapKeyRefs;
1809
+ if (context.sourceConfig.unwrapResponseData !== void 0) typeEmitterOptions.unwrapResponseData = context.sourceConfig.unwrapResponseData;
1810
+ if (primaryEnvelope !== void 0) typeEmitterOptions.sharedEnvelope = primaryEnvelope;
1811
+ const typesTsconfigPaths = loadTsconfigPaths(context.typesDir);
1812
+ if (typesTsconfigPaths !== void 0) typeEmitterOptions.tsconfigPaths = typesTsconfigPaths;
1813
+ if (context.sourceConfig.maxRenderDepth !== void 0) typeEmitterOptions.maxRenderDepth = context.sourceConfig.maxRenderDepth;
1814
+ if (context.sourceConfig.queryExtends !== void 0) typeEmitterOptions.queryExtends = context.sourceConfig.queryExtends;
1815
+ if (isJsonObject(rawSpec)) typeEmitterOptions.rawSpec = rawSpec;
1816
+ const typeFiles = emitTypeFiles(typeEmitterOptions);
1817
+ for (const file of typeFiles) outputFiles.push({
1818
+ content: file.content,
1819
+ path: join(context.typesDir, file.relativePath)
1820
+ });
1821
+ const routeEnumName = context.sourceConfig.routeEnumName ?? "RouteTargets";
1822
+ const routesOptions = {
1823
+ paths: source.paths,
1824
+ routeEnumName
1825
+ };
1826
+ if (context.sourceConfig.stripApiPrefix === true) routesOptions.stripApiPrefix = true;
1827
+ const routesContent = emitRoutesFile(routesOptions);
1828
+ let finalRoutes = routesContent;
1829
+ if (context.sourceConfig.generationMode === "merge" && existsSync(context.routesFile)) finalRoutes = mergeRoutesFile(readFileSync(context.routesFile, "utf8"), routesContent, routeEnumName, context.sourceConfig.pathPrefix);
1830
+ outputFiles.push({
1831
+ content: finalRoutes,
1832
+ path: context.routesFile
1833
+ });
1834
+ outputFiles.push({
1835
+ content: emitRuntimeFile({
1836
+ hasQueryScope: context.hasQueryScope,
1837
+ httpMode: context.httpMode
1838
+ }),
1839
+ path: join(context.generatedDir, "runtime.ts")
1840
+ });
1841
+ const tsconfigPaths = loadTsconfigPaths(context.functionsDir);
1842
+ const functionEmitterOptions = {
1843
+ functionsDir: context.functionsDir,
1844
+ generatedDir: context.generatedDir,
1845
+ hasQueryScope: context.hasQueryScope,
1846
+ httpMode: context.httpMode,
1847
+ paths: source.paths,
1848
+ routeEnumName,
1849
+ typesDir: context.typesDir
1850
+ };
1851
+ if (context.sourceConfig.importBase !== void 0) functionEmitterOptions.importBase = context.sourceConfig.importBase;
1852
+ else if (tsconfigPaths !== void 0) functionEmitterOptions.tsconfigPaths = tsconfigPaths;
1853
+ const functionFiles = emitFunctionFiles(functionEmitterOptions);
1854
+ for (const file of functionFiles) outputFiles.push({
1855
+ content: file.content,
1856
+ path: join(context.functionsDir, file.relativePath)
1857
+ });
1858
+ return {
1859
+ changed: (await writeOutputFiles(outputFiles, options.check === true)).changed,
1860
+ check: options.check === true,
1861
+ files: outputFiles.length,
1862
+ sourceKey: options.sourceKey
1863
+ };
1864
+ }
1865
+ //#endregion
1866
+ //#region src/init/index.ts
1867
+ const AXIOS_HTTP_TEMPLATE = `import axiosBase from "axios";
1868
+ import { createAxiosAdapter } from "@openmirai/typeforge/adapters/axios";
1869
+
1870
+ const axios = axiosBase.create({
1871
+ baseURL: process.env.NEXT_PUBLIC_API_URL,
1872
+ });
1873
+
1874
+ axios.interceptors.request.use(
1875
+ async (config) => {
1876
+ // Add auth headers, tracing, or Content-Type defaults here.
1877
+ return config;
1878
+ },
1879
+ (error) => Promise.reject(error),
1880
+ );
1881
+
1882
+ axios.interceptors.response.use(
1883
+ (response) => response,
1884
+ (error) => Promise.reject(error),
1885
+ );
1886
+
1887
+ export const httpFetch = createAxiosAdapter(axios);
1888
+ export { axios };
1889
+ export type { HTTPFetch, HTTPFetchConfig } from "@openmirai/typeforge/adapters/axios";
1890
+ `;
1891
+ const FETCH_HTTP_TEMPLATE = `import { createFetchAdapter } from "@openmirai/typeforge/adapters/fetch";
1892
+
1893
+ export const httpFetch = createFetchAdapter({
1894
+ baseURL: process.env.NEXT_PUBLIC_API_URL,
1895
+ });
1896
+
1897
+ export type { HTTPFetch, HTTPFetchConfig } from "@openmirai/typeforge/adapters/fetch";
1898
+ `;
1899
+ const CUSTOM_HTTP_TEMPLATE = `import type { HTTPFetch, HTTPFetchConfig } from "@openmirai/typeforge/http";
1900
+
1901
+ export type { HTTPFetch, HTTPFetchConfig };
1902
+
1903
+ export const httpFetch: HTTPFetch = {
1904
+ delete: async <TResponse>(
1905
+ _route: string,
1906
+ _config?: HTTPFetchConfig
1907
+ ): Promise<{ data: TResponse }> => {
1908
+ throw new Error("Implement httpFetch.delete");
1909
+ },
1910
+ get: async <TResponse>(
1911
+ _route: string,
1912
+ _config?: HTTPFetchConfig
1913
+ ): Promise<{ data: TResponse }> => {
1914
+ throw new Error("Implement httpFetch.get");
1915
+ },
1916
+ patch: async <TResponse, TBody = unknown>(
1917
+ _route: string,
1918
+ _body: TBody,
1919
+ _config?: HTTPFetchConfig
1920
+ ): Promise<{ data: TResponse }> => {
1921
+ throw new Error("Implement httpFetch.patch");
1922
+ },
1923
+ post: async <TResponse, TBody = unknown>(
1924
+ _route: string,
1925
+ _body: TBody,
1926
+ _config?: HTTPFetchConfig
1927
+ ): Promise<{ data: TResponse }> => {
1928
+ throw new Error("Implement httpFetch.post");
1929
+ },
1930
+ put: async <TResponse, TBody = unknown>(
1931
+ _route: string,
1932
+ _body: TBody,
1933
+ _config?: HTTPFetchConfig
1934
+ ): Promise<{ data: TResponse }> => {
1935
+ throw new Error("Implement httpFetch.put");
1936
+ },
1937
+ };
1938
+ `;
1939
+ const SOURCE_TEMPLATE = `import { defineSourceConfig } from "@openmirai/typeforge";
1940
+
1941
+ export default defineSourceConfig({
1942
+ // Path to the OpenAPI spec file, relative to the project root.
1943
+ // Set this so \`typeforge generate --source <key>\` (or --all) works
1944
+ // without a per-invocation --spec flag.
1945
+ // spec: "./specs/acme.json",
1946
+ pathPrefix: "/api/acme/v3",
1947
+ stripApiPrefix: true,
1948
+ routeEnumName: "RouteTargets",
1949
+ generationMode: "authoritative",
1950
+ naming: "path",
1951
+ ignorePaths: [],
1952
+ maxRenderDepth: 50,
1953
+ resolveMapKeyRefs: true,
1954
+ queryExtends: {
1955
+ page: "page",
1956
+ limit: "limit",
1957
+ sortBy: "sortBy",
1958
+ sortOrder: "sortOrder",
1959
+ paginationTypeName: "OffsetLimitQuery",
1960
+ paginationImportPath: "./pagination",
1961
+ sortTypeName: "SortParams",
1962
+ sortImportPath: "./pagination",
1963
+ },
1964
+ });
1965
+ `;
1966
+ const KNOWN_TYPES_TEMPLATE = `/** Map OpenAPI object shapes to your own TypeScript types by property pattern. */
1967
+ export const knownTypes = [
1968
+ // {
1969
+ // name: "BlobAsset",
1970
+ // typeName: "BlobAsset",
1971
+ // importPath: "./blob/types",
1972
+ // exactProperties: ["id", "url", "file"],
1973
+ // },
1974
+ // {
1975
+ // name: "TiptapNode",
1976
+ // typeName: "TiptapNode",
1977
+ // importPath: "./tiptap/types",
1978
+ // requireProperties: ["type"],
1979
+ // excludeProperties: ["courseCount"],
1980
+ // },
1981
+ ];
1982
+ `;
1983
+ function defaultApiRoot(layout) {
1984
+ return layout === "packages" ? "packages/utils/src/api" : "src/api";
1985
+ }
1986
+ function writeIfMissing(path, content) {
1987
+ if (existsSync(path)) return "skipped";
1988
+ mkdirSync(join(path, ".."), { recursive: true });
1989
+ writeFileSync(path, content, "utf8");
1990
+ return "created";
1991
+ }
1992
+ function initProject(options) {
1993
+ const cwd = options.cwd ?? process.cwd();
1994
+ const layout = options.layout ?? "monolith";
1995
+ const configPath = resolve(cwd, "typeforge.json");
1996
+ const legacyConfigPath = resolve(cwd, "openapi-codegen.json");
1997
+ const projectConfig = loadProjectConfig(cwd);
1998
+ const apiRoot = existsSync(configPath) || existsSync(legacyConfigPath) ? projectConfig.apiRoot ?? "src/api" : defaultApiRoot(layout);
1999
+ const apiRootPath = resolve(cwd, apiRoot);
2000
+ const sourceDir = join(apiRootPath, options.sourceKey);
2001
+ let httpTemplate = CUSTOM_HTTP_TEMPLATE;
2002
+ if (options.client === "axios") httpTemplate = AXIOS_HTTP_TEMPLATE;
2003
+ else if (options.client === "fetch") httpTemplate = FETCH_HTTP_TEMPLATE;
2004
+ const created = [];
2005
+ const skipped = [];
2006
+ const httpPath = join(apiRootPath, "http.ts");
2007
+ if (writeIfMissing(httpPath, httpTemplate) === "created") created.push(httpPath);
2008
+ else skipped.push(httpPath);
2009
+ const sourcePath = join(sourceDir, "source.ts");
2010
+ if (writeIfMissing(sourcePath, SOURCE_TEMPLATE) === "created") created.push(sourcePath);
2011
+ else skipped.push(sourcePath);
2012
+ const knownTypesPath = join(apiRootPath, "known-types.ts");
2013
+ if (writeIfMissing(knownTypesPath, KNOWN_TYPES_TEMPLATE) === "created") created.push(knownTypesPath);
2014
+ else skipped.push(knownTypesPath);
2015
+ const configWritePath = resolve(cwd, "typeforge.json");
2016
+ if (!existsSync(configWritePath) && !existsSync(legacyConfigPath)) {
2017
+ writeFileSync(configWritePath, `${JSON.stringify({ apiRoot }, null, 2)}\n`, "utf8");
2018
+ created.push(configWritePath);
2019
+ }
2020
+ mkdirSync(join(sourceDir, "generated"), { recursive: true });
2021
+ return {
2022
+ created,
2023
+ skipped
2024
+ };
2025
+ }
2026
+ //#endregion
2027
+ export { resolveSpecSource as a, RecursiveRefError as c, analyzeEnvelope as d, buildBaseResponseInterface as f, loadProjectConfig as g, listSourceKeys as h, loadSpec as i, parseSchema as l, parseUserBaseResponse as m, buildGenerateContext as n, loadKnownTypeRules as o, diffEnvelopeFields as p, generateForSource as r, matchKnownType as s, initProject as t, parseSpec as u };
2028
+
2029
+ //# sourceMappingURL=init-BBNO0SYd.js.map