@qraft/plugin 1.0.0-beta.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.
Files changed (78) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +1 -0
  3. package/dist/index.d.ts +5 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +3 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/lib/GeneratorFile.d.ts +9 -0
  8. package/dist/lib/GeneratorFile.d.ts.map +1 -0
  9. package/dist/lib/GeneratorFile.js +2 -0
  10. package/dist/lib/GeneratorFile.js.map +1 -0
  11. package/dist/lib/OutputOptions.d.ts +6 -0
  12. package/dist/lib/OutputOptions.d.ts.map +1 -0
  13. package/dist/lib/OutputOptions.js +2 -0
  14. package/dist/lib/OutputOptions.js.map +1 -0
  15. package/dist/lib/QraftCommand.d.ts +47 -0
  16. package/dist/lib/QraftCommand.d.ts.map +1 -0
  17. package/dist/lib/QraftCommand.js +187 -0
  18. package/dist/lib/QraftCommand.js.map +1 -0
  19. package/dist/lib/QraftCommandPlugin.d.ts +6 -0
  20. package/dist/lib/QraftCommandPlugin.d.ts.map +1 -0
  21. package/dist/lib/QraftCommandPlugin.js +2 -0
  22. package/dist/lib/QraftCommandPlugin.js.map +1 -0
  23. package/dist/lib/RedoclyConfigCommand.d.ts +28 -0
  24. package/dist/lib/RedoclyConfigCommand.d.ts.map +1 -0
  25. package/dist/lib/RedoclyConfigCommand.js +158 -0
  26. package/dist/lib/RedoclyConfigCommand.js.map +1 -0
  27. package/dist/lib/formatFileHeader.d.ts +2 -0
  28. package/dist/lib/formatFileHeader.d.ts.map +1 -0
  29. package/dist/lib/formatFileHeader.js +6 -0
  30. package/dist/lib/formatFileHeader.js.map +1 -0
  31. package/dist/lib/getRedocAPIsToQraft.d.ts +14 -0
  32. package/dist/lib/getRedocAPIsToQraft.d.ts.map +1 -0
  33. package/dist/lib/getRedocAPIsToQraft.js +49 -0
  34. package/dist/lib/getRedocAPIsToQraft.js.map +1 -0
  35. package/dist/lib/handleSchemaInput.d.ts +10 -0
  36. package/dist/lib/handleSchemaInput.d.ts.map +1 -0
  37. package/dist/lib/handleSchemaInput.js +40 -0
  38. package/dist/lib/handleSchemaInput.js.map +1 -0
  39. package/dist/lib/loadRedoclyConfig.d.ts +12 -0
  40. package/dist/lib/loadRedoclyConfig.d.ts.map +1 -0
  41. package/dist/lib/loadRedoclyConfig.js +17 -0
  42. package/dist/lib/loadRedoclyConfig.js.map +1 -0
  43. package/dist/lib/maybeEscapeShellArg.d.ts +2 -0
  44. package/dist/lib/maybeEscapeShellArg.d.ts.map +1 -0
  45. package/dist/lib/maybeEscapeShellArg.js +9 -0
  46. package/dist/lib/maybeEscapeShellArg.js.map +1 -0
  47. package/dist/lib/parseConfigToArgs.d.ts +7 -0
  48. package/dist/lib/parseConfigToArgs.d.ts.map +1 -0
  49. package/dist/lib/parseConfigToArgs.js +130 -0
  50. package/dist/lib/parseConfigToArgs.js.map +1 -0
  51. package/dist/lib/shouldQuoteCommandLineArg.d.ts +11 -0
  52. package/dist/lib/shouldQuoteCommandLineArg.d.ts.map +1 -0
  53. package/dist/lib/shouldQuoteCommandLineArg.js +33 -0
  54. package/dist/lib/shouldQuoteCommandLineArg.js.map +1 -0
  55. package/dist/lib/writeGeneratorFiles.d.ts +7 -0
  56. package/dist/lib/writeGeneratorFiles.d.ts.map +1 -0
  57. package/dist/lib/writeGeneratorFiles.js +51 -0
  58. package/dist/lib/writeGeneratorFiles.js.map +1 -0
  59. package/dist/packageVersion.d.ts +2 -0
  60. package/dist/packageVersion.d.ts.map +1 -0
  61. package/dist/packageVersion.js +3 -0
  62. package/dist/packageVersion.js.map +1 -0
  63. package/package.json +68 -0
  64. package/src/index.ts +12 -0
  65. package/src/lib/GeneratorFile.ts +5 -0
  66. package/src/lib/OutputOptions.ts +6 -0
  67. package/src/lib/QraftCommand.ts +381 -0
  68. package/src/lib/QraftCommandPlugin.ts +9 -0
  69. package/src/lib/RedoclyConfigCommand.ts +290 -0
  70. package/src/lib/formatFileHeader.ts +4 -0
  71. package/src/lib/getRedocAPIsToQraft.ts +92 -0
  72. package/src/lib/handleSchemaInput.ts +58 -0
  73. package/src/lib/loadRedoclyConfig.ts +47 -0
  74. package/src/lib/maybeEscapeShellArg.ts +10 -0
  75. package/src/lib/parseConfigToArgs.ts +134 -0
  76. package/src/lib/shouldQuoteCommandLineArg.ts +32 -0
  77. package/src/lib/writeGeneratorFiles.ts +82 -0
  78. package/src/packageVersion.ts +2 -0
@@ -0,0 +1,381 @@
1
+ import { sep } from 'node:path';
2
+ import process from 'node:process';
3
+ import { pathToFileURL, URL } from 'node:url';
4
+ import c from 'ansi-colors';
5
+ import { Command, Option, ParseOptions } from 'commander';
6
+ import ora, { Ora } from 'ora';
7
+ import { GeneratorFile } from './GeneratorFile.js';
8
+ import { OutputOptions } from './OutputOptions.js';
9
+ import { writeGeneratorFiles } from './writeGeneratorFiles.js';
10
+
11
+ export interface QraftCommandOptions {
12
+ defaultFileHeader?: string;
13
+ }
14
+
15
+ export class QraftCommand<
16
+ TActionOptions extends QraftCommandActionOptions = QraftCommandActionOptions,
17
+ > extends Command {
18
+ static spinner = ora();
19
+
20
+ protected readonly cwd: URL;
21
+ protected registeredPluginActions: QraftCommandActionCallback<TActionOptions>[] =
22
+ [];
23
+
24
+ constructor(name?: string, options?: QraftCommandOptions) {
25
+ super(name);
26
+ this.cwd = pathToFileURL(`${process.cwd()}/`);
27
+
28
+ this.option(
29
+ '-o, --output-dir <path>',
30
+ 'Output directory for generated files'
31
+ )
32
+ .addOption(
33
+ new Option(
34
+ '-c, --clean',
35
+ 'Clean output directory before generating files'
36
+ )
37
+ )
38
+ .addOption(
39
+ new Option(
40
+ '--file-header <string>',
41
+ 'Header to be added to the generated file (eg: /* eslint-disable */)'
42
+ ).default(options?.defaultFileHeader ?? '')
43
+ );
44
+ }
45
+
46
+ action(callback: QraftCommandActionCallback<TActionOptions>): this {
47
+ this.registerPluginAction(callback);
48
+ return super.action(this.actionCallback.bind(this));
49
+ }
50
+
51
+ async actionCallback(...actionArgs: any[]): Promise<void> {
52
+ const inputs = actionArgs.filter(
53
+ (arg) => typeof arg === 'string'
54
+ ) as string[];
55
+ const args = actionArgs.find(
56
+ (arg) => arg && typeof arg === 'object'
57
+ ) as Record<string, any>;
58
+
59
+ if (!args) throw new Error('Arguments object not found');
60
+
61
+ const spinner = QraftCommand.spinner;
62
+
63
+ spinner.start('Initializing process...');
64
+
65
+ const actionOptions = await this.prepareActionOptions(inputs, args);
66
+
67
+ spinner.text = 'Generating code...';
68
+
69
+ for (const pluginAction of this.registeredPluginActions) {
70
+ const fileItems = await new Promise<GeneratorFile[]>(
71
+ (resolve, reject) => {
72
+ pluginAction(actionOptions, resolve).catch(reject);
73
+ }
74
+ );
75
+
76
+ try {
77
+ if (this.registeredPluginActions.indexOf(pluginAction) === 0) {
78
+ await writeGeneratorFiles({
79
+ fileItems: [
80
+ { directory: actionOptions.output.dir, clean: false },
81
+ ...fileItems,
82
+ ],
83
+ spinner,
84
+ });
85
+ } else {
86
+ await writeGeneratorFiles({ fileItems, spinner });
87
+ }
88
+ } catch (error) {
89
+ spinner.fail(
90
+ c.red('An error occurred during the code generation process.')
91
+ );
92
+
93
+ if (error instanceof Error) {
94
+ console.error(c.red(error.message), c.red(error.stack ?? ''));
95
+ }
96
+
97
+ throw error;
98
+ }
99
+ }
100
+
101
+ spinner.succeed(c.green('Qraft process completed successfully'));
102
+ }
103
+
104
+ protected async prepareActionOptions(
105
+ inputs: string[],
106
+ args: Record<string, any>
107
+ ): Promise<TActionOptions> {
108
+ const outputDir = normalizeOutputDirPath(args.outputDir);
109
+
110
+ return {
111
+ inputs,
112
+ args,
113
+ spinner: QraftCommand.spinner,
114
+ output: {
115
+ dir: outputDir,
116
+ clean: args.clean,
117
+ },
118
+ } as TActionOptions;
119
+ }
120
+
121
+ protected registerPluginAction(
122
+ callback: QraftCommandActionCallback<TActionOptions>
123
+ ) {
124
+ this.registeredPluginActions.push(callback);
125
+ }
126
+
127
+ option(
128
+ flags: string,
129
+ description?: string,
130
+ defaultValue?: string | boolean | string[]
131
+ ): this;
132
+ option<T>(
133
+ flags: string,
134
+ description: string,
135
+ parseArg: (value: string, previous: T) => T,
136
+ defaultValue?: T
137
+ ): this;
138
+ /** @deprecated since v7, instead use choices or a custom function */
139
+ option(
140
+ flags: string,
141
+ description: string,
142
+ regexp: RegExp,
143
+ defaultValue?: string | boolean | string[]
144
+ ): this;
145
+ option<T>(
146
+ flags: string,
147
+ description?: string,
148
+ parseArg?:
149
+ | ((value: string, previous: T) => T)
150
+ | string
151
+ | boolean
152
+ | string[]
153
+ | RegExp,
154
+ defaultValue?: T
155
+ ): this {
156
+ if (
157
+ this.findSimilarOption({
158
+ flags,
159
+ mandatory: false,
160
+ })
161
+ ) {
162
+ return this;
163
+ }
164
+
165
+ return super.option(
166
+ flags,
167
+ // @ts-expect-error - Issues with overloading
168
+ description,
169
+ parseArg,
170
+ defaultValue
171
+ );
172
+ }
173
+
174
+ requiredOption(
175
+ flags: string,
176
+ description?: string,
177
+ defaultValue?: string | boolean | string[]
178
+ ): this;
179
+ requiredOption<T>(
180
+ flags: string,
181
+ description: string,
182
+ parseArg: (value: string, previous: T) => T,
183
+ defaultValue?: T
184
+ ): this;
185
+ /** @deprecated since v7, instead use choices or a custom function */
186
+ requiredOption(
187
+ flags: string,
188
+ description: string,
189
+ regexp: RegExp,
190
+ defaultValue?: string | boolean | string[]
191
+ ): this;
192
+ requiredOption<T>(
193
+ flags: string,
194
+ description?: string,
195
+ regexpOrDefaultValue?:
196
+ | string
197
+ | boolean
198
+ | string[]
199
+ | RegExp
200
+ | ((value: string, previous: T) => T),
201
+ defaultValue?: string | boolean | string[]
202
+ ): this {
203
+ if (
204
+ this.findSimilarOption({
205
+ flags,
206
+ mandatory: true,
207
+ })
208
+ ) {
209
+ return this;
210
+ }
211
+
212
+ return super.requiredOption(
213
+ flags,
214
+ // @ts-expect-error - Issues with overloading
215
+ description,
216
+ regexpOrDefaultValue,
217
+ defaultValue
218
+ );
219
+ }
220
+
221
+ addOption(option: Option): this {
222
+ if (
223
+ this.findSimilarOption({
224
+ flags: option.flags,
225
+ mandatory: option.mandatory,
226
+ })
227
+ ) {
228
+ return this;
229
+ }
230
+
231
+ return super.addOption(option);
232
+ }
233
+
234
+ parseAsync(argv?: readonly string[], options?: ParseOptions): Promise<this> {
235
+ if (options?.from !== 'user') this.logVersion();
236
+ return super.parseAsync(argv, options);
237
+ }
238
+
239
+ parse(argv?: readonly string[], options?: ParseOptions): this {
240
+ if (options?.from !== 'user') this.logVersion();
241
+ return super.parse(argv, options);
242
+ }
243
+
244
+ protected logVersion() {
245
+ QraftCommand.spinner.info(`✨ ${c.bold('Qraft')}`);
246
+ }
247
+
248
+ protected findSimilarOption(option: { flags: string; mandatory: boolean }) {
249
+ try {
250
+ return findSimilarOption(option, this.options);
251
+ } catch (error) {
252
+ console.error(
253
+ c.red(
254
+ error instanceof Error
255
+ ? error.message
256
+ : 'An error occurred during command option setup'
257
+ )
258
+ );
259
+
260
+ throw error;
261
+ }
262
+ }
263
+ }
264
+
265
+ export function normalizeOutputDirPath(outputDir: string): URL {
266
+ return pathToFileURL(
267
+ outputDir.endsWith(sep) ? outputDir : `${outputDir}${sep}`
268
+ );
269
+ }
270
+
271
+ export interface QraftCommandActionOptions {
272
+ inputs: string[];
273
+ args: Record<string, any>;
274
+ spinner: Ora;
275
+ output: OutputOptions;
276
+ }
277
+
278
+ export type QraftCommandActionCallback<
279
+ TActionOptions extends QraftCommandActionOptions = QraftCommandActionOptions,
280
+ > = (
281
+ options: TActionOptions,
282
+ resolve: (files: GeneratorFile[]) => void
283
+ ) => Promise<void>;
284
+
285
+ function findSimilarOption(
286
+ {
287
+ flags,
288
+ mandatory,
289
+ }: {
290
+ flags: string;
291
+ mandatory: boolean;
292
+ },
293
+ options: readonly Option[]
294
+ ) {
295
+ const newOptionParsedFlags = splitOptionFlags(flags);
296
+ const optional = flags.includes('[');
297
+ const required = flags.includes('<');
298
+ const variadic = /\w\.\.\.[>\]]$/.test(flags);
299
+
300
+ return options.find((existingOption) => {
301
+ const existingOptionParsedFlags = splitOptionFlags(existingOption.flags);
302
+
303
+ if (
304
+ !(
305
+ (existingOptionParsedFlags.longFlag !== undefined &&
306
+ existingOptionParsedFlags.longFlag ===
307
+ newOptionParsedFlags.longFlag) ||
308
+ (existingOptionParsedFlags.shortFlag !== undefined &&
309
+ existingOptionParsedFlags.shortFlag ===
310
+ newOptionParsedFlags.shortFlag)
311
+ )
312
+ ) {
313
+ return false;
314
+ }
315
+
316
+ if (
317
+ existingOptionParsedFlags.longFlag !== undefined &&
318
+ newOptionParsedFlags.longFlag !== undefined &&
319
+ existingOptionParsedFlags.longFlag !== newOptionParsedFlags.longFlag
320
+ ) {
321
+ throw new Error(
322
+ `Long flag ${flags} already exists in the option list with flags: "${existingOption.flags}" and description: "${existingOption.description}"`
323
+ );
324
+ }
325
+
326
+ if (
327
+ existingOptionParsedFlags.shortFlag !== undefined &&
328
+ newOptionParsedFlags.shortFlag !== undefined &&
329
+ existingOptionParsedFlags.shortFlag !== newOptionParsedFlags.shortFlag
330
+ ) {
331
+ throw new Error(
332
+ `Short flag ${flags} already exists in the option list with flags: "${existingOption.flags}" and description: "${existingOption.description}"`
333
+ );
334
+ }
335
+
336
+ if (required !== existingOption.required) {
337
+ throw new Error(
338
+ `Flag ${flags} already exists in the option list with flags: "${existingOption.flags}" and description: "${existingOption.description}" but with different required status`
339
+ );
340
+ }
341
+
342
+ if (optional !== existingOption.optional) {
343
+ throw new Error(
344
+ `Flag ${flags} already exists in the option list with flags: "${existingOption.flags}" and description: "${existingOption.description}" but with different optional status`
345
+ );
346
+ }
347
+
348
+ if (mandatory !== existingOption.mandatory) {
349
+ throw new Error(
350
+ `Flag ${flags} already exists in the option list with flags: "${existingOption.flags}" and description: "${existingOption.description}" but with different mandatory status`
351
+ );
352
+ }
353
+
354
+ if (variadic !== existingOption.variadic) {
355
+ throw new Error(
356
+ `Flag ${flags} already exists in the option list with flags: "${existingOption.flags}" and description: "${existingOption.description}" but with different variadic status`
357
+ );
358
+ }
359
+
360
+ return existingOption;
361
+ });
362
+ }
363
+
364
+ export function splitOptionFlags(flags: string) {
365
+ let shortFlag;
366
+ let longFlag;
367
+
368
+ const flagParts = flags.split(/[ |,]+/);
369
+
370
+ if (flagParts.length > 1 && !/^[[<]/.test(flagParts[1]))
371
+ shortFlag = flagParts.shift();
372
+
373
+ longFlag = flagParts.shift();
374
+
375
+ if (!shortFlag && /^-[^-]$/.test(longFlag ?? '')) {
376
+ shortFlag = longFlag;
377
+ longFlag = undefined;
378
+ }
379
+
380
+ return { shortFlag, longFlag };
381
+ }
@@ -0,0 +1,9 @@
1
+ import { QraftCommand, QraftCommandActionOptions } from './QraftCommand.js';
2
+
3
+ export interface QraftCommandPlugin<
4
+ TCommand extends QraftCommand<any> = QraftCommand<QraftCommandActionOptions>,
5
+ > {
6
+ setupCommand(command: TCommand): void | Promise<void>;
7
+
8
+ postSetupCommand?(command: TCommand, plugins: string[]): void | Promise<void>;
9
+ }
@@ -0,0 +1,290 @@
1
+ import { relative } from 'node:path';
2
+ import { fileURLToPath, pathToFileURL, URL } from 'node:url';
3
+ import { CONFIG_FILE_NAMES } from '@redocly/openapi-core';
4
+ import c from 'ansi-colors';
5
+ import { Command, CommanderError, Option, ParseOptions } from 'commander';
6
+ import {
7
+ ASYNCAPI_QRAFT_REDOC_CONFIG_KEY,
8
+ getRedocAPIsToQraft,
9
+ OPENAPI_QRAFT_REDOC_CONFIG_KEY,
10
+ } from './getRedocAPIsToQraft.js';
11
+ import { loadRedoclyConfig } from './loadRedoclyConfig.js';
12
+ import { maybeEscapeShellArg } from './maybeEscapeShellArg.js';
13
+ import { parseConfigToArgs } from './parseConfigToArgs.js';
14
+ import { QraftCommand } from './QraftCommand.js';
15
+
16
+ export { ASYNCAPI_QRAFT_REDOC_CONFIG_KEY, OPENAPI_QRAFT_REDOC_CONFIG_KEY };
17
+
18
+ export const redoclyOption = createRedoclyOption();
19
+
20
+ export function createRedoclyOption() {
21
+ const bin = c.gray.underline('bin');
22
+ const __redocly = c.yellow('--redocly');
23
+
24
+ const examples = [
25
+ `${bin} ${__redocly}`,
26
+ `${bin} ${c.green('my-api')} ${__redocly}`,
27
+ `${bin} ${c.green('my-api@v1 my-api@v2')} ${__redocly}`,
28
+ `${bin} ${__redocly} ${c.yellow.underline('./my-redocly-config.yaml')}`,
29
+ ].map((example) => `${c.italic.yellow(example)}`);
30
+
31
+ return new Option(
32
+ '--redocly [config]',
33
+ [
34
+ c.bold(`Use the Redocly configuration to generate multiple API clients`),
35
+ `If the [config] parameter is not specified, the default Redocly configuration will be used: [${CONFIG_FILE_NAMES.join(' | ')}].`,
36
+ `For more information about this option, use the command: ${c.yellow.italic('--redocly-help')}`,
37
+ `${c.underline('Examples:')}`,
38
+ ...examples.map((example) => `${c.gray('$')} ${example}`),
39
+ ].join('\n')
40
+ );
41
+ }
42
+
43
+ export type ConfigKeyCallbacks<T> = {
44
+ [configKey: string]: CallbackFn<T>;
45
+ };
46
+
47
+ export class RedoclyConfigCommand extends Command {
48
+ protected readonly cwd: URL;
49
+ protected configKeys: string[] = [];
50
+
51
+ /**
52
+ * Redocly API grouped by configKey
53
+ * { [configKey]: { [apiName]: argv[] } }
54
+ */
55
+ protected parsedAPIsByConfigKey: RedoclyQraftAPIsByConfigKey = {};
56
+
57
+ constructor(name?: string) {
58
+ super(name);
59
+ this.cwd = pathToFileURL(`${process.cwd()}/`);
60
+
61
+ this.usage(c.green('[apis...] --redocly <config>'))
62
+ .argument(
63
+ '[apis...]',
64
+ 'Optional list of Redocly APIs for which to generate API clients. If not specified, clients for all APIs will be generated.'
65
+ )
66
+ .allowUnknownOption(true)
67
+ .helpOption('--redocly-help', 'Display help for the `--redocly` option')
68
+ .addOption(redoclyOption)
69
+ .action(async (apis: string[] = [], args) => {
70
+ const redocly = args.redocly;
71
+
72
+ if (!redocly) return;
73
+
74
+ const spinner = QraftCommand.spinner;
75
+
76
+ spinner.start('Loading Redocly configuration ⚙︎');
77
+
78
+ const redoc = await loadRedoclyConfig(redocly || true, this.cwd);
79
+
80
+ const redocConfigFile = redoc.configFile;
81
+
82
+ if (!redocConfigFile) {
83
+ spinner.fail(
84
+ 'No "configFile" found in the Redocly configuration. Please specify the correct path to the Redocly configuration file.'
85
+ );
86
+ process.exit(1);
87
+ }
88
+
89
+ type RawApisMap = ReturnType<typeof getRedocAPIsToQraft>;
90
+ const allRawApisByConfigKey: Record<string, RawApisMap> = {};
91
+
92
+ for (const configKey of this.configKeys) {
93
+ const apisForKey = getRedocAPIsToQraft(
94
+ redoc,
95
+ this.cwd,
96
+ spinner,
97
+ configKey
98
+ );
99
+ if (Object.keys(apisForKey).length > 0) {
100
+ allRawApisByConfigKey[configKey] = apisForKey;
101
+ }
102
+ }
103
+
104
+ const allApiNames = new Set(
105
+ Object.values(allRawApisByConfigKey).flatMap((apis) =>
106
+ Object.keys(apis)
107
+ )
108
+ );
109
+
110
+ if (apis.length) {
111
+ const notFoundAPIs = apis.filter(
112
+ (inputAPIName) => !allApiNames.has(inputAPIName)
113
+ );
114
+
115
+ if (notFoundAPIs.length) {
116
+ spinner.fail(
117
+ `The specified API${
118
+ notFoundAPIs.length > 1 ? 's' : ''
119
+ } ${notFoundAPIs.map(c.red).join(', ')} ${notFoundAPIs.length > 1 ? 'were' : 'was'} not found in the Redocly configuration.`
120
+ );
121
+
122
+ throw new CommanderError(
123
+ 1,
124
+ 'ERR_API_NOT_FOUND',
125
+ `The specified API${
126
+ notFoundAPIs.length > 1 ? 's' : ''
127
+ } ${c.red(notFoundAPIs.join(', '))} ${notFoundAPIs.length > 1 ? 'were' : 'was'} not found in the Redocly configuration.`
128
+ );
129
+ }
130
+ }
131
+
132
+ for (const [configKey, apisForKey] of Object.entries(
133
+ allRawApisByConfigKey
134
+ )) {
135
+ const filteredEntries = Object.entries(apisForKey).filter(
136
+ ([apiName]) => !apis.length || apis.includes(apiName)
137
+ );
138
+
139
+ if (filteredEntries.length === 0) continue;
140
+
141
+ this.parsedAPIsByConfigKey[configKey] = {};
142
+
143
+ for (const [apiName, api] of filteredEntries) {
144
+ const globalQraftConfig =
145
+ configKey in redoc.rawConfig
146
+ ? (redoc.rawConfig as Record<string, unknown>)[configKey]
147
+ : undefined;
148
+
149
+ const apiQraftConfigWithOutput = (
150
+ api as unknown as Record<string, unknown>
151
+ )[configKey] as { ['output-dir']: string };
152
+ const { ['output-dir']: outputDir, ...apiQraftConfig } =
153
+ apiQraftConfigWithOutput;
154
+
155
+ const cwd = fileURLToPath(this.cwd);
156
+
157
+ this.parsedAPIsByConfigKey[configKey][apiName] = [
158
+ apiName,
159
+ ...parseConfigToArgs({
160
+ redocly: relative(cwd, redocConfigFile),
161
+ 'output-dir': relative(cwd, outputDir),
162
+ ...(globalQraftConfig && typeof globalQraftConfig === 'object'
163
+ ? globalQraftConfig
164
+ : undefined),
165
+ ...apiQraftConfig,
166
+ }),
167
+ ];
168
+ }
169
+ }
170
+
171
+ spinner.stop();
172
+ });
173
+ }
174
+
175
+ async parseConfig<T>(
176
+ callbacks: ConfigKeyCallbacks<T>,
177
+ argv: readonly string[],
178
+ options?: ParseOptions
179
+ ): Promise<T[] | undefined> {
180
+ this.configKeys = Object.keys(callbacks);
181
+
182
+ await this.parseAsync(argv, options);
183
+
184
+ const hasAnyAPIs = Object.values(this.parsedAPIsByConfigKey).some(
185
+ (apis) => Object.keys(apis).length > 0
186
+ );
187
+
188
+ if (!hasAnyAPIs) return; // todo::improve error reporting
189
+
190
+ return RedoclyConfigCommand.forEachRedoclyAPIEntry(
191
+ this.parsedAPIsByConfigKey,
192
+ callbacks
193
+ );
194
+ }
195
+
196
+ static async forEachRedoclyAPIEntry<T>(
197
+ apisByConfigKey: RedoclyQraftAPIsByConfigKey,
198
+ callbacks: ConfigKeyCallbacks<T>
199
+ ) {
200
+ const spinner = QraftCommand.spinner;
201
+
202
+ spinner.info('Loading Redocly configuration...');
203
+
204
+ const allResults: T[] = [];
205
+ const allErrors: unknown[] = [];
206
+
207
+ for (const [configKey, apis] of Object.entries(apisByConfigKey)) {
208
+ const callback = callbacks[configKey];
209
+ if (!callback) continue;
210
+
211
+ const apiEntries = Object.entries(apis);
212
+
213
+ const { errors, results } = await Promise.allSettled(
214
+ apiEntries.map(
215
+ async ([apiName, [openAPIDocument, ...apiProcessArgv]]) => {
216
+ spinner.info(
217
+ `Generating API client for ${c.magenta(apiName)} with the following parameters:\n` +
218
+ c.gray.italic('bin ') +
219
+ `${c.green.italic(openAPIDocument)} ` +
220
+ apiProcessArgv
221
+ .map((arg) =>
222
+ arg.startsWith('--')
223
+ ? c.yellow.italic(arg)
224
+ : c.yellow.italic.underline(maybeEscapeShellArg(arg))
225
+ )
226
+ .join(' ')
227
+ );
228
+ spinner.text = '';
229
+ spinner.start();
230
+
231
+ return await callback([openAPIDocument, ...apiProcessArgv], {
232
+ from: 'user',
233
+ });
234
+ }
235
+ )
236
+ ).then((results) =>
237
+ results.reduce<{ errors: unknown[]; results: T[] }>(
238
+ (acc, result) => {
239
+ if (result.status === 'rejected') acc.errors.push(result.reason);
240
+ else acc.results.push(result.value);
241
+
242
+ return acc;
243
+ },
244
+ { errors: [], results: [] }
245
+ )
246
+ );
247
+
248
+ allResults.push(...results);
249
+ allErrors.push(...errors);
250
+ }
251
+
252
+ if (allErrors.length) {
253
+ throw new CommanderError(
254
+ 1,
255
+ 'ERROR',
256
+ 'The following errors occurred during API client generation:\n' +
257
+ allErrors
258
+ .map((error) =>
259
+ error instanceof Error
260
+ ? error.message + '\n' + error.stack
261
+ : JSON.stringify(error, null, 2)
262
+ )
263
+ .join('\n\n')
264
+ );
265
+ }
266
+
267
+ const allApiNames = Object.values(apisByConfigKey).flatMap((apis) =>
268
+ Object.keys(apis)
269
+ );
270
+
271
+ spinner.succeed(
272
+ c.green(`API clients successfully generated for: `) +
273
+ allApiNames.map((apiName) => `"${c.magenta(apiName)}"`).join(', ') +
274
+ '.'
275
+ );
276
+
277
+ return allResults;
278
+ }
279
+ }
280
+
281
+ type CallbackFn<T> = (
282
+ processArgv: string[],
283
+ processArgvParseOptions?: ParseOptions
284
+ ) => T | Promise<T>;
285
+
286
+ type RedoclyQraftAPIsByConfigKey = {
287
+ [configKey: string]: {
288
+ [apiName: string]: string[];
289
+ };
290
+ };
@@ -0,0 +1,4 @@
1
+ export const formatFileHeader = (fileHeader: string | undefined) => {
2
+ if (!fileHeader) return '';
3
+ return `${fileHeader}${fileHeader.endsWith('\n') ? '' : '\n'}`;
4
+ };