@ygorazambuja/sauron 1.0.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.
@@ -0,0 +1,276 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import {
4
+ createModelsWithOperationTypes,
5
+ fetchJsonFromUrl,
6
+ readJsonFile,
7
+ verifySwaggerComposition,
8
+ } from "../utils";
9
+ import { runHttpPlugins } from "../plugins/runner";
10
+ import { parseArgs, parseCommand, showHelp } from "./args";
11
+ import {
12
+ createGeneratedFileHeader,
13
+ formatGeneratedFile,
14
+ initConfigFile,
15
+ loadSauronConfig,
16
+ mergeOptionsWithConfig,
17
+ } from "./config";
18
+ import { isAngularProject, resolveOutputBasePath } from "./project";
19
+ import { DEFAULT_CONFIG_FILE, type CliOptions } from "./types";
20
+
21
+ /**
22
+ * Main.
23
+ * @example
24
+ * ```ts
25
+ * main();
26
+ * ```
27
+ */
28
+ export async function main() {
29
+ const command = parseCommand();
30
+ const cliOptions = parseArgs();
31
+
32
+ if (cliOptions.help) {
33
+ showHelp();
34
+ return;
35
+ }
36
+
37
+ if (command === "init") {
38
+ await initConfigFile(cliOptions.config || DEFAULT_CONFIG_FILE);
39
+ return;
40
+ }
41
+
42
+ let options = cliOptions;
43
+
44
+ try {
45
+ const loadedConfig = await loadSauronConfig(
46
+ options.config || DEFAULT_CONFIG_FILE,
47
+ );
48
+ if (loadedConfig) {
49
+ options = mergeOptionsWithConfig(cliOptions, loadedConfig);
50
+ console.log(
51
+ `āš™ļø Using config file: ${options.config || DEFAULT_CONFIG_FILE}`,
52
+ );
53
+ }
54
+
55
+ let config: unknown;
56
+ if (options.url) {
57
+ console.log(`šŸ“– Downloading OpenAPI spec from: ${options.url}`);
58
+ config = await fetchJsonFromUrl(options.url);
59
+ }
60
+ if (!options.url) {
61
+ console.log(`šŸ“– Reading OpenAPI spec from: ${options.input}`);
62
+ config = await readJsonFile(options.input);
63
+ }
64
+
65
+ if (typeof config !== "object") {
66
+ throw new Error("Config is not an object");
67
+ }
68
+
69
+ console.log("āœ… Validating OpenAPI schema...");
70
+ const schema = verifySwaggerComposition(config as Record<string, unknown>);
71
+
72
+ const requestedPluginIds = resolveEffectivePluginIds(options);
73
+ logPluginCompatibilityNotice(options, requestedPluginIds);
74
+
75
+ const angularDetected = isAngularProject();
76
+ const preferAngularOutput = requestedPluginIds.includes("angular");
77
+ const baseOutputPath = resolveOutputBasePath(options, preferAngularOutput);
78
+ const modelsPath = join(baseOutputPath, "models", "index.ts");
79
+ mkdirSync(dirname(modelsPath), { recursive: true });
80
+
81
+ const fileHeader = createGeneratedFileHeader(schema);
82
+
83
+ console.log("šŸ”§ Generating TypeScript models...");
84
+ const { models, operationTypes, typeNameMap } =
85
+ createModelsWithOperationTypes(schema);
86
+ const formattedModels = await formatGeneratedFile(
87
+ `${fileHeader}\n${models.join("\n")}`,
88
+ modelsPath,
89
+ );
90
+ writeFileSync(modelsPath, formattedModels);
91
+
92
+ const pluginResults = await runHttpPlugins(requestedPluginIds, {
93
+ schema,
94
+ options,
95
+ baseOutputPath,
96
+ modelsPath,
97
+ fileHeader,
98
+ operationTypes,
99
+ typeNameMap,
100
+ isAngularProject: angularDetected,
101
+ writeFormattedFile: async (filePath: string, content: string) => {
102
+ const formattedContent = await formatGeneratedFile(content, filePath);
103
+ writeFileSync(filePath, formattedContent);
104
+ },
105
+ });
106
+
107
+ logPluginReports(pluginResults);
108
+
109
+ console.log("\nāœ… Generation complete!");
110
+ console.log(`šŸ“„ Models: ${models.length} TypeScript interfaces/types`);
111
+ logPluginMethodSummary(pluginResults);
112
+ console.log(`šŸ“ Output: ${resolveOutputDisplayPath(options, angularDetected, preferAngularOutput)}`);
113
+ } catch (error) {
114
+ console.error("āŒ Error:", error);
115
+ process.exit(1);
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Resolve effective plugin IDs.
121
+ * @param options Input parameter `options`.
122
+ * @returns Resolve effective plugin IDs output as `string[]`.
123
+ * @example
124
+ * ```ts
125
+ * const result = resolveEffectivePluginIds({
126
+ * input: "swagger.json",
127
+ * angular: false,
128
+ * http: true,
129
+ * help: false,
130
+ * });
131
+ * // result: string[]
132
+ * ```
133
+ */
134
+ function resolveEffectivePluginIds(options: CliOptions): string[] {
135
+ if (options.plugin && options.plugin.length > 0) {
136
+ return normalizePluginIds(options.plugin);
137
+ }
138
+
139
+ if (!options.http) {
140
+ return [];
141
+ }
142
+
143
+ if (options.angular) {
144
+ return ["angular"];
145
+ }
146
+
147
+ return ["fetch"];
148
+ }
149
+
150
+ /**
151
+ * Normalize plugin IDs.
152
+ * @param pluginIds Input parameter `pluginIds`.
153
+ * @returns Normalize plugin IDs output as `string[]`.
154
+ * @example
155
+ * ```ts
156
+ * const result = normalizePluginIds(["fetch", "Angular"]);
157
+ * // result: string[]
158
+ * ```
159
+ */
160
+ function normalizePluginIds(pluginIds: string[]): string[] {
161
+ const normalizedIds: string[] = [];
162
+ const usedIds = new Set<string>();
163
+
164
+ for (const pluginId of pluginIds) {
165
+ const normalizedId = pluginId.trim().toLowerCase();
166
+ if (!normalizedId) {
167
+ continue;
168
+ }
169
+ if (usedIds.has(normalizedId)) {
170
+ continue;
171
+ }
172
+ usedIds.add(normalizedId);
173
+ normalizedIds.push(normalizedId);
174
+ }
175
+
176
+ return normalizedIds;
177
+ }
178
+
179
+ /**
180
+ * Log plugin compatibility notice.
181
+ * @param options Input parameter `options`.
182
+ * @param effectivePluginIds Input parameter `effectivePluginIds`.
183
+ * @example
184
+ * ```ts
185
+ * logPluginCompatibilityNotice(
186
+ * { input: "swagger.json", angular: true, http: true, help: false, plugin: ["fetch"] },
187
+ * ["fetch"],
188
+ * );
189
+ * ```
190
+ */
191
+ function logPluginCompatibilityNotice(
192
+ options: CliOptions,
193
+ effectivePluginIds: string[],
194
+ ): void {
195
+ if (!options.plugin || options.plugin.length === 0) {
196
+ return;
197
+ }
198
+ if (!options.http && !options.angular) {
199
+ return;
200
+ }
201
+ if (effectivePluginIds.length === 0) {
202
+ return;
203
+ }
204
+
205
+ console.log(
206
+ "ā„¹ļø --plugin provided. Ignoring compatibility aliases --http/--angular.",
207
+ );
208
+ }
209
+
210
+ /**
211
+ * Log plugin reports.
212
+ * @param pluginResults Input parameter `pluginResults`.
213
+ * @example
214
+ * ```ts
215
+ * logPluginReports([]);
216
+ * ```
217
+ */
218
+ function logPluginReports(
219
+ pluginResults: Array<{ reportPath: string; typeCoverageReportPath?: string }>,
220
+ ): void {
221
+ for (const result of pluginResults) {
222
+ console.log(`🧾 Missing Swagger definitions report: ${result.reportPath}`);
223
+ if (!result.typeCoverageReportPath) {
224
+ continue;
225
+ }
226
+ console.log(`šŸ“Š Type coverage report: ${result.typeCoverageReportPath}`);
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Log plugin method summary.
232
+ * @param pluginResults Input parameter `pluginResults`.
233
+ * @example
234
+ * ```ts
235
+ * logPluginMethodSummary([]);
236
+ * ```
237
+ */
238
+ function logPluginMethodSummary(
239
+ pluginResults: Array<{ executedPluginId: string; methodCount: number }>,
240
+ ): void {
241
+ for (const result of pluginResults) {
242
+ console.log(
243
+ `šŸ”— HTTP Methods (${result.executedPluginId}): ${result.methodCount} methods`,
244
+ );
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Resolve output display path.
250
+ * @param options Input parameter `options`.
251
+ * @param angularDetected Input parameter `angularDetected`.
252
+ * @param preferAngularOutput Input parameter `preferAngularOutput`.
253
+ * @returns Resolve output display path output as `string`.
254
+ * @example
255
+ * ```ts
256
+ * const result = resolveOutputDisplayPath(
257
+ * { input: "swagger.json", angular: false, http: false, help: false },
258
+ * false,
259
+ * false,
260
+ * );
261
+ * // result: string
262
+ * ```
263
+ */
264
+ function resolveOutputDisplayPath(
265
+ options: CliOptions,
266
+ angularDetected: boolean,
267
+ preferAngularOutput: boolean,
268
+ ): string {
269
+ if (options.output) {
270
+ return options.output;
271
+ }
272
+ if (preferAngularOutput && angularDetected) {
273
+ return "src/app/sauron";
274
+ }
275
+ return "outputs";
276
+ }
@@ -0,0 +1,113 @@
1
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import type { CliOptions } from "./types";
4
+
5
+ /**
6
+ * Is angular project.
7
+ * @returns Is angular project output as `boolean`.
8
+ * @example
9
+ * ```ts
10
+ * const result = isAngularProject();
11
+ * // result: boolean
12
+ * ```
13
+ */
14
+ export function isAngularProject(): boolean {
15
+ if (existsSync("angular.json")) {
16
+ return true;
17
+ }
18
+
19
+ try {
20
+ const packageJsonPath = join(process.cwd(), "package.json");
21
+ if (existsSync(packageJsonPath)) {
22
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
23
+ const deps = {
24
+ ...packageJson.dependencies,
25
+ ...packageJson.devDependencies,
26
+ };
27
+ return "@angular/core" in deps;
28
+ }
29
+ } catch (_error) {
30
+ // Ignore errors when checking package.json
31
+ }
32
+
33
+ return false;
34
+ }
35
+
36
+ /**
37
+ * Get output paths.
38
+ * @param options Input parameter `options`.
39
+ * @returns Get output paths output as `unknown`.
40
+ * @example
41
+ * ```ts
42
+ * const result = getOutputPaths({});
43
+ * // result: unknown
44
+ * ```
45
+ */
46
+ export function getOutputPaths(options: CliOptions): {
47
+ modelsPath: string;
48
+ servicePath: string | undefined;
49
+ } {
50
+ const basePath = resolveOutputBasePath(options, options.angular);
51
+
52
+ mkdirSync(join(basePath, "models"), { recursive: true });
53
+
54
+ let servicePath: string;
55
+ if (options.http) {
56
+ const serviceDir =
57
+ options.angular && isAngularProject()
58
+ ? "angular-http-client"
59
+ : "http-client";
60
+ mkdirSync(join(basePath, serviceDir), { recursive: true });
61
+
62
+ const serviceFileName =
63
+ options.angular && isAngularProject()
64
+ ? "sauron-api.service.ts"
65
+ : "sauron-api.client.ts";
66
+
67
+ servicePath = join(basePath, serviceDir, serviceFileName);
68
+ } else {
69
+ servicePath = "";
70
+ }
71
+
72
+ return {
73
+ modelsPath: join(basePath, "models", "index.ts"),
74
+ servicePath,
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Resolve output base path.
80
+ * @param options Input parameter `options`.
81
+ * @param preferAngularOutput Input parameter `preferAngularOutput`.
82
+ * @returns Resolve output base path output as `string`.
83
+ * @example
84
+ * ```ts
85
+ * const result = resolveOutputBasePath(
86
+ * { input: "swagger.json", angular: false, http: false, help: false },
87
+ * false,
88
+ * );
89
+ * // result: string
90
+ * ```
91
+ */
92
+ export function resolveOutputBasePath(
93
+ options: CliOptions,
94
+ preferAngularOutput: boolean,
95
+ ): string {
96
+ if (options.output) {
97
+ return resolve(options.output);
98
+ }
99
+
100
+ const angularDetected = isAngularProject();
101
+ if (preferAngularOutput && angularDetected) {
102
+ console.log("āœ… Angular project detected! Generating in src/app/sauron/");
103
+ return "src/app/sauron";
104
+ }
105
+
106
+ if (options.angular && !angularDetected) {
107
+ console.warn(
108
+ "āš ļø --angular flag used but Angular project not detected. Generating in outputs/ instead.",
109
+ );
110
+ }
111
+
112
+ return "outputs";
113
+ }
@@ -0,0 +1,22 @@
1
+ export interface CliOptions {
2
+ input: string;
3
+ url?: string;
4
+ angular: boolean;
5
+ http: boolean;
6
+ plugin?: string[];
7
+ output?: string;
8
+ config?: string;
9
+ help: boolean;
10
+ }
11
+
12
+ export interface SauronConfig {
13
+ input?: string;
14
+ url?: string;
15
+ angular?: boolean;
16
+ http?: boolean;
17
+ plugin?: string[];
18
+ output?: string;
19
+ }
20
+
21
+ export const DEFAULT_CONFIG_FILE = "sauron.config.ts";
22
+ export const DEFAULT_SAURON_VERSION = "1.0.0";
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Generate angular service.
3
+ * @param methods Input parameter `methods`.
4
+ * @param imports Input parameter `imports`.
5
+ * @param _isAngularProject Input parameter `_isAngularProject`.
6
+ * @param paramsInterfaces Input parameter `paramsInterfaces`.
7
+ * @returns Generate angular service output as `string`.
8
+ * @example
9
+ * ```ts
10
+ * const result = generateAngularService([], [], true, []);
11
+ * // result: string
12
+ * ```
13
+ */
14
+ export function generateAngularService(
15
+ methods: string[],
16
+ imports: string[],
17
+ _isAngularProject: boolean,
18
+ paramsInterfaces: string[] = [],
19
+ ): string {
20
+ const importStatement = buildModelImportStatement(imports);
21
+ const interfacesBlock = buildInterfacesBlock(paramsInterfaces);
22
+ const methodsBlock = methods.join("\n\n");
23
+
24
+ const serviceTemplate = `import { Injectable, inject } from "@angular/core";
25
+ import { HttpClient } from "@angular/common/http";
26
+ import { Observable } from "rxjs";
27
+
28
+ ${importStatement}\n${interfacesBlock}\n\n
29
+ @Injectable({
30
+ providedIn: "root"
31
+ })
32
+ export class SauronApiService {
33
+ private readonly httpClient = inject(HttpClient);
34
+
35
+ ${methodsBlock}
36
+ }
37
+ `;
38
+
39
+ return serviceTemplate;
40
+ }
41
+
42
+ /**
43
+ * Build model import statement.
44
+ * @param imports Input parameter `imports`.
45
+ * @returns Build model import statement output as `string`.
46
+ * @example
47
+ * ```ts
48
+ * const result = buildModelImportStatement([]);
49
+ * // result: string
50
+ * ```
51
+ */
52
+ function buildModelImportStatement(imports: string[]): string {
53
+ if (imports.length === 0) {
54
+ return "";
55
+ }
56
+ const importList = imports.join(", ");
57
+ const importPath = "../models";
58
+ return `import { ${importList} } from "${importPath}";\n`;
59
+ }
60
+
61
+ /**
62
+ * Build interfaces block.
63
+ * @param paramsInterfaces Input parameter `paramsInterfaces`.
64
+ * @returns Build interfaces block output as `string`.
65
+ * @example
66
+ * ```ts
67
+ * const result = buildInterfacesBlock([]);
68
+ * // result: string
69
+ * ```
70
+ */
71
+ function buildInterfacesBlock(paramsInterfaces: string[]): string {
72
+ if (paramsInterfaces.length === 0) {
73
+ return "";
74
+ }
75
+ return `${paramsInterfaces.join("\n\n")}\n\n`;
76
+ }