@redocly/cli 1.0.0-beta.96

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 (79) hide show
  1. package/README.md +39 -0
  2. package/bin/cli.js +3 -0
  3. package/lib/__mocks__/utils.d.ts +17 -0
  4. package/lib/__mocks__/utils.js +14 -0
  5. package/lib/__tests__/commands/bundle.test.d.ts +1 -0
  6. package/lib/__tests__/commands/bundle.test.js +92 -0
  7. package/lib/__tests__/commands/push-region.test.d.ts +1 -0
  8. package/lib/__tests__/commands/push-region.test.js +55 -0
  9. package/lib/__tests__/commands/push.test.d.ts +1 -0
  10. package/lib/__tests__/commands/push.test.js +153 -0
  11. package/lib/__tests__/utils.test.d.ts +1 -0
  12. package/lib/__tests__/utils.test.js +41 -0
  13. package/lib/assert-node-version.d.ts +1 -0
  14. package/lib/assert-node-version.js +10 -0
  15. package/lib/commands/bundle.d.ts +19 -0
  16. package/lib/commands/bundle.js +128 -0
  17. package/lib/commands/join.d.ts +7 -0
  18. package/lib/commands/join.js +421 -0
  19. package/lib/commands/lint.d.ts +11 -0
  20. package/lib/commands/lint.js +80 -0
  21. package/lib/commands/login.d.ts +6 -0
  22. package/lib/commands/login.js +28 -0
  23. package/lib/commands/preview-docs/index.d.ts +12 -0
  24. package/lib/commands/preview-docs/index.js +141 -0
  25. package/lib/commands/preview-docs/preview-server/default.hbs +24 -0
  26. package/lib/commands/preview-docs/preview-server/hot.js +42 -0
  27. package/lib/commands/preview-docs/preview-server/oauth2-redirect.html +21 -0
  28. package/lib/commands/preview-docs/preview-server/preview-server.d.ts +5 -0
  29. package/lib/commands/preview-docs/preview-server/preview-server.js +120 -0
  30. package/lib/commands/preview-docs/preview-server/server.d.ts +23 -0
  31. package/lib/commands/preview-docs/preview-server/server.js +85 -0
  32. package/lib/commands/push.d.ts +25 -0
  33. package/lib/commands/push.js +247 -0
  34. package/lib/commands/split/__tests__/index.test.d.ts +1 -0
  35. package/lib/commands/split/__tests__/index.test.js +70 -0
  36. package/lib/commands/split/index.d.ts +8 -0
  37. package/lib/commands/split/index.js +279 -0
  38. package/lib/commands/split/types.d.ts +37 -0
  39. package/lib/commands/split/types.js +52 -0
  40. package/lib/commands/stats.d.ts +5 -0
  41. package/lib/commands/stats.js +92 -0
  42. package/lib/index.d.ts +2 -0
  43. package/lib/index.js +269 -0
  44. package/lib/js-utils.d.ts +3 -0
  45. package/lib/js-utils.js +16 -0
  46. package/lib/types.d.ts +13 -0
  47. package/lib/types.js +5 -0
  48. package/lib/utils.d.ts +28 -0
  49. package/lib/utils.js +260 -0
  50. package/package.json +54 -0
  51. package/src/__mocks__/utils.ts +11 -0
  52. package/src/__tests__/commands/bundle.test.ts +120 -0
  53. package/src/__tests__/commands/push-region.test.ts +51 -0
  54. package/src/__tests__/commands/push.test.ts +156 -0
  55. package/src/__tests__/utils.test.ts +50 -0
  56. package/src/assert-node-version.ts +8 -0
  57. package/src/commands/bundle.ts +178 -0
  58. package/src/commands/join.ts +488 -0
  59. package/src/commands/lint.ts +110 -0
  60. package/src/commands/login.ts +19 -0
  61. package/src/commands/preview-docs/index.ts +188 -0
  62. package/src/commands/preview-docs/preview-server/default.hbs +24 -0
  63. package/src/commands/preview-docs/preview-server/hot.js +42 -0
  64. package/src/commands/preview-docs/preview-server/oauth2-redirect.html +21 -0
  65. package/src/commands/preview-docs/preview-server/preview-server.ts +150 -0
  66. package/src/commands/preview-docs/preview-server/server.ts +91 -0
  67. package/src/commands/push.ts +355 -0
  68. package/src/commands/split/__tests__/fixtures/spec.json +70 -0
  69. package/src/commands/split/__tests__/fixtures/webhooks.json +88 -0
  70. package/src/commands/split/__tests__/index.test.ts +96 -0
  71. package/src/commands/split/index.ts +349 -0
  72. package/src/commands/split/types.ts +73 -0
  73. package/src/commands/stats.ts +115 -0
  74. package/src/index.ts +311 -0
  75. package/src/js-utils.ts +12 -0
  76. package/src/types.ts +13 -0
  77. package/src/utils.ts +300 -0
  78. package/tsconfig.json +9 -0
  79. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,488 @@
1
+ import * as path from 'path';
2
+ import { red, blue, yellow, green } from 'colorette';
3
+ import { performance } from 'perf_hooks';
4
+ const isEqual = require('lodash.isequal');
5
+ import {
6
+ Config,
7
+ Oas3Definition,
8
+ OasVersion,
9
+ BaseResolver,
10
+ Document,
11
+ LintConfig,
12
+ Oas3Tag,
13
+ loadConfig,
14
+ formatProblems,
15
+ getTotals,
16
+ lintDocument,
17
+ detectOpenAPI,
18
+ bundleDocument,
19
+ } from '@redocly/openapi-core';
20
+
21
+ import {
22
+ getFallbackEntryPointsOrExit,
23
+ printExecutionTime,
24
+ handleError,
25
+ printLintTotals,
26
+ writeYaml,
27
+ exitWithError
28
+ } from '../utils';
29
+ import { isObject, isString } from '../js-utils';
30
+
31
+ const COMPONENTS = 'components';
32
+ let potentialConflictsTotal = 0;
33
+
34
+ type JoinDocumentContext = {
35
+ entrypoint: string,
36
+ entrypointFilename: string,
37
+ tags: Oas3Tag[],
38
+ potentialConflicts: any,
39
+ tagsPrefix: string,
40
+ componentsPrefix: string | undefined
41
+ }
42
+
43
+ export async function handleJoin (argv: {
44
+ entrypoints: string[],
45
+ lint?: boolean,
46
+ 'prefix-tags-with-info-prop'?: string,
47
+ 'prefix-tags-with-filename'?: boolean,
48
+ 'prefix-components-with-info-prop'?: string
49
+ },
50
+ packageVersion: string
51
+ ) {
52
+ const startedAt = performance.now();
53
+ if (argv.entrypoints.length < 2) { return exitWithError(`At least 2 entrypoints should be provided. \n\n`); }
54
+
55
+ const config: Config = await loadConfig();
56
+ const entrypoints = await getFallbackEntryPointsOrExit(argv.entrypoints, config);
57
+ const externalRefResolver = new BaseResolver(config.resolve);
58
+ const documents = await Promise.all(
59
+ entrypoints.map(
60
+ ({ path }) => externalRefResolver.resolveDocument(null, path, true) as Promise<Document>
61
+ )
62
+ );
63
+
64
+ const bundleResults = await Promise.all(
65
+ documents.map(document => bundleDocument({
66
+ document,
67
+ config: config.lint,
68
+ externalRefResolver
69
+ }).catch(e => {
70
+ exitWithError(`${e.message}: ${blue(document.source.absoluteRef)}`)
71
+ }))
72
+ );
73
+
74
+ for (const { problems, bundle: document } of (bundleResults as any)) {
75
+ const fileTotals = getTotals(problems);
76
+ if (fileTotals.errors) {
77
+ formatProblems(problems, {
78
+ totals: fileTotals,
79
+ version: document.parsed.version
80
+ });
81
+ exitWithError(`❌ Errors encountered while bundling ${blue(document.source.absoluteRef)}: join will not proceed.\n`);
82
+ }
83
+ }
84
+
85
+ for (const document of documents) {
86
+ try {
87
+ const version = detectOpenAPI(document.parsed)
88
+ if (version !== OasVersion.Version3_0) {
89
+ return exitWithError(`Only OpenAPI 3 is supported: ${blue(document.source.absoluteRef)} \n\n`);
90
+ }
91
+ } catch (e) {
92
+ return exitWithError(`${e.message}: ${blue(document.source.absoluteRef)}`);
93
+ }
94
+ }
95
+
96
+ if (argv.lint) {
97
+ for (const document of documents) {
98
+ await validateEntrypoint(document, config.lint, externalRefResolver, packageVersion);
99
+ }
100
+ }
101
+
102
+ let joinedDef: any = {};
103
+ let potentialConflicts = {
104
+ tags: {},
105
+ paths: {},
106
+ components: {},
107
+ xWebhooks: {}
108
+ };
109
+
110
+ const prefixComponentsWithInfoProp = argv['prefix-components-with-info-prop'];
111
+ const prefixTagsWithFilename = argv['prefix-tags-with-filename'];
112
+ const prefixTagsWithInfoProp = argv['prefix-tags-with-info-prop'];
113
+ if (prefixTagsWithFilename && prefixTagsWithInfoProp) {
114
+ return exitWithError(
115
+ `You used ${yellow('prefix-tags-with-filename')} and ${yellow('prefix-tags-with-info-prop')} that do not go together.\nPlease choose only one! \n\n`
116
+ );
117
+ }
118
+
119
+ addInfoSectionAndSpecVersion(documents, prefixComponentsWithInfoProp);
120
+
121
+ for (const document of documents) {
122
+ const openapi = document.parsed;
123
+ const { tags, info } = openapi;
124
+ const entrypoint = path.relative(process.cwd(), document.source.absoluteRef);
125
+ const entrypointFilename = getEntrypointFilename(entrypoint);
126
+ const tagsPrefix = prefixTagsWithFilename ? entrypointFilename : getInfoPrefix(info, prefixTagsWithInfoProp, 'tags');
127
+ const componentsPrefix = getInfoPrefix(info, prefixComponentsWithInfoProp, COMPONENTS);
128
+
129
+ if (openapi.hasOwnProperty('x-tagGroups')) {
130
+ process.stderr.write(yellow(`warning: x-tagGroups at ${blue(entrypoint)} will be skipped \n`));
131
+ }
132
+
133
+ const context = { entrypoint, entrypointFilename, tags, potentialConflicts, tagsPrefix, componentsPrefix };
134
+ if (tags) { populateTags(context); }
135
+ collectServers(openapi);
136
+ collectInfoDescriptions(openapi, context);
137
+ collectExternalDocs(openapi, context);
138
+ collectPaths(openapi, context);
139
+ collectComponents(openapi, context);
140
+ collectXWebhooks(openapi, context);
141
+ if (componentsPrefix) { replace$Refs(openapi, componentsPrefix); }
142
+ }
143
+
144
+ iteratePotentialConflicts(potentialConflicts);
145
+ const specFilename = 'openapi.yaml';
146
+ const noRefs = true;
147
+ if (!potentialConflictsTotal) { writeYaml(joinedDef, specFilename, noRefs); }
148
+ printExecutionTime('join', startedAt, specFilename);
149
+
150
+ function populateTags({
151
+ entrypoint,
152
+ entrypointFilename,
153
+ tags,
154
+ potentialConflicts,
155
+ tagsPrefix,
156
+ componentsPrefix
157
+ }: JoinDocumentContext) {
158
+ const xTagGroups = 'x-tagGroups';
159
+ const Tags = 'tags';
160
+ if (!joinedDef.hasOwnProperty(Tags)) { joinedDef[Tags] = []; }
161
+ if (!joinedDef.hasOwnProperty(xTagGroups)) { joinedDef[xTagGroups] = []; }
162
+ if (!potentialConflicts.tags.hasOwnProperty('all')) { potentialConflicts.tags['all'] = {}; }
163
+ if (!joinedDef[xTagGroups].some((g: any) => g.name === entrypointFilename)) {
164
+ joinedDef[xTagGroups].push({ name: entrypointFilename, tags: [] });
165
+ }
166
+ const indexGroup = joinedDef[xTagGroups].findIndex((item: any) => item.name === entrypointFilename);
167
+ if (!joinedDef[xTagGroups][indexGroup].hasOwnProperty(Tags)) { joinedDef[xTagGroups][indexGroup][Tags] = []; }
168
+ for (const tag of tags) {
169
+ const entrypointTagName = addPrefix(tag.name, tagsPrefix);
170
+ if (tag.description) {
171
+ tag.description = addComponentsPrefix(tag.description, componentsPrefix!);
172
+ }
173
+ if (!joinedDef.tags.find((t: any) => t.name === entrypointTagName)) {
174
+ tag['x-displayName'] = tag['x-displayName'] || tag.name;
175
+ tag.name = entrypointTagName;
176
+ joinedDef.tags.push(tag);
177
+ }
178
+ if (!joinedDef[xTagGroups][indexGroup][Tags].find((t: any) => t === entrypointTagName)) {
179
+ joinedDef[xTagGroups][indexGroup][Tags].push(entrypointTagName);
180
+ }
181
+
182
+ const doesEntrypointExist = !potentialConflicts.tags.all[entrypointTagName] || (
183
+ potentialConflicts.tags.all[entrypointTagName] &&
184
+ !potentialConflicts.tags.all[entrypointTagName].includes(entrypoint)
185
+ )
186
+ potentialConflicts.tags.all[entrypointTagName] = [
187
+ ...(potentialConflicts.tags.all[entrypointTagName] || []), ...(doesEntrypointExist ? [entrypoint] : [])
188
+ ];
189
+ }
190
+ }
191
+
192
+ function collectServers(openapi: Oas3Definition) {
193
+ const { servers } = openapi;
194
+ if (servers) {
195
+ if (!joinedDef.hasOwnProperty('servers')) { joinedDef['servers'] = []; }
196
+ for (const server of servers) {
197
+ if (!joinedDef.servers.some((s: any) => s.url === server.url)) {
198
+ joinedDef.servers.push(server);
199
+ }
200
+ }
201
+ }
202
+ }
203
+
204
+ function collectInfoDescriptions(
205
+ openapi: Oas3Definition,
206
+ {
207
+ entrypointFilename,
208
+ componentsPrefix
209
+ }: JoinDocumentContext
210
+ ) {
211
+ const { info } = openapi;
212
+ if (info?.description) {
213
+ const xTagGroups = 'x-tagGroups';
214
+ const groupIndex = joinedDef[xTagGroups] ? joinedDef[xTagGroups].findIndex((item: any) => item.name === entrypointFilename) : -1;
215
+ if (
216
+ joinedDef.hasOwnProperty(xTagGroups) &&
217
+ groupIndex !== -1 &&
218
+ joinedDef[xTagGroups][groupIndex]['tags'] &&
219
+ joinedDef[xTagGroups][groupIndex]['tags'].length
220
+ ) {
221
+ joinedDef[xTagGroups][groupIndex]['description'] = addComponentsPrefix(info.description, componentsPrefix!);
222
+ }
223
+ }
224
+ }
225
+
226
+ function collectExternalDocs(openapi: Oas3Definition, { entrypoint }: JoinDocumentContext) {
227
+ const { externalDocs } = openapi;
228
+ if (externalDocs) {
229
+ if (joinedDef.hasOwnProperty('externalDocs')) {
230
+ process.stderr.write(yellow(`warning: skip externalDocs from ${blue(path.basename(entrypoint))} \n`));
231
+ return;
232
+ }
233
+ joinedDef['externalDocs'] = externalDocs;
234
+ }
235
+ }
236
+
237
+ function collectPaths(
238
+ openapi: Oas3Definition,
239
+ {
240
+ entrypointFilename,
241
+ entrypoint,
242
+ potentialConflicts,
243
+ tagsPrefix,
244
+ componentsPrefix
245
+ }: JoinDocumentContext
246
+ ) {
247
+ const { paths } = openapi;
248
+ if (paths) {
249
+ if (!joinedDef.hasOwnProperty('paths')) { joinedDef['paths'] = {}; }
250
+ for (const path of Object.keys(paths)) {
251
+ if (!joinedDef.paths.hasOwnProperty(path)) { joinedDef.paths[path] = {}; }
252
+ if (!potentialConflicts.paths.hasOwnProperty(path)) { potentialConflicts.paths[path] = {}; }
253
+ for (const operation of Object.keys(paths[path])) {
254
+ // @ts-ignore
255
+ const pathOperation = paths[path][operation];
256
+ joinedDef.paths[path][operation] = pathOperation;
257
+ potentialConflicts.paths[path][operation] = [...(potentialConflicts.paths[path][operation] || []), entrypoint];
258
+ const { operationId } = pathOperation;
259
+ if (operationId) {
260
+ if (!potentialConflicts.paths.hasOwnProperty('operationIds')) { potentialConflicts.paths['operationIds'] = {}; }
261
+ potentialConflicts.paths.operationIds[operationId] = [...(potentialConflicts.paths.operationIds[operationId] || []), entrypoint];
262
+ }
263
+ let { tags, security } = joinedDef.paths[path][operation];
264
+ if (tags) {
265
+ joinedDef.paths[path][operation].tags = tags.map((tag: string) => addPrefix(tag, tagsPrefix));
266
+ populateTags({ entrypoint, entrypointFilename, tags: formatTags(tags), potentialConflicts, tagsPrefix, componentsPrefix });
267
+ } else {
268
+ joinedDef.paths[path][operation]['tags'] = [addPrefix('other', tagsPrefix || entrypointFilename)];
269
+ populateTags({ entrypoint, entrypointFilename, tags: formatTags(['other']), potentialConflicts, tagsPrefix: tagsPrefix || entrypointFilename, componentsPrefix });
270
+ }
271
+ if (!security && openapi.hasOwnProperty('security')) {
272
+ joinedDef.paths[path][operation]['security'] = addSecurityPrefix(openapi.security, componentsPrefix!);
273
+ } else if (pathOperation.security) {
274
+ joinedDef.paths[path][operation].security = addSecurityPrefix(pathOperation.security, componentsPrefix!);
275
+ }
276
+ }
277
+ }
278
+ }
279
+ }
280
+
281
+ function collectComponents(
282
+ openapi: Oas3Definition,
283
+ {
284
+ entrypoint,
285
+ potentialConflicts,
286
+ componentsPrefix
287
+ }: JoinDocumentContext
288
+ ) {
289
+ const { components } = openapi;
290
+ if (components) {
291
+ if (!joinedDef.hasOwnProperty(COMPONENTS)) { joinedDef[COMPONENTS] = {}; }
292
+ for (const component of Object.keys(components)) {
293
+ if (!potentialConflicts[COMPONENTS].hasOwnProperty(component)) {
294
+ potentialConflicts[COMPONENTS][component] = {};
295
+ joinedDef[COMPONENTS][component] = {};
296
+ }
297
+ // @ts-ignore
298
+ const componentObj = components[component];
299
+ for (const item of Object.keys(componentObj)) {
300
+ const componentPrefix = addPrefix(item, componentsPrefix!);
301
+ potentialConflicts.components[component][componentPrefix] = [
302
+ ...(potentialConflicts.components[component][item] || []), { [entrypoint]: componentObj[item]}
303
+ ];
304
+ joinedDef.components[component][componentPrefix] = componentObj[item];
305
+ }
306
+ }
307
+ }
308
+ }
309
+
310
+ function collectXWebhooks(
311
+ openapi: Oas3Definition,
312
+ {
313
+ entrypointFilename,
314
+ entrypoint,
315
+ potentialConflicts,
316
+ tagsPrefix,
317
+ componentsPrefix
318
+ }: JoinDocumentContext
319
+ ) {
320
+ const xWebhooks = 'x-webhooks';
321
+ // @ts-ignore
322
+ const openapiXWebhooks = openapi[xWebhooks];
323
+ if (openapiXWebhooks) {
324
+ if (!joinedDef.hasOwnProperty(xWebhooks)) { joinedDef[xWebhooks] = {}; }
325
+ for (const webhook of Object.keys(openapiXWebhooks)) {
326
+ joinedDef[xWebhooks][webhook] = openapiXWebhooks[webhook];
327
+
328
+ if (!potentialConflicts.xWebhooks.hasOwnProperty(webhook)) { potentialConflicts.xWebhooks[webhook] = {}; }
329
+ for (const operation of Object.keys(openapiXWebhooks[webhook])) {
330
+ potentialConflicts.xWebhooks[webhook][operation] = [...(potentialConflicts.xWebhooks[webhook][operation] || []), entrypoint];
331
+ }
332
+ for (const operationKey of Object.keys(joinedDef[xWebhooks][webhook])) {
333
+ let { tags } = joinedDef[xWebhooks][webhook][operationKey];
334
+ if (tags) {
335
+ joinedDef[xWebhooks][webhook][operationKey].tags = tags.map((tag: string) => addPrefix(tag, tagsPrefix));
336
+ populateTags({ entrypoint, entrypointFilename, tags: formatTags(tags), potentialConflicts, tagsPrefix, componentsPrefix });
337
+ }
338
+ }
339
+ }
340
+ }
341
+ }
342
+
343
+ function addInfoSectionAndSpecVersion(documents: any, prefixComponentsWithInfoProp: string | undefined) {
344
+ const firstEntrypoint = documents[0];
345
+ const openapi = firstEntrypoint.parsed;
346
+ const componentsPrefix = getInfoPrefix(openapi.info, prefixComponentsWithInfoProp, COMPONENTS);
347
+ if (!openapi.openapi) exitWithError('Version of specification is not found in. \n');
348
+ if (!openapi.info) exitWithError('Info section is not found in specification. \n');
349
+ if (openapi.info?.description) {
350
+ openapi.info.description = addComponentsPrefix(openapi.info.description, componentsPrefix);
351
+ }
352
+ joinedDef.openapi = openapi.openapi;
353
+ joinedDef.info = openapi.info;
354
+ }
355
+ }
356
+
357
+ function doesComponentsDiffer(curr: object, next: object) {
358
+ return !isEqual(Object.values(curr)[0], Object.values(next)[0]);
359
+ }
360
+
361
+ function validateComponentsDifference(files: any) {
362
+ let isDiffer = false;
363
+ for (let i = 0, len = files.length; i < len; i++) {
364
+ let next = files[i + 1];
365
+ if (next && doesComponentsDiffer(files[i], next)) {
366
+ isDiffer = true;
367
+ }
368
+ }
369
+ return isDiffer;
370
+ }
371
+
372
+ function iteratePotentialConflicts(potentialConflicts: any) {
373
+ for (const group of Object.keys(potentialConflicts)) {
374
+ for (const [key, value] of Object.entries(potentialConflicts[group])) {
375
+ const conflicts = filterConflicts(value as object);
376
+ if (conflicts.length) {
377
+ if (group === COMPONENTS) {
378
+ for (const [_, conflict] of Object.entries(conflicts)) {
379
+ if (validateComponentsDifference(conflict[1])) {
380
+ conflict[1] = conflict[1].map((c: string) => Object.keys(c)[0]);
381
+ showConflicts(green(group) + ' => ' + key, [conflict]);
382
+ potentialConflictsTotal += 1;
383
+ }
384
+ }
385
+ } else {
386
+ showConflicts(green(group) +' => '+ key, conflicts);
387
+ potentialConflictsTotal += conflicts.length;
388
+ }
389
+ prefixTagSuggestion(group, conflicts.length);
390
+ }
391
+ }
392
+ }
393
+ }
394
+
395
+ function prefixTagSuggestion(group: string, conflictsLength: number) {
396
+ if (group === 'tags') {
397
+ process.stderr.write(green(`
398
+ ${conflictsLength} conflict(s) on tags.
399
+ Suggestion: please use ${blue('prefix-tags-with-filename')} or ${blue('prefix-tags-with-info-prop')} to prevent naming conflicts. \n\n`
400
+ ));
401
+ }
402
+ }
403
+
404
+ function showConflicts(key: string, conflicts: any) {
405
+ for (const [path, files] of conflicts) {
406
+ process.stderr.write(yellow(`Conflict on ${key} : ${red(path)} in files: ${blue(files)} \n`));
407
+ }
408
+ }
409
+
410
+ function filterConflicts(entities: object) {
411
+ return Object.entries(entities).filter(([_, files]) => files.length > 1);
412
+ }
413
+
414
+ function getEntrypointFilename(filePath: string) {
415
+ return path.basename(filePath, path.extname(filePath))
416
+ }
417
+
418
+ function addPrefix(tag: string, tagsPrefix: string) {
419
+ return tagsPrefix ? tagsPrefix +'_'+ tag : tag;
420
+ }
421
+
422
+ function formatTags(tags: string[]) {
423
+ return tags.map((tag: string) => ({ name: tag }));
424
+ }
425
+
426
+ function addComponentsPrefix(description: string, componentsPrefix: string) {
427
+ return description.replace(/"(#\/components\/.*?)"/g, (match) => {
428
+ const componentName = path.basename(match);
429
+ return match.replace(componentName, addPrefix(componentName, componentsPrefix));
430
+ })
431
+ }
432
+
433
+ function addSecurityPrefix(security: any, componentsPrefix: string) {
434
+ return componentsPrefix ? security?.map((s: any) => {
435
+ const key = Object.keys(s)[0];
436
+ return { [componentsPrefix +'_'+ key]: s[key] }
437
+ }) : security;
438
+ }
439
+
440
+ function getInfoPrefix(info: any, prefixArg: string | undefined, type: string) {
441
+ if (!prefixArg) return '';
442
+ if (!info) exitWithError('Info section is not found in specification. \n');
443
+ if (!info[prefixArg]) exitWithError(`${yellow(`prefix-${type}-with-info-prop`)} argument value is not found in info section. \n`);
444
+ if (!isString(info[prefixArg])) exitWithError(`${yellow(`prefix-${type}-with-info-prop`)} argument value should be string. \n\n`);
445
+ if (info[prefixArg].length > 50) exitWithError(`${yellow(`prefix-${type}-with-info-prop`)} argument value length should not exceed 50 characters. \n\n`);
446
+ return info[prefixArg];
447
+ }
448
+
449
+ async function validateEntrypoint(document: Document, config: LintConfig, externalRefResolver: BaseResolver, packageVersion: string) {
450
+ try {
451
+ const results = await lintDocument({ document, config, externalRefResolver });
452
+ const fileTotals = getTotals(results);
453
+ formatProblems(results, { format: 'stylish', totals: fileTotals, version: packageVersion });
454
+ printLintTotals(fileTotals, 2);
455
+ } catch (err) {
456
+ handleError(err, document.parsed);
457
+ }
458
+ }
459
+
460
+ function crawl(object: any, visitor: any) {
461
+ if (!isObject(object)) return;
462
+ for (const key of Object.keys(object)) {
463
+ visitor(object, key);
464
+ crawl(object[key], visitor);
465
+ }
466
+ }
467
+
468
+ function replace$Refs(obj: any, componentsPrefix: string) {
469
+ crawl(obj, (node: any) => {
470
+ if (node.$ref && isString(node.$ref) && node.$ref.startsWith(`#/${COMPONENTS}/`)) {
471
+ const name = path.basename(node.$ref);
472
+ node.$ref = node.$ref.replace(name, componentsPrefix +'_'+ name);
473
+ } else if (
474
+ node.discriminator &&
475
+ node.discriminator.mapping &&
476
+ isObject(node.discriminator.mapping)
477
+ ) {
478
+ const { mapping } = node.discriminator;
479
+ for (const name of Object.keys(mapping)) {
480
+ if (isString(mapping[name]) && mapping[name].startsWith(`#/${COMPONENTS}/`)) {
481
+ mapping[name] = mapping[name].split('/').map((name: string, i: number, arr: []) => {
482
+ return (arr.length - 1 === i && !name.includes(componentsPrefix)) ? componentsPrefix+'_'+name : name;
483
+ }).join('/')
484
+ }
485
+ }
486
+ }
487
+ })
488
+ }
@@ -0,0 +1,110 @@
1
+ import {
2
+ Config,
3
+ formatProblems,
4
+ getTotals,
5
+ lint,
6
+ loadConfig,
7
+ getMergedConfig,
8
+ OutputFormat,
9
+ } from '@redocly/openapi-core';
10
+ import {
11
+ getExecutionTime,
12
+ getFallbackEntryPointsOrExit,
13
+ handleError,
14
+ pluralize,
15
+ printLintTotals,
16
+ printUnusedWarnings,
17
+ } from '../utils';
18
+ import { Totals } from '../types';
19
+ import { blue, gray, red } from 'colorette';
20
+ import { performance } from 'perf_hooks';
21
+
22
+ export async function handleLint(
23
+ argv: {
24
+ entrypoints: string[];
25
+ 'max-problems'?: number;
26
+ 'generate-ignore-file'?: boolean;
27
+ 'skip-rule'?: string[];
28
+ 'skip-preprocessor'?: string[];
29
+ extends?: string[];
30
+ config?: string;
31
+ format: OutputFormat;
32
+ },
33
+ version: string,
34
+ ) {
35
+ const config: Config = await loadConfig(argv.config, argv.extends);
36
+ const entrypoints = await getFallbackEntryPointsOrExit(argv.entrypoints, config);
37
+
38
+ if (argv['generate-ignore-file']) {
39
+ config.lint.ignore = {}; // clear ignore
40
+ }
41
+ const totals: Totals = { errors: 0, warnings: 0, ignored: 0 };
42
+ let totalIgnored = 0;
43
+
44
+ // TODO: use shared externalRef resolver, blocked by preprocessors now as they can mutate documents
45
+ for (const { path, alias } of entrypoints) {
46
+ try {
47
+ const startedAt = performance.now();
48
+ const resolvedConfig = getMergedConfig(config, alias);
49
+ resolvedConfig.lint.skipRules(argv['skip-rule']);
50
+ resolvedConfig.lint.skipPreprocessors(argv['skip-preprocessor']);
51
+
52
+ if (resolvedConfig.lint.recommendedFallback) {
53
+ process.stderr.write(
54
+ `No configurations were defined in extends -- using built in ${blue(
55
+ 'recommended',
56
+ )} configuration by default.\n${red(
57
+ 'Warning! This default behavior is going to be deprecated soon.',
58
+ )}\n\n`,
59
+ );
60
+ }
61
+ process.stderr.write(gray(`validating ${path.replace(process.cwd(), '')}...\n`));
62
+ const results = await lint({
63
+ ref: path,
64
+ config: resolvedConfig,
65
+ });
66
+
67
+ const fileTotals = getTotals(results);
68
+ totals.errors += fileTotals.errors;
69
+ totals.warnings += fileTotals.warnings;
70
+ totals.ignored += fileTotals.ignored;
71
+
72
+ if (argv['generate-ignore-file']) {
73
+ for (let m of results) {
74
+ config.lint.addIgnore(m);
75
+ totalIgnored++;
76
+ }
77
+ } else {
78
+ formatProblems(results, {
79
+ format: argv.format,
80
+ maxProblems: argv['max-problems'],
81
+ totals: fileTotals,
82
+ version,
83
+ });
84
+ }
85
+
86
+ const elapsed = getExecutionTime(startedAt);
87
+ process.stderr.write(gray(`${path.replace(process.cwd(), '')}: validated in ${elapsed}\n\n`));
88
+ } catch (e) {
89
+ totals.errors++;
90
+ handleError(e, path);
91
+ }
92
+ }
93
+
94
+ if (argv['generate-ignore-file']) {
95
+ config.lint.saveIgnore();
96
+ process.stderr.write(
97
+ `Generated ignore file with ${totalIgnored} ${pluralize('problem', totalIgnored)}.\n\n`,
98
+ );
99
+ } else {
100
+ printLintTotals(totals, entrypoints.length);
101
+ }
102
+
103
+ printUnusedWarnings(config.lint);
104
+
105
+ // defer process exit to allow STDOUT pipe to flush
106
+ // see https://github.com/nodejs/node-v0.x-archive/issues/3737#issuecomment-19156072
107
+ process.once('exit', () =>
108
+ process.exit(totals.errors === 0 || argv['generate-ignore-file'] ? 0 : 1),
109
+ );
110
+ }
@@ -0,0 +1,19 @@
1
+ import { Region, RedoclyClient, loadConfig } from '@redocly/openapi-core';
2
+ import { blue, green } from 'colorette';
3
+ import { promptUser } from '../utils';
4
+
5
+ export function promptClientToken(domain: string) {
6
+ return promptUser(
7
+ green(
8
+ `\n 🔑 Copy your API key from ${blue(`https://app.${domain}/profile`)} and paste it below`,
9
+ ),
10
+ true,
11
+ );
12
+ }
13
+
14
+ export async function handleLogin(argv: { verbose?: boolean; region?: Region }) {
15
+ const region = argv.region || (await loadConfig()).region;
16
+ const client = new RedoclyClient(region);
17
+ const clientToken = await promptClientToken(client.domain);
18
+ client.login(clientToken, argv.verbose);
19
+ }