openapi-domainify 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +303 -0
- package/bin/cli.js +2 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +886 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.js +808 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,886 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { writeFileSync as writeFileSync2, existsSync as existsSync3 } from "fs";
|
|
5
|
+
import { resolve as resolve3 } from "path";
|
|
6
|
+
|
|
7
|
+
// src/config.ts
|
|
8
|
+
import { existsSync } from "fs";
|
|
9
|
+
import { pathToFileURL } from "url";
|
|
10
|
+
import { resolve } from "path";
|
|
11
|
+
var CONFIG_FILES = [
|
|
12
|
+
"domainify.config.ts",
|
|
13
|
+
"domainify.config.js",
|
|
14
|
+
"domainify.config.mjs"
|
|
15
|
+
];
|
|
16
|
+
async function loadConfig(configPath) {
|
|
17
|
+
let resolvedPath;
|
|
18
|
+
if (configPath) {
|
|
19
|
+
resolvedPath = resolve(configPath);
|
|
20
|
+
if (!existsSync(resolvedPath)) {
|
|
21
|
+
throw new Error(`Config file not found: ${configPath}`);
|
|
22
|
+
}
|
|
23
|
+
} else {
|
|
24
|
+
for (const file of CONFIG_FILES) {
|
|
25
|
+
const fullPath = resolve(process.cwd(), file);
|
|
26
|
+
if (existsSync(fullPath)) {
|
|
27
|
+
resolvedPath = fullPath;
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (!resolvedPath) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`No config file found. Create one of: ${CONFIG_FILES.join(", ")}
|
|
35
|
+
Or run: npx openapi-domainify init`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
const configModule = await import(pathToFileURL(resolvedPath).href);
|
|
39
|
+
const config = configModule.default;
|
|
40
|
+
if (!config.input) {
|
|
41
|
+
throw new Error("Config missing required field: input");
|
|
42
|
+
}
|
|
43
|
+
if (!config.output) {
|
|
44
|
+
throw new Error("Config missing required field: output");
|
|
45
|
+
}
|
|
46
|
+
if (!config.domains || config.domains.length === 0) {
|
|
47
|
+
throw new Error("Config missing required field: domains (must have at least one)");
|
|
48
|
+
}
|
|
49
|
+
return resolveConfig(config);
|
|
50
|
+
}
|
|
51
|
+
function resolveConfig(config) {
|
|
52
|
+
return {
|
|
53
|
+
...config,
|
|
54
|
+
fallback: config.fallback ?? { name: "api", prefix: "/", className: "Api" },
|
|
55
|
+
httpClientImport: config.httpClientImport ?? "../../http",
|
|
56
|
+
overridesDir: config.overridesDir ?? "./overrides",
|
|
57
|
+
generateIndex: config.generateIndex ?? true
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/generator.ts
|
|
62
|
+
import { writeFileSync, mkdirSync, existsSync as existsSync2, rmSync, readFileSync } from "fs";
|
|
63
|
+
import { join, dirname, resolve as resolve2 } from "path";
|
|
64
|
+
import { execSync } from "child_process";
|
|
65
|
+
import * as ts from "typescript";
|
|
66
|
+
async function loadSpec(config) {
|
|
67
|
+
const { input } = config;
|
|
68
|
+
if (input.startsWith("http://") || input.startsWith("https://")) {
|
|
69
|
+
console.log("Fetching OpenAPI spec...");
|
|
70
|
+
console.log(` Source: ${input}`);
|
|
71
|
+
const response = await fetch(input);
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
74
|
+
}
|
|
75
|
+
const text = await response.text();
|
|
76
|
+
console.log(` Fetched successfully`);
|
|
77
|
+
return text;
|
|
78
|
+
}
|
|
79
|
+
const filePath = resolve2(input);
|
|
80
|
+
if (!existsSync2(filePath)) {
|
|
81
|
+
throw new Error(`OpenAPI spec not found: ${filePath}`);
|
|
82
|
+
}
|
|
83
|
+
console.log("Loading OpenAPI spec...");
|
|
84
|
+
console.log(` Source: ${filePath}`);
|
|
85
|
+
return readFileSync(filePath, "utf-8");
|
|
86
|
+
}
|
|
87
|
+
function parseSpec(content) {
|
|
88
|
+
try {
|
|
89
|
+
return JSON.parse(content);
|
|
90
|
+
} catch {
|
|
91
|
+
throw new Error(
|
|
92
|
+
"Failed to parse OpenAPI spec. Currently only JSON format is supported.\nIf you have a YAML spec, convert it to JSON first."
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function generateOpenapiTypes(specPath, outputPath) {
|
|
97
|
+
console.log("\nGenerating TypeScript types from OpenAPI spec...");
|
|
98
|
+
try {
|
|
99
|
+
execSync(`npx openapi-typescript "${specPath}" -o "${outputPath}"`, {
|
|
100
|
+
stdio: "pipe",
|
|
101
|
+
encoding: "utf-8"
|
|
102
|
+
});
|
|
103
|
+
console.log(` Generated base types`);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
106
|
+
throw new Error(`Failed to generate types: ${message}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function createProgram2(openapiTsPath) {
|
|
110
|
+
if (!existsSync2(openapiTsPath)) {
|
|
111
|
+
throw new Error(`Generated types not found: ${openapiTsPath}`);
|
|
112
|
+
}
|
|
113
|
+
const program = ts.createProgram([openapiTsPath], {
|
|
114
|
+
target: ts.ScriptTarget.ESNext,
|
|
115
|
+
module: ts.ModuleKind.ESNext,
|
|
116
|
+
strict: true
|
|
117
|
+
});
|
|
118
|
+
const checker = program.getTypeChecker();
|
|
119
|
+
const sourceFile = program.getSourceFile(openapiTsPath);
|
|
120
|
+
if (!sourceFile) {
|
|
121
|
+
throw new Error(`Could not load ${openapiTsPath}`);
|
|
122
|
+
}
|
|
123
|
+
let pathsSymbol;
|
|
124
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
125
|
+
if (ts.isInterfaceDeclaration(node) && node.name.text === "paths") {
|
|
126
|
+
pathsSymbol = checker.getSymbolAtLocation(node.name);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
if (!pathsSymbol) {
|
|
130
|
+
throw new Error('Could not find "paths" interface in generated types');
|
|
131
|
+
}
|
|
132
|
+
const pathsType = checker.getDeclaredTypeOfSymbol(pathsSymbol);
|
|
133
|
+
return { checker, sourceFile, pathsType };
|
|
134
|
+
}
|
|
135
|
+
function extractEndpoints(checker, sourceFile, pathsType) {
|
|
136
|
+
const endpoints = [];
|
|
137
|
+
const methods = ["get", "post", "put", "patch", "delete"];
|
|
138
|
+
for (const pathProp of pathsType.getProperties()) {
|
|
139
|
+
const path = pathProp.name;
|
|
140
|
+
const pathType = checker.getTypeOfSymbolAtLocation(pathProp, sourceFile);
|
|
141
|
+
for (const method of methods) {
|
|
142
|
+
const methodProp = pathType.getProperty(method);
|
|
143
|
+
if (!methodProp) continue;
|
|
144
|
+
const methodType = checker.getTypeOfSymbolAtLocation(methodProp, sourceFile);
|
|
145
|
+
const typeString = checker.typeToString(methodType);
|
|
146
|
+
if (typeString === "undefined" || typeString === "never") continue;
|
|
147
|
+
const operationType = checker.getApparentType(methodType);
|
|
148
|
+
const pathParams = (path.match(/\{([^}]+)\}/g) || []).map((p) => p.slice(1, -1));
|
|
149
|
+
const queryParams = [];
|
|
150
|
+
const parametersProp = operationType.getProperty("parameters");
|
|
151
|
+
if (parametersProp) {
|
|
152
|
+
const paramsType = checker.getApparentType(
|
|
153
|
+
checker.getTypeOfSymbolAtLocation(parametersProp, sourceFile)
|
|
154
|
+
);
|
|
155
|
+
const queryProp = paramsType.getProperty("query");
|
|
156
|
+
if (queryProp) {
|
|
157
|
+
const queryType = checker.getTypeOfSymbolAtLocation(queryProp, sourceFile);
|
|
158
|
+
for (const prop of queryType.getProperties()) {
|
|
159
|
+
queryParams.push(prop.name);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const requestBodyProp = operationType.getProperty("requestBody");
|
|
164
|
+
let hasBody = false;
|
|
165
|
+
if (requestBodyProp) {
|
|
166
|
+
const bodyType = checker.getTypeOfSymbolAtLocation(requestBodyProp, sourceFile);
|
|
167
|
+
hasBody = !(bodyType.flags & (ts.TypeFlags.Never | ts.TypeFlags.Undefined));
|
|
168
|
+
}
|
|
169
|
+
endpoints.push({
|
|
170
|
+
path,
|
|
171
|
+
method,
|
|
172
|
+
pathParams,
|
|
173
|
+
queryParams,
|
|
174
|
+
hasBody
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return endpoints;
|
|
179
|
+
}
|
|
180
|
+
function createTypeExtractor(checker, sourceFile, pathsType) {
|
|
181
|
+
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
|
182
|
+
function formatType(type) {
|
|
183
|
+
const typeNode = checker.typeToTypeNode(
|
|
184
|
+
type,
|
|
185
|
+
sourceFile,
|
|
186
|
+
ts.NodeBuilderFlags.NoTruncation | ts.NodeBuilderFlags.MultilineObjectLiterals
|
|
187
|
+
);
|
|
188
|
+
if (!typeNode) {
|
|
189
|
+
return checker.typeToString(type, sourceFile, ts.TypeFormatFlags.NoTruncation);
|
|
190
|
+
}
|
|
191
|
+
return printer.printNode(ts.EmitHint.Unspecified, typeNode, sourceFile);
|
|
192
|
+
}
|
|
193
|
+
function getNestedType(type, ...propertyPath) {
|
|
194
|
+
let currentType = type;
|
|
195
|
+
for (const prop of propertyPath) {
|
|
196
|
+
currentType = checker.getApparentType(currentType);
|
|
197
|
+
const property = currentType.getProperty(prop);
|
|
198
|
+
if (!property) return void 0;
|
|
199
|
+
currentType = checker.getTypeOfSymbolAtLocation(property, sourceFile);
|
|
200
|
+
}
|
|
201
|
+
return checker.getApparentType(currentType);
|
|
202
|
+
}
|
|
203
|
+
function getMethodType(path, method) {
|
|
204
|
+
const pathProp = pathsType.getProperty(path);
|
|
205
|
+
if (!pathProp) return void 0;
|
|
206
|
+
const pathType = checker.getTypeOfSymbolAtLocation(pathProp, sourceFile);
|
|
207
|
+
const methodProp = pathType.getProperty(method);
|
|
208
|
+
if (!methodProp) return void 0;
|
|
209
|
+
return checker.getApparentType(checker.getTypeOfSymbolAtLocation(methodProp, sourceFile));
|
|
210
|
+
}
|
|
211
|
+
function isUnknownType(typeStr) {
|
|
212
|
+
return typeStr === "unknown" || typeStr === "never" || typeStr === "undefined";
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
getResponseType(path, method) {
|
|
216
|
+
const methodType = getMethodType(path, method);
|
|
217
|
+
if (!methodType) return null;
|
|
218
|
+
const responseType = getNestedType(methodType, "responses", "200", "content", "application/json");
|
|
219
|
+
if (!responseType) return null;
|
|
220
|
+
const formatted = formatType(responseType);
|
|
221
|
+
return isUnknownType(formatted) ? null : formatted;
|
|
222
|
+
},
|
|
223
|
+
getRequestType(path, method) {
|
|
224
|
+
const methodType = getMethodType(path, method);
|
|
225
|
+
if (!methodType) return null;
|
|
226
|
+
const requestBodyProp = methodType.getProperty("requestBody");
|
|
227
|
+
if (!requestBodyProp) return null;
|
|
228
|
+
let requestBodyType = checker.getTypeOfSymbolAtLocation(requestBodyProp, sourceFile);
|
|
229
|
+
if (requestBodyType.isUnion()) {
|
|
230
|
+
const nonUndefinedTypes = requestBodyType.types.filter(
|
|
231
|
+
(t) => !(t.flags & ts.TypeFlags.Undefined)
|
|
232
|
+
);
|
|
233
|
+
if (nonUndefinedTypes.length === 1) {
|
|
234
|
+
requestBodyType = nonUndefinedTypes[0];
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
requestBodyType = checker.getApparentType(requestBodyType);
|
|
238
|
+
const requestType = getNestedType(requestBodyType, "content", "application/json");
|
|
239
|
+
if (!requestType) return null;
|
|
240
|
+
const formatted = formatType(requestType);
|
|
241
|
+
return isUnknownType(formatted) ? null : formatted;
|
|
242
|
+
},
|
|
243
|
+
getQueryType(path, method) {
|
|
244
|
+
const methodType = getMethodType(path, method);
|
|
245
|
+
if (!methodType) return null;
|
|
246
|
+
const queryType = getNestedType(methodType, "parameters", "query");
|
|
247
|
+
if (!queryType) return null;
|
|
248
|
+
const formatted = formatType(queryType);
|
|
249
|
+
return isUnknownType(formatted) ? null : formatted;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function getAllDomains(config) {
|
|
254
|
+
return [...config.domains, config.fallback];
|
|
255
|
+
}
|
|
256
|
+
function getDomain(path, config) {
|
|
257
|
+
const strippedPath = config.stripPrefix ? path.replace(config.stripPrefix, "") : path;
|
|
258
|
+
for (const domain of config.domains) {
|
|
259
|
+
if (strippedPath.startsWith(domain.prefix)) {
|
|
260
|
+
return domain.name;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return config.fallback.name;
|
|
264
|
+
}
|
|
265
|
+
function getDomainConfig(name, config) {
|
|
266
|
+
const found = getAllDomains(config).find((d) => d.name === name);
|
|
267
|
+
if (!found) {
|
|
268
|
+
throw new Error(`Unknown domain: ${name}`);
|
|
269
|
+
}
|
|
270
|
+
return found;
|
|
271
|
+
}
|
|
272
|
+
function pascalCase(str) {
|
|
273
|
+
return str.replace(/[-_\/]([a-z])/g, (_, c) => c.toUpperCase()).replace(/^[a-z]/, (c) => c.toUpperCase()).replace(/[{}]/g, "");
|
|
274
|
+
}
|
|
275
|
+
function camelCase(str) {
|
|
276
|
+
return str.replace(/[-_\/]([a-z])/g, (_, c) => c.toUpperCase()).replace(/^[A-Z]/, (c) => c.toLowerCase()).replace(/[{}]/g, "");
|
|
277
|
+
}
|
|
278
|
+
function singularize(word) {
|
|
279
|
+
if (word.endsWith("ies")) return word.slice(0, -3) + "y";
|
|
280
|
+
if (/(x|ch|sh|ss|z)es$/i.test(word)) return word.slice(0, -2);
|
|
281
|
+
if (word.endsWith("s") && !word.endsWith("ss")) return word.slice(0, -1);
|
|
282
|
+
return word;
|
|
283
|
+
}
|
|
284
|
+
function getMethodName(endpoint, domain, config) {
|
|
285
|
+
const domainConfig = getDomainConfig(domain, config);
|
|
286
|
+
let relativePath = endpoint.path;
|
|
287
|
+
if (config.stripPrefix) {
|
|
288
|
+
relativePath = relativePath.replace(config.stripPrefix, "");
|
|
289
|
+
}
|
|
290
|
+
relativePath = relativePath.replace(domainConfig.prefix, "").replace(/^\//, "");
|
|
291
|
+
const allSegments = relativePath.split("/").filter(Boolean);
|
|
292
|
+
const endsWithParam = allSegments.length > 0 && allSegments[allSegments.length - 1].startsWith("{");
|
|
293
|
+
const segments = allSegments.filter((seg) => !seg.startsWith("{"));
|
|
294
|
+
if (segments.length === 0) {
|
|
295
|
+
return endpoint.method;
|
|
296
|
+
}
|
|
297
|
+
const resource = segments[0];
|
|
298
|
+
const subPath = segments.slice(1);
|
|
299
|
+
const resourceName = endsWithParam ? singularize(pascalCase(resource)) : pascalCase(resource);
|
|
300
|
+
const subPathPart = subPath.map(pascalCase).join("");
|
|
301
|
+
const fullPath = resourceName + subPathPart;
|
|
302
|
+
switch (endpoint.method) {
|
|
303
|
+
case "get":
|
|
304
|
+
if (endsWithParam || subPath.length > 0) {
|
|
305
|
+
return `get${fullPath}`;
|
|
306
|
+
}
|
|
307
|
+
return camelCase(fullPath);
|
|
308
|
+
case "post":
|
|
309
|
+
return `create${singularize(fullPath)}`;
|
|
310
|
+
case "put":
|
|
311
|
+
return `update${singularize(fullPath)}`;
|
|
312
|
+
case "patch":
|
|
313
|
+
return `patch${singularize(fullPath)}`;
|
|
314
|
+
case "delete":
|
|
315
|
+
return `delete${singularize(fullPath)}`;
|
|
316
|
+
default:
|
|
317
|
+
return `${endpoint.method}${fullPath}`;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
function getTypeName(path, method, kind, config) {
|
|
321
|
+
let cleanPath = path;
|
|
322
|
+
if (config.stripPrefix) {
|
|
323
|
+
cleanPath = cleanPath.replace(config.stripPrefix, "");
|
|
324
|
+
}
|
|
325
|
+
cleanPath = cleanPath.replace(/^\//, "").replace(/\{[^}]+\}/g, "ById").split("/").map(pascalCase).join("");
|
|
326
|
+
const methodPrefix = method.charAt(0).toUpperCase() + method.slice(1);
|
|
327
|
+
return `${methodPrefix}${cleanPath}${kind}`;
|
|
328
|
+
}
|
|
329
|
+
function groupByDomain(endpoints, config) {
|
|
330
|
+
const groups = /* @__PURE__ */ new Map();
|
|
331
|
+
for (const domain of getAllDomains(config)) {
|
|
332
|
+
groups.set(domain.name, []);
|
|
333
|
+
}
|
|
334
|
+
for (const endpoint of endpoints) {
|
|
335
|
+
const domain = getDomain(endpoint.path, config);
|
|
336
|
+
const domainEndpoints = groups.get(domain);
|
|
337
|
+
if (domainEndpoints) {
|
|
338
|
+
domainEndpoints.push(endpoint);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return groups;
|
|
342
|
+
}
|
|
343
|
+
function generateTypes(domain, endpoints, extractor, config) {
|
|
344
|
+
const lines = [];
|
|
345
|
+
const domainConfig = getDomainConfig(domain, config);
|
|
346
|
+
const allTypes = [];
|
|
347
|
+
const requestTypes = [];
|
|
348
|
+
for (const endpoint of endpoints) {
|
|
349
|
+
if (endpoint.hasBody) {
|
|
350
|
+
const typeBody = extractor.getRequestType(endpoint.path, endpoint.method);
|
|
351
|
+
if (typeBody) {
|
|
352
|
+
const typeName = getTypeName(endpoint.path, endpoint.method, "Request", config);
|
|
353
|
+
requestTypes.push(`export type ${typeName} = ${typeBody}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
allTypes.push(...requestTypes);
|
|
358
|
+
const queryTypes = [];
|
|
359
|
+
for (const endpoint of endpoints) {
|
|
360
|
+
if (endpoint.queryParams.length > 0) {
|
|
361
|
+
const typeBody = extractor.getQueryType(endpoint.path, endpoint.method);
|
|
362
|
+
if (typeBody) {
|
|
363
|
+
const typeName = getTypeName(endpoint.path, endpoint.method, "Query", config);
|
|
364
|
+
queryTypes.push(`export type ${typeName} = ${typeBody}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
allTypes.push(...queryTypes);
|
|
369
|
+
const responseTypes = [];
|
|
370
|
+
for (const endpoint of endpoints) {
|
|
371
|
+
const typeBody = extractor.getResponseType(endpoint.path, endpoint.method);
|
|
372
|
+
if (typeBody) {
|
|
373
|
+
const typeName = getTypeName(endpoint.path, endpoint.method, "Response", config);
|
|
374
|
+
responseTypes.push(`export type ${typeName} = ${typeBody}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
allTypes.push(...responseTypes);
|
|
378
|
+
const usesComponents = allTypes.some((t) => t.includes("components["));
|
|
379
|
+
lines.push(`/**`);
|
|
380
|
+
lines.push(` * ${domainConfig.className} Domain Types`);
|
|
381
|
+
lines.push(` * Auto-generated - DO NOT EDIT`);
|
|
382
|
+
lines.push(` */`);
|
|
383
|
+
if (usesComponents) {
|
|
384
|
+
lines.push(`import type { components } from '../openapi'`);
|
|
385
|
+
}
|
|
386
|
+
lines.push(``);
|
|
387
|
+
lines.push(`// \u2500\u2500\u2500 Common Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
388
|
+
lines.push(``);
|
|
389
|
+
lines.push(`export interface PaginatedResponse<T> {`);
|
|
390
|
+
lines.push(` data: T[]`);
|
|
391
|
+
lines.push(` current_page: number`);
|
|
392
|
+
lines.push(` last_page: number`);
|
|
393
|
+
lines.push(` per_page: number`);
|
|
394
|
+
lines.push(` total: number`);
|
|
395
|
+
lines.push(` from: number | null`);
|
|
396
|
+
lines.push(` to: number | null`);
|
|
397
|
+
lines.push(`}`);
|
|
398
|
+
lines.push(``);
|
|
399
|
+
if (requestTypes.length > 0) {
|
|
400
|
+
lines.push(`// \u2500\u2500\u2500 Request Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
401
|
+
lines.push(``);
|
|
402
|
+
lines.push(...requestTypes);
|
|
403
|
+
lines.push(``);
|
|
404
|
+
}
|
|
405
|
+
if (queryTypes.length > 0) {
|
|
406
|
+
lines.push(`// \u2500\u2500\u2500 Query Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
407
|
+
lines.push(``);
|
|
408
|
+
lines.push(...queryTypes);
|
|
409
|
+
lines.push(``);
|
|
410
|
+
}
|
|
411
|
+
if (responseTypes.length > 0) {
|
|
412
|
+
lines.push(`// \u2500\u2500\u2500 Response Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
413
|
+
lines.push(``);
|
|
414
|
+
lines.push(...responseTypes);
|
|
415
|
+
lines.push(``);
|
|
416
|
+
}
|
|
417
|
+
return lines.join("\n");
|
|
418
|
+
}
|
|
419
|
+
function generateService(domain, endpoints, extractor, config) {
|
|
420
|
+
const lines = [];
|
|
421
|
+
const domainConfig = getDomainConfig(domain, config);
|
|
422
|
+
const className = `${domainConfig.className}Service`;
|
|
423
|
+
lines.push(`/**`);
|
|
424
|
+
lines.push(` * ${domainConfig.className} Domain Service`);
|
|
425
|
+
lines.push(` * Auto-generated - DO NOT EDIT`);
|
|
426
|
+
lines.push(` */`);
|
|
427
|
+
lines.push(`import type { HttpClient } from '${config.httpClientImport}'`);
|
|
428
|
+
lines.push(`import type * as T from './types'`);
|
|
429
|
+
lines.push(``);
|
|
430
|
+
lines.push(`export class ${className} {`);
|
|
431
|
+
lines.push(` constructor(private http: HttpClient) {}`);
|
|
432
|
+
const usedNames = /* @__PURE__ */ new Map();
|
|
433
|
+
const resourceGroups = /* @__PURE__ */ new Map();
|
|
434
|
+
for (const endpoint of endpoints) {
|
|
435
|
+
let relativePath = endpoint.path;
|
|
436
|
+
if (config.stripPrefix) {
|
|
437
|
+
relativePath = relativePath.replace(config.stripPrefix, "");
|
|
438
|
+
}
|
|
439
|
+
relativePath = relativePath.replace(domainConfig.prefix, "").replace(/^\//, "");
|
|
440
|
+
const resource = relativePath.split("/")[0]?.replace(/\{[^}]+\}/, "") || "root";
|
|
441
|
+
if (!resourceGroups.has(resource)) resourceGroups.set(resource, []);
|
|
442
|
+
resourceGroups.get(resource).push(endpoint);
|
|
443
|
+
}
|
|
444
|
+
for (const [resource, resourceEndpoints] of resourceGroups) {
|
|
445
|
+
lines.push(``);
|
|
446
|
+
lines.push(` // \u2500\u2500\u2500 ${pascalCase(resource) || "Root"} \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
447
|
+
for (const endpoint of resourceEndpoints) {
|
|
448
|
+
let methodName = getMethodName(endpoint, domain, config);
|
|
449
|
+
const count = usedNames.get(methodName) || 0;
|
|
450
|
+
if (count > 0) {
|
|
451
|
+
methodName = `${methodName}${count + 1}`;
|
|
452
|
+
}
|
|
453
|
+
usedNames.set(methodName, count + 1);
|
|
454
|
+
const params = [];
|
|
455
|
+
for (const param of endpoint.pathParams) {
|
|
456
|
+
const paramName = camelCase(param.replace(/_id$/, "Id"));
|
|
457
|
+
params.push(`${paramName}: string | number`);
|
|
458
|
+
}
|
|
459
|
+
if (endpoint.queryParams.length > 0) {
|
|
460
|
+
const queryType = extractor.getQueryType(endpoint.path, endpoint.method);
|
|
461
|
+
if (queryType) {
|
|
462
|
+
const queryTypeName = getTypeName(endpoint.path, endpoint.method, "Query", config);
|
|
463
|
+
params.push(`params?: T.${queryTypeName}`);
|
|
464
|
+
} else {
|
|
465
|
+
params.push(`params?: Record<string, unknown>`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (endpoint.hasBody && endpoint.method !== "get") {
|
|
469
|
+
const requestType = extractor.getRequestType(endpoint.path, endpoint.method);
|
|
470
|
+
if (requestType) {
|
|
471
|
+
const bodyTypeName = getTypeName(endpoint.path, endpoint.method, "Request", config);
|
|
472
|
+
params.push(`data: T.${bodyTypeName}`);
|
|
473
|
+
} else {
|
|
474
|
+
params.push(`data: unknown`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
const responseType = extractor.getResponseType(endpoint.path, endpoint.method);
|
|
478
|
+
let returnType;
|
|
479
|
+
if (responseType) {
|
|
480
|
+
const responseTypeName = getTypeName(endpoint.path, endpoint.method, "Response", config);
|
|
481
|
+
returnType = `T.${responseTypeName}`;
|
|
482
|
+
} else {
|
|
483
|
+
returnType = "unknown";
|
|
484
|
+
}
|
|
485
|
+
let urlPath = endpoint.path;
|
|
486
|
+
if (config.stripPrefix) {
|
|
487
|
+
urlPath = urlPath.replace(config.stripPrefix, "");
|
|
488
|
+
}
|
|
489
|
+
for (const param of endpoint.pathParams) {
|
|
490
|
+
const paramName = camelCase(param.replace(/_id$/, "Id"));
|
|
491
|
+
urlPath = urlPath.replace(`{${param}}`, `\${${paramName}}`);
|
|
492
|
+
}
|
|
493
|
+
const pathLiteral = urlPath.includes("$") ? "`" + urlPath + "`" : `'${urlPath}'`;
|
|
494
|
+
lines.push(` ${methodName}(${params.join(", ")}): Promise<${returnType}> {`);
|
|
495
|
+
const hasParams = endpoint.queryParams.length > 0;
|
|
496
|
+
switch (endpoint.method) {
|
|
497
|
+
case "get":
|
|
498
|
+
lines.push(hasParams ? ` return this.http.get(${pathLiteral}, { params })` : ` return this.http.get(${pathLiteral})`);
|
|
499
|
+
break;
|
|
500
|
+
case "post":
|
|
501
|
+
lines.push(endpoint.hasBody ? ` return this.http.post(${pathLiteral}, data)` : ` return this.http.post(${pathLiteral})`);
|
|
502
|
+
break;
|
|
503
|
+
case "put":
|
|
504
|
+
lines.push(endpoint.hasBody ? ` return this.http.put(${pathLiteral}, data)` : ` return this.http.put(${pathLiteral})`);
|
|
505
|
+
break;
|
|
506
|
+
case "patch":
|
|
507
|
+
lines.push(endpoint.hasBody ? ` return this.http.patch(${pathLiteral}, data)` : ` return this.http.patch(${pathLiteral})`);
|
|
508
|
+
break;
|
|
509
|
+
case "delete":
|
|
510
|
+
lines.push(hasParams ? ` return this.http.delete(${pathLiteral}, { params })` : ` return this.http.delete(${pathLiteral})`);
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
lines.push(` }`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
lines.push(`}`);
|
|
517
|
+
lines.push(``);
|
|
518
|
+
return lines.join("\n");
|
|
519
|
+
}
|
|
520
|
+
function generateOverrideTemplate(domain, config) {
|
|
521
|
+
const domainConfig = getDomainConfig(domain, config);
|
|
522
|
+
const className = `${domainConfig.className}Service`;
|
|
523
|
+
return `/**
|
|
524
|
+
* ${domainConfig.className} Service Overrides
|
|
525
|
+
*
|
|
526
|
+
* Extend or override methods from the generated ${className}.
|
|
527
|
+
* This file is NOT overwritten by the generator - safe to edit.
|
|
528
|
+
*
|
|
529
|
+
* To enable: update index.ts to import from here instead of generated.
|
|
530
|
+
*/
|
|
531
|
+
import { ${className} as Generated${className} } from '../../generated/${domain}/service'
|
|
532
|
+
import type { HttpClient } from '../../http'
|
|
533
|
+
|
|
534
|
+
export class ${className} extends Generated${className} {
|
|
535
|
+
constructor(http: HttpClient) {
|
|
536
|
+
super(http)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Example: Override a method that has issues in the OpenAPI spec
|
|
540
|
+
// async someMethod(): Promise<SomeType> {
|
|
541
|
+
// // Custom implementation or fix
|
|
542
|
+
// return super.someMethod()
|
|
543
|
+
// }
|
|
544
|
+
|
|
545
|
+
// Example: Add a method not in the OpenAPI spec
|
|
546
|
+
// async customMethod(): Promise<void> {
|
|
547
|
+
// // Custom logic
|
|
548
|
+
// }
|
|
549
|
+
}
|
|
550
|
+
`;
|
|
551
|
+
}
|
|
552
|
+
function generateHttpTemplate(config) {
|
|
553
|
+
return `/**
|
|
554
|
+
* HTTP Client Interface
|
|
555
|
+
*
|
|
556
|
+
* Implement this interface with your preferred HTTP library.
|
|
557
|
+
* This file is NOT overwritten by the generator - safe to edit.
|
|
558
|
+
*/
|
|
559
|
+
|
|
560
|
+
export interface HttpClient {
|
|
561
|
+
get<T>(url: string, options?: { params?: Record<string, unknown> }): Promise<T>
|
|
562
|
+
post<T>(url: string, body?: Record<string, unknown>): Promise<T>
|
|
563
|
+
put<T>(url: string, body?: Record<string, unknown>): Promise<T>
|
|
564
|
+
patch<T>(url: string, body?: Record<string, unknown>): Promise<T>
|
|
565
|
+
delete<T>(url: string, options?: { params?: Record<string, unknown> }): Promise<T>
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// \u2500\u2500\u2500 Example Implementation (using fetch) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
569
|
+
|
|
570
|
+
export interface HttpClientOptions {
|
|
571
|
+
baseUrl: string
|
|
572
|
+
getToken?: () => string | null | Promise<string | null>
|
|
573
|
+
onError?: (error: ApiError) => void
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
export interface ApiError {
|
|
577
|
+
status: number
|
|
578
|
+
statusText: string
|
|
579
|
+
message: string
|
|
580
|
+
data?: unknown
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
export class FetchHttpClient implements HttpClient {
|
|
584
|
+
constructor(private options: HttpClientOptions) {}
|
|
585
|
+
|
|
586
|
+
private async request<T>(
|
|
587
|
+
method: string,
|
|
588
|
+
url: string,
|
|
589
|
+
body?: unknown,
|
|
590
|
+
params?: Record<string, unknown>
|
|
591
|
+
): Promise<T> {
|
|
592
|
+
const query = params
|
|
593
|
+
? '?' + new URLSearchParams(
|
|
594
|
+
Object.entries(params)
|
|
595
|
+
.filter(([, v]) => v !== undefined)
|
|
596
|
+
.map(([k, v]) => [k, String(v)])
|
|
597
|
+
).toString()
|
|
598
|
+
: ''
|
|
599
|
+
|
|
600
|
+
const token = await this.options.getToken?.()
|
|
601
|
+
|
|
602
|
+
const response = await fetch(this.options.baseUrl + url + query, {
|
|
603
|
+
method,
|
|
604
|
+
headers: {
|
|
605
|
+
'Content-Type': 'application/json',
|
|
606
|
+
...(token && { Authorization: \`Bearer \${token}\` }),
|
|
607
|
+
},
|
|
608
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
if (!response.ok) {
|
|
612
|
+
const error: ApiError = {
|
|
613
|
+
status: response.status,
|
|
614
|
+
statusText: response.statusText,
|
|
615
|
+
message: response.statusText,
|
|
616
|
+
}
|
|
617
|
+
try {
|
|
618
|
+
error.data = await response.json()
|
|
619
|
+
error.message = (error.data as { message?: string })?.message || error.statusText
|
|
620
|
+
} catch {}
|
|
621
|
+
this.options.onError?.(error)
|
|
622
|
+
throw error
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return response.json()
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
get<T>(url: string, opts?: { params?: Record<string, unknown> }) {
|
|
629
|
+
return this.request<T>('GET', url, undefined, opts?.params)
|
|
630
|
+
}
|
|
631
|
+
post<T>(url: string, body?: Record<string, unknown>) {
|
|
632
|
+
return this.request<T>('POST', url, body)
|
|
633
|
+
}
|
|
634
|
+
put<T>(url: string, body?: Record<string, unknown>) {
|
|
635
|
+
return this.request<T>('PUT', url, body)
|
|
636
|
+
}
|
|
637
|
+
patch<T>(url: string, body?: Record<string, unknown>) {
|
|
638
|
+
return this.request<T>('PATCH', url, body)
|
|
639
|
+
}
|
|
640
|
+
delete<T>(url: string, opts?: { params?: Record<string, unknown> }) {
|
|
641
|
+
return this.request<T>('DELETE', url, undefined, opts?.params)
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
`;
|
|
645
|
+
}
|
|
646
|
+
function generateIndexFile(activeDomains, config, overridesDir) {
|
|
647
|
+
const lines = [];
|
|
648
|
+
lines.push(`/**`);
|
|
649
|
+
lines.push(` * API Client`);
|
|
650
|
+
lines.push(` *`);
|
|
651
|
+
lines.push(` * Auto-generated index that wires domain services together.`);
|
|
652
|
+
lines.push(` * This file IS overwritten by the generator.`);
|
|
653
|
+
lines.push(` *`);
|
|
654
|
+
lines.push(` * To use overrides: uncomment the override import and comment out the generated import.`);
|
|
655
|
+
lines.push(` */`);
|
|
656
|
+
lines.push(`import type { HttpClient } from './http'`);
|
|
657
|
+
lines.push(``);
|
|
658
|
+
for (const domain of activeDomains) {
|
|
659
|
+
const domainConfig = getDomainConfig(domain, config);
|
|
660
|
+
const className = `${domainConfig.className}Service`;
|
|
661
|
+
const overridePath = join(overridesDir, domain, "service.ts");
|
|
662
|
+
const hasOverride = existsSync2(overridePath);
|
|
663
|
+
if (hasOverride) {
|
|
664
|
+
const content = readFileSync(overridePath, "utf-8");
|
|
665
|
+
const isCustomized = !content.includes("// Example: Override a method") || content.split("async ").length > 2;
|
|
666
|
+
if (isCustomized) {
|
|
667
|
+
lines.push(`import { ${className} } from './overrides/${domain}/service'`);
|
|
668
|
+
} else {
|
|
669
|
+
lines.push(`// import { ${className} } from './overrides/${domain}/service'`);
|
|
670
|
+
lines.push(`import { ${className} } from './generated/${domain}/service'`);
|
|
671
|
+
}
|
|
672
|
+
} else {
|
|
673
|
+
lines.push(`import { ${className} } from './generated/${domain}/service'`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
lines.push(``);
|
|
677
|
+
lines.push(`// \u2500\u2500\u2500 API Client \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
678
|
+
lines.push(``);
|
|
679
|
+
lines.push(`export class ApiClient {`);
|
|
680
|
+
lines.push(` constructor(private http: HttpClient) {`);
|
|
681
|
+
for (const domain of activeDomains) {
|
|
682
|
+
const domainConfig = getDomainConfig(domain, config);
|
|
683
|
+
lines.push(` this.${domain} = new ${domainConfig.className}Service(http)`);
|
|
684
|
+
}
|
|
685
|
+
lines.push(` }`);
|
|
686
|
+
lines.push(``);
|
|
687
|
+
for (const domain of activeDomains) {
|
|
688
|
+
const domainConfig = getDomainConfig(domain, config);
|
|
689
|
+
lines.push(` readonly ${domain}: ${domainConfig.className}Service`);
|
|
690
|
+
}
|
|
691
|
+
lines.push(`}`);
|
|
692
|
+
lines.push(``);
|
|
693
|
+
lines.push(`export function createApiClient(http: HttpClient): ApiClient {`);
|
|
694
|
+
lines.push(` return new ApiClient(http)`);
|
|
695
|
+
lines.push(`}`);
|
|
696
|
+
lines.push(``);
|
|
697
|
+
lines.push(`// \u2500\u2500\u2500 Re-exports \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
698
|
+
lines.push(``);
|
|
699
|
+
for (const domain of activeDomains) {
|
|
700
|
+
const domainConfig = getDomainConfig(domain, config);
|
|
701
|
+
lines.push(`export * as ${domainConfig.className}Types from './generated/${domain}/types'`);
|
|
702
|
+
}
|
|
703
|
+
lines.push(``);
|
|
704
|
+
return lines.join("\n");
|
|
705
|
+
}
|
|
706
|
+
function writeFileSafe(path, content, overwrite = true) {
|
|
707
|
+
if (!overwrite && existsSync2(path)) {
|
|
708
|
+
return false;
|
|
709
|
+
}
|
|
710
|
+
writeFileSync(path, content);
|
|
711
|
+
return true;
|
|
712
|
+
}
|
|
713
|
+
async function generate(config) {
|
|
714
|
+
console.log("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
|
|
715
|
+
console.log("\u2551 openapi-domainify \u2551");
|
|
716
|
+
console.log("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D\n");
|
|
717
|
+
const specContent = await loadSpec(config);
|
|
718
|
+
const spec = parseSpec(specContent);
|
|
719
|
+
const outputDir = resolve2(config.output);
|
|
720
|
+
const generatedDir = join(outputDir, "generated");
|
|
721
|
+
const overridesDir = resolve2(outputDir, config.overridesDir);
|
|
722
|
+
if (!existsSync2(outputDir)) {
|
|
723
|
+
mkdirSync(outputDir, { recursive: true });
|
|
724
|
+
}
|
|
725
|
+
if (!existsSync2(generatedDir)) {
|
|
726
|
+
mkdirSync(generatedDir, { recursive: true });
|
|
727
|
+
}
|
|
728
|
+
const specPath = config.specOutput ? resolve2(config.specOutput) : join(generatedDir, "openapi.json");
|
|
729
|
+
const specDir = dirname(specPath);
|
|
730
|
+
if (!existsSync2(specDir)) {
|
|
731
|
+
mkdirSync(specDir, { recursive: true });
|
|
732
|
+
}
|
|
733
|
+
writeFileSync(specPath, JSON.stringify(spec, null, 2));
|
|
734
|
+
const openapiTsPath = join(generatedDir, "openapi.ts");
|
|
735
|
+
generateOpenapiTypes(specPath, openapiTsPath);
|
|
736
|
+
console.log("\n\u{1F4CA} Generating domain services...");
|
|
737
|
+
const { checker, sourceFile, pathsType } = createProgram2(openapiTsPath);
|
|
738
|
+
console.log(" Extracting endpoints...");
|
|
739
|
+
const endpoints = extractEndpoints(checker, sourceFile, pathsType);
|
|
740
|
+
console.log(` Found ${endpoints.length} endpoints`);
|
|
741
|
+
const extractor = createTypeExtractor(checker, sourceFile, pathsType);
|
|
742
|
+
const domains = groupByDomain(endpoints, config);
|
|
743
|
+
const activeDomains = [];
|
|
744
|
+
for (const [domain, domainEndpoints] of domains) {
|
|
745
|
+
if (domainEndpoints.length === 0) continue;
|
|
746
|
+
activeDomains.push(domain);
|
|
747
|
+
const domainDir = join(generatedDir, domain);
|
|
748
|
+
if (existsSync2(domainDir)) {
|
|
749
|
+
rmSync(domainDir, { recursive: true });
|
|
750
|
+
}
|
|
751
|
+
mkdirSync(domainDir, { recursive: true });
|
|
752
|
+
console.log(`
|
|
753
|
+
\u{1F4C1} generated/${domain}/ (${domainEndpoints.length} endpoints)`);
|
|
754
|
+
const typesContent = generateTypes(domain, domainEndpoints, extractor, config);
|
|
755
|
+
writeFileSync(join(domainDir, "types.ts"), typesContent);
|
|
756
|
+
console.log(` \u2728 types.ts`);
|
|
757
|
+
const serviceContent = generateService(domain, domainEndpoints, extractor, config);
|
|
758
|
+
writeFileSync(join(domainDir, "service.ts"), serviceContent);
|
|
759
|
+
console.log(` \u2728 service.ts`);
|
|
760
|
+
}
|
|
761
|
+
console.log("\n\u{1F3D7}\uFE0F Scaffolding...");
|
|
762
|
+
if (!existsSync2(overridesDir)) {
|
|
763
|
+
mkdirSync(overridesDir, { recursive: true });
|
|
764
|
+
}
|
|
765
|
+
for (const domain of activeDomains) {
|
|
766
|
+
const overrideDomainDir = join(overridesDir, domain);
|
|
767
|
+
if (!existsSync2(overrideDomainDir)) {
|
|
768
|
+
mkdirSync(overrideDomainDir, { recursive: true });
|
|
769
|
+
}
|
|
770
|
+
const overrideServicePath = join(overrideDomainDir, "service.ts");
|
|
771
|
+
if (writeFileSafe(overrideServicePath, generateOverrideTemplate(domain, config), false)) {
|
|
772
|
+
console.log(` \u{1F4DD} overrides/${domain}/service.ts (new)`);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
const httpPath = join(outputDir, "http.ts");
|
|
776
|
+
if (writeFileSafe(httpPath, generateHttpTemplate(config), false)) {
|
|
777
|
+
console.log(` \u{1F4DD} http.ts (new)`);
|
|
778
|
+
}
|
|
779
|
+
if (config.generateIndex) {
|
|
780
|
+
const indexContent = generateIndexFile(activeDomains, config, overridesDir);
|
|
781
|
+
writeFileSync(join(outputDir, "index.ts"), indexContent);
|
|
782
|
+
console.log(` \u2728 index.ts`);
|
|
783
|
+
}
|
|
784
|
+
console.log("\n\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
|
|
785
|
+
console.log("\u2551 \u2705 Generation Complete! \u2551");
|
|
786
|
+
console.log("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
|
|
787
|
+
console.log(`
|
|
788
|
+
Output: ${outputDir}`);
|
|
789
|
+
console.log(`
|
|
790
|
+
Structure:`);
|
|
791
|
+
console.log(` ${outputDir}/`);
|
|
792
|
+
console.log(` \u251C\u2500\u2500 index.ts # API client (regenerated)`);
|
|
793
|
+
console.log(` \u251C\u2500\u2500 http.ts # HTTP client template (safe to edit)`);
|
|
794
|
+
console.log(` \u251C\u2500\u2500 generated/ # Auto-generated (DO NOT EDIT)`);
|
|
795
|
+
for (const domain of activeDomains) {
|
|
796
|
+
console.log(` \u2502 \u2514\u2500\u2500 ${domain}/`);
|
|
797
|
+
}
|
|
798
|
+
console.log(` \u2514\u2500\u2500 overrides/ # Your customizations (safe to edit)`);
|
|
799
|
+
for (const domain of activeDomains) {
|
|
800
|
+
console.log(` \u2514\u2500\u2500 ${domain}/`);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// src/cli.ts
|
|
805
|
+
var INIT_CONFIG = `import { defineConfig } from 'openapi-domainify'
|
|
806
|
+
|
|
807
|
+
export default defineConfig({
|
|
808
|
+
// OpenAPI spec source (URL or local file path)
|
|
809
|
+
input: 'https://api.example.com/openapi.json',
|
|
810
|
+
|
|
811
|
+
// Where to write generated code
|
|
812
|
+
// Creates: output/generated/, output/overrides/, output/index.ts, output/http.ts
|
|
813
|
+
output: './src/api',
|
|
814
|
+
|
|
815
|
+
// Domain splitting rules (order matters - first match wins)
|
|
816
|
+
domains: [
|
|
817
|
+
{ name: 'auth', prefix: '/auth/', className: 'Auth' },
|
|
818
|
+
{ name: 'users', prefix: '/users/', className: 'Users' },
|
|
819
|
+
],
|
|
820
|
+
|
|
821
|
+
// Catch-all for unmatched paths
|
|
822
|
+
fallback: { name: 'api', prefix: '/', className: 'Api' },
|
|
823
|
+
})
|
|
824
|
+
`;
|
|
825
|
+
var HELP = `
|
|
826
|
+
openapi-domainify - Split OpenAPI specs into domain-based TypeScript services
|
|
827
|
+
|
|
828
|
+
USAGE
|
|
829
|
+
npx openapi-domainify <command> [options]
|
|
830
|
+
|
|
831
|
+
COMMANDS
|
|
832
|
+
init Create a domainify.config.ts file
|
|
833
|
+
generate Generate domain services from OpenAPI spec
|
|
834
|
+
help Show this help message
|
|
835
|
+
|
|
836
|
+
OPTIONS
|
|
837
|
+
-c, --config Path to config file (default: domainify.config.ts)
|
|
838
|
+
|
|
839
|
+
EXAMPLES
|
|
840
|
+
npx openapi-domainify init
|
|
841
|
+
npx openapi-domainify generate
|
|
842
|
+
npx openapi-domainify generate --config ./custom.config.ts
|
|
843
|
+
`;
|
|
844
|
+
async function main() {
|
|
845
|
+
const args = process.argv.slice(2);
|
|
846
|
+
const command = args[0];
|
|
847
|
+
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
848
|
+
console.log(HELP);
|
|
849
|
+
process.exit(0);
|
|
850
|
+
}
|
|
851
|
+
if (command === "init") {
|
|
852
|
+
const configPath = resolve3("domainify.config.ts");
|
|
853
|
+
if (existsSync3(configPath)) {
|
|
854
|
+
console.error("\u274C domainify.config.ts already exists");
|
|
855
|
+
process.exit(1);
|
|
856
|
+
}
|
|
857
|
+
writeFileSync2(configPath, INIT_CONFIG);
|
|
858
|
+
console.log("\u2705 Created domainify.config.ts");
|
|
859
|
+
console.log("\nNext steps:");
|
|
860
|
+
console.log(" 1. Edit domainify.config.ts with your OpenAPI spec URL");
|
|
861
|
+
console.log(" 2. Run: npx openapi-domainify generate");
|
|
862
|
+
process.exit(0);
|
|
863
|
+
}
|
|
864
|
+
if (command === "generate") {
|
|
865
|
+
let configPath;
|
|
866
|
+
const configIdx = args.findIndex((a) => a === "-c" || a === "--config");
|
|
867
|
+
if (configIdx !== -1 && args[configIdx + 1]) {
|
|
868
|
+
configPath = args[configIdx + 1];
|
|
869
|
+
}
|
|
870
|
+
try {
|
|
871
|
+
const config = await loadConfig(configPath);
|
|
872
|
+
await generate(config);
|
|
873
|
+
} catch (error) {
|
|
874
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
875
|
+
console.error(`
|
|
876
|
+
\u274C ${message}`);
|
|
877
|
+
process.exit(1);
|
|
878
|
+
}
|
|
879
|
+
process.exit(0);
|
|
880
|
+
}
|
|
881
|
+
console.error(`Unknown command: ${command}`);
|
|
882
|
+
console.log(HELP);
|
|
883
|
+
process.exit(1);
|
|
884
|
+
}
|
|
885
|
+
main();
|
|
886
|
+
//# sourceMappingURL=cli.js.map
|