@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.
- package/CHANGELOG.md +15 -0
- package/README.md +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/GeneratorFile.d.ts +9 -0
- package/dist/lib/GeneratorFile.d.ts.map +1 -0
- package/dist/lib/GeneratorFile.js +2 -0
- package/dist/lib/GeneratorFile.js.map +1 -0
- package/dist/lib/OutputOptions.d.ts +6 -0
- package/dist/lib/OutputOptions.d.ts.map +1 -0
- package/dist/lib/OutputOptions.js +2 -0
- package/dist/lib/OutputOptions.js.map +1 -0
- package/dist/lib/QraftCommand.d.ts +47 -0
- package/dist/lib/QraftCommand.d.ts.map +1 -0
- package/dist/lib/QraftCommand.js +187 -0
- package/dist/lib/QraftCommand.js.map +1 -0
- package/dist/lib/QraftCommandPlugin.d.ts +6 -0
- package/dist/lib/QraftCommandPlugin.d.ts.map +1 -0
- package/dist/lib/QraftCommandPlugin.js +2 -0
- package/dist/lib/QraftCommandPlugin.js.map +1 -0
- package/dist/lib/RedoclyConfigCommand.d.ts +28 -0
- package/dist/lib/RedoclyConfigCommand.d.ts.map +1 -0
- package/dist/lib/RedoclyConfigCommand.js +158 -0
- package/dist/lib/RedoclyConfigCommand.js.map +1 -0
- package/dist/lib/formatFileHeader.d.ts +2 -0
- package/dist/lib/formatFileHeader.d.ts.map +1 -0
- package/dist/lib/formatFileHeader.js +6 -0
- package/dist/lib/formatFileHeader.js.map +1 -0
- package/dist/lib/getRedocAPIsToQraft.d.ts +14 -0
- package/dist/lib/getRedocAPIsToQraft.d.ts.map +1 -0
- package/dist/lib/getRedocAPIsToQraft.js +49 -0
- package/dist/lib/getRedocAPIsToQraft.js.map +1 -0
- package/dist/lib/handleSchemaInput.d.ts +10 -0
- package/dist/lib/handleSchemaInput.d.ts.map +1 -0
- package/dist/lib/handleSchemaInput.js +40 -0
- package/dist/lib/handleSchemaInput.js.map +1 -0
- package/dist/lib/loadRedoclyConfig.d.ts +12 -0
- package/dist/lib/loadRedoclyConfig.d.ts.map +1 -0
- package/dist/lib/loadRedoclyConfig.js +17 -0
- package/dist/lib/loadRedoclyConfig.js.map +1 -0
- package/dist/lib/maybeEscapeShellArg.d.ts +2 -0
- package/dist/lib/maybeEscapeShellArg.d.ts.map +1 -0
- package/dist/lib/maybeEscapeShellArg.js +9 -0
- package/dist/lib/maybeEscapeShellArg.js.map +1 -0
- package/dist/lib/parseConfigToArgs.d.ts +7 -0
- package/dist/lib/parseConfigToArgs.d.ts.map +1 -0
- package/dist/lib/parseConfigToArgs.js +130 -0
- package/dist/lib/parseConfigToArgs.js.map +1 -0
- package/dist/lib/shouldQuoteCommandLineArg.d.ts +11 -0
- package/dist/lib/shouldQuoteCommandLineArg.d.ts.map +1 -0
- package/dist/lib/shouldQuoteCommandLineArg.js +33 -0
- package/dist/lib/shouldQuoteCommandLineArg.js.map +1 -0
- package/dist/lib/writeGeneratorFiles.d.ts +7 -0
- package/dist/lib/writeGeneratorFiles.d.ts.map +1 -0
- package/dist/lib/writeGeneratorFiles.js +51 -0
- package/dist/lib/writeGeneratorFiles.js.map +1 -0
- package/dist/packageVersion.d.ts +2 -0
- package/dist/packageVersion.d.ts.map +1 -0
- package/dist/packageVersion.js +3 -0
- package/dist/packageVersion.js.map +1 -0
- package/package.json +68 -0
- package/src/index.ts +12 -0
- package/src/lib/GeneratorFile.ts +5 -0
- package/src/lib/OutputOptions.ts +6 -0
- package/src/lib/QraftCommand.ts +381 -0
- package/src/lib/QraftCommandPlugin.ts +9 -0
- package/src/lib/RedoclyConfigCommand.ts +290 -0
- package/src/lib/formatFileHeader.ts +4 -0
- package/src/lib/getRedocAPIsToQraft.ts +92 -0
- package/src/lib/handleSchemaInput.ts +58 -0
- package/src/lib/loadRedoclyConfig.ts +47 -0
- package/src/lib/maybeEscapeShellArg.ts +10 -0
- package/src/lib/parseConfigToArgs.ts +134 -0
- package/src/lib/shouldQuoteCommandLineArg.ts +32 -0
- package/src/lib/writeGeneratorFiles.ts +82 -0
- 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
|
+
};
|