@redocly/cli 1.0.0 → 1.0.1

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 (57) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/lib/commands/build-docs/index.js +2 -4
  3. package/lib/commands/build-docs/utils.d.ts +1 -1
  4. package/lib/commands/build-docs/utils.js +3 -3
  5. package/package.json +2 -2
  6. package/src/__mocks__/@redocly/openapi-core.ts +80 -0
  7. package/src/__mocks__/documents.ts +63 -0
  8. package/src/__mocks__/fs.ts +6 -0
  9. package/src/__mocks__/perf_hooks.ts +3 -0
  10. package/src/__mocks__/redoc.ts +2 -0
  11. package/src/__mocks__/utils.ts +19 -0
  12. package/src/__tests__/commands/build-docs.test.ts +62 -0
  13. package/src/__tests__/commands/bundle.test.ts +150 -0
  14. package/src/__tests__/commands/join.test.ts +122 -0
  15. package/src/__tests__/commands/lint.test.ts +190 -0
  16. package/src/__tests__/commands/push-region.test.ts +58 -0
  17. package/src/__tests__/commands/push.test.ts +492 -0
  18. package/src/__tests__/fetch-with-timeout.test.ts +35 -0
  19. package/src/__tests__/fixtures/config.ts +21 -0
  20. package/src/__tests__/fixtures/openapi.json +0 -0
  21. package/src/__tests__/fixtures/openapi.yaml +0 -0
  22. package/src/__tests__/fixtures/redocly.yaml +0 -0
  23. package/src/__tests__/utils.test.ts +564 -0
  24. package/src/__tests__/wrapper.test.ts +57 -0
  25. package/src/assert-node-version.ts +8 -0
  26. package/src/commands/build-docs/index.ts +50 -0
  27. package/src/commands/build-docs/template.hbs +23 -0
  28. package/src/commands/build-docs/types.ts +24 -0
  29. package/src/commands/build-docs/utils.ts +110 -0
  30. package/src/commands/bundle.ts +177 -0
  31. package/src/commands/join.ts +811 -0
  32. package/src/commands/lint.ts +151 -0
  33. package/src/commands/login.ts +27 -0
  34. package/src/commands/preview-docs/index.ts +190 -0
  35. package/src/commands/preview-docs/preview-server/default.hbs +24 -0
  36. package/src/commands/preview-docs/preview-server/hot.js +42 -0
  37. package/src/commands/preview-docs/preview-server/oauth2-redirect.html +21 -0
  38. package/src/commands/preview-docs/preview-server/preview-server.ts +156 -0
  39. package/src/commands/preview-docs/preview-server/server.ts +91 -0
  40. package/src/commands/push.ts +441 -0
  41. package/src/commands/split/__tests__/fixtures/samples.json +61 -0
  42. package/src/commands/split/__tests__/fixtures/spec.json +70 -0
  43. package/src/commands/split/__tests__/fixtures/webhooks.json +85 -0
  44. package/src/commands/split/__tests__/index.test.ts +137 -0
  45. package/src/commands/split/index.ts +385 -0
  46. package/src/commands/split/types.ts +85 -0
  47. package/src/commands/stats.ts +119 -0
  48. package/src/custom.d.ts +1 -0
  49. package/src/fetch-with-timeout.ts +21 -0
  50. package/src/index.ts +484 -0
  51. package/src/js-utils.ts +17 -0
  52. package/src/types.ts +40 -0
  53. package/src/update-version-notifier.ts +106 -0
  54. package/src/utils.ts +590 -0
  55. package/src/wrapper.ts +42 -0
  56. package/tsconfig.json +9 -0
  57. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,811 @@
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
+ StyleguideConfig,
12
+ Oas3Tag,
13
+ formatProblems,
14
+ getTotals,
15
+ lintDocument,
16
+ detectOpenAPI,
17
+ bundleDocument,
18
+ Referenced,
19
+ isRef,
20
+ } from '@redocly/openapi-core';
21
+
22
+ import {
23
+ getFallbackApisOrExit,
24
+ printExecutionTime,
25
+ handleError,
26
+ printLintTotals,
27
+ writeYaml,
28
+ exitWithError,
29
+ sortTopLevelKeysForOas,
30
+ } from '../utils';
31
+ import { isObject, isString, keysOf } from '../js-utils';
32
+ import { Oas3Parameter, Oas3PathItem, Oas3Server } from '@redocly/openapi-core/lib/typings/openapi';
33
+ import { OPENAPI3_METHOD } from './split/types';
34
+ import { BundleResult } from '@redocly/openapi-core/lib/bundle';
35
+
36
+ const COMPONENTS = 'components';
37
+ const Tags = 'tags';
38
+ const xTagGroups = 'x-tagGroups';
39
+ let potentialConflictsTotal = 0;
40
+
41
+ type JoinDocumentContext = {
42
+ api: string;
43
+ apiFilename: string;
44
+ tags: Oas3Tag[];
45
+ potentialConflicts: any;
46
+ tagsPrefix: string;
47
+ componentsPrefix: string | undefined;
48
+ };
49
+
50
+ export type JoinOptions = {
51
+ apis: string[];
52
+ lint?: boolean;
53
+ decorate?: boolean;
54
+ preprocess?: boolean;
55
+ 'prefix-tags-with-info-prop'?: string;
56
+ 'prefix-tags-with-filename'?: boolean;
57
+ 'prefix-components-with-info-prop'?: string;
58
+ 'without-x-tag-groups'?: boolean;
59
+ output?: string;
60
+ config?: string;
61
+ extends?: undefined;
62
+ 'lint-config'?: undefined;
63
+ };
64
+
65
+ export async function handleJoin(argv: JoinOptions, config: Config, packageVersion: string) {
66
+ const startedAt = performance.now();
67
+ if (argv.apis.length < 2) {
68
+ return exitWithError(`At least 2 apis should be provided. \n\n`);
69
+ }
70
+
71
+ const {
72
+ 'prefix-components-with-info-prop': prefixComponentsWithInfoProp,
73
+ 'prefix-tags-with-filename': prefixTagsWithFilename,
74
+ 'prefix-tags-with-info-prop': prefixTagsWithInfoProp,
75
+ 'without-x-tag-groups': withoutXTagGroups,
76
+ output: specFilename = 'openapi.yaml',
77
+ } = argv;
78
+
79
+ const usedTagsOptions = [
80
+ prefixTagsWithFilename && 'prefix-tags-with-filename',
81
+ prefixTagsWithInfoProp && 'prefix-tags-with-info-prop',
82
+ withoutXTagGroups && 'without-x-tag-groups',
83
+ ].filter(Boolean);
84
+
85
+ if (usedTagsOptions.length > 1) {
86
+ return exitWithError(
87
+ `You use ${yellow(usedTagsOptions.join(', '))} together.\nPlease choose only one! \n\n`
88
+ );
89
+ }
90
+
91
+ const apis = await getFallbackApisOrExit(argv.apis, config);
92
+ const externalRefResolver = new BaseResolver(config.resolve);
93
+ const documents = await Promise.all(
94
+ apis.map(
95
+ ({ path }) => externalRefResolver.resolveDocument(null, path, true) as Promise<Document>
96
+ )
97
+ );
98
+
99
+ if (!argv.decorate) {
100
+ const decorators = new Set([
101
+ ...Object.keys(config.styleguide.decorators.oas3_0),
102
+ ...Object.keys(config.styleguide.decorators.oas3_1),
103
+ ...Object.keys(config.styleguide.decorators.oas2),
104
+ ]);
105
+ config.styleguide.skipDecorators(Array.from(decorators));
106
+ }
107
+
108
+ if (!argv.preprocess) {
109
+ const preprocessors = new Set([
110
+ ...Object.keys(config.styleguide.preprocessors.oas3_0),
111
+ ...Object.keys(config.styleguide.preprocessors.oas3_1),
112
+ ...Object.keys(config.styleguide.preprocessors.oas2),
113
+ ]);
114
+ config.styleguide.skipPreprocessors(Array.from(preprocessors));
115
+ }
116
+
117
+ const bundleResults = await Promise.all(
118
+ documents.map((document) =>
119
+ bundleDocument({
120
+ document,
121
+ config: config.styleguide,
122
+ externalRefResolver,
123
+ }).catch((e) => {
124
+ exitWithError(`${e.message}: ${blue(document.source.absoluteRef)}`);
125
+ })
126
+ )
127
+ );
128
+
129
+ for (const { problems, bundle: document } of bundleResults as BundleResult[]) {
130
+ const fileTotals = getTotals(problems);
131
+ if (fileTotals.errors) {
132
+ formatProblems(problems, {
133
+ totals: fileTotals,
134
+ version: document.parsed.version,
135
+ });
136
+ exitWithError(
137
+ `❌ Errors encountered while bundling ${blue(
138
+ document.source.absoluteRef
139
+ )}: join will not proceed.\n`
140
+ );
141
+ }
142
+ }
143
+
144
+ for (const document of documents) {
145
+ try {
146
+ const version = detectOpenAPI(document.parsed);
147
+ if (version !== OasVersion.Version3_0) {
148
+ return exitWithError(
149
+ `Only OpenAPI 3 is supported: ${blue(document.source.absoluteRef)} \n\n`
150
+ );
151
+ }
152
+ } catch (e) {
153
+ return exitWithError(`${e.message}: ${blue(document.source.absoluteRef)}`);
154
+ }
155
+ }
156
+
157
+ if (argv.lint) {
158
+ for (const document of documents) {
159
+ await validateApi(document, config.styleguide, externalRefResolver, packageVersion);
160
+ }
161
+ }
162
+
163
+ const joinedDef: any = {};
164
+ const potentialConflicts = {
165
+ tags: {},
166
+ paths: {},
167
+ components: {},
168
+ xWebhooks: {},
169
+ };
170
+
171
+ addInfoSectionAndSpecVersion(documents, prefixComponentsWithInfoProp);
172
+
173
+ for (const document of documents) {
174
+ const openapi = document.parsed;
175
+ const { tags, info } = openapi;
176
+ const api = path.relative(process.cwd(), document.source.absoluteRef);
177
+ const apiFilename = getApiFilename(api);
178
+ const tagsPrefix = prefixTagsWithFilename
179
+ ? apiFilename
180
+ : getInfoPrefix(info, prefixTagsWithInfoProp, 'tags');
181
+ const componentsPrefix = getInfoPrefix(info, prefixComponentsWithInfoProp, COMPONENTS);
182
+
183
+ if (openapi.hasOwnProperty('x-tagGroups')) {
184
+ process.stderr.write(yellow(`warning: x-tagGroups at ${blue(api)} will be skipped \n`));
185
+ }
186
+
187
+ const context = {
188
+ api,
189
+ apiFilename,
190
+ tags,
191
+ potentialConflicts,
192
+ tagsPrefix,
193
+ componentsPrefix,
194
+ };
195
+ if (tags) {
196
+ populateTags(context);
197
+ }
198
+ collectServers(openapi);
199
+ collectInfoDescriptions(openapi, context);
200
+ collectExternalDocs(openapi, context);
201
+ collectPaths(openapi, context);
202
+ collectComponents(openapi, context);
203
+ collectXWebhooks(openapi, context);
204
+ if (componentsPrefix) {
205
+ replace$Refs(openapi, componentsPrefix);
206
+ }
207
+ }
208
+
209
+ iteratePotentialConflicts(potentialConflicts, withoutXTagGroups);
210
+ const noRefs = true;
211
+
212
+ if (potentialConflictsTotal) {
213
+ return exitWithError(`Please fix conflicts before running ${yellow('join')}.`);
214
+ }
215
+
216
+ writeYaml(sortTopLevelKeysForOas(joinedDef), specFilename, noRefs);
217
+ printExecutionTime('join', startedAt, specFilename);
218
+
219
+ function populateTags({
220
+ api,
221
+ apiFilename,
222
+ tags,
223
+ potentialConflicts,
224
+ tagsPrefix,
225
+ componentsPrefix,
226
+ }: JoinDocumentContext) {
227
+ if (!joinedDef.hasOwnProperty(Tags)) {
228
+ joinedDef[Tags] = [];
229
+ }
230
+ if (!potentialConflicts.tags.hasOwnProperty('all')) {
231
+ potentialConflicts.tags['all'] = {};
232
+ }
233
+ if (withoutXTagGroups && !potentialConflicts.tags.hasOwnProperty('description')) {
234
+ potentialConflicts.tags['description'] = {};
235
+ }
236
+ for (const tag of tags) {
237
+ const entrypointTagName = addPrefix(tag.name, tagsPrefix);
238
+ if (tag.description) {
239
+ tag.description = addComponentsPrefix(tag.description, componentsPrefix!);
240
+ }
241
+
242
+ const tagDuplicate = joinedDef.tags.find((t: Oas3Tag) => t.name === entrypointTagName);
243
+
244
+ if (tagDuplicate && withoutXTagGroups) {
245
+ // If tag already exist and `without-x-tag-groups` option,
246
+ // check if description are different for potential conflicts warning.
247
+ const isTagDescriptionNotEqual =
248
+ tag.hasOwnProperty('description') && tagDuplicate.description !== tag.description;
249
+
250
+ potentialConflicts.tags.description[entrypointTagName].push(
251
+ ...(isTagDescriptionNotEqual ? [api] : [])
252
+ );
253
+ } else if (!tagDuplicate) {
254
+ // Instead add tag to joinedDef if there no duplicate;
255
+ tag['x-displayName'] = tag['x-displayName'] || tag.name;
256
+ tag.name = entrypointTagName;
257
+ joinedDef.tags.push(tag);
258
+
259
+ if (withoutXTagGroups) {
260
+ potentialConflicts.tags.description[entrypointTagName] = [api];
261
+ }
262
+ }
263
+
264
+ if (!withoutXTagGroups) {
265
+ createXTagGroups(apiFilename);
266
+ if (!tagDuplicate) {
267
+ populateXTagGroups(entrypointTagName, getIndexGroup(apiFilename));
268
+ }
269
+ }
270
+
271
+ const doesEntrypointExist =
272
+ !potentialConflicts.tags.all[entrypointTagName] ||
273
+ (potentialConflicts.tags.all[entrypointTagName] &&
274
+ !potentialConflicts.tags.all[entrypointTagName].includes(api));
275
+ potentialConflicts.tags.all[entrypointTagName] = [
276
+ ...(potentialConflicts.tags.all[entrypointTagName] || []),
277
+ ...(!withoutXTagGroups && doesEntrypointExist ? [api] : []),
278
+ ];
279
+ }
280
+ }
281
+
282
+ function getIndexGroup(apiFilename: string): number {
283
+ return joinedDef[xTagGroups].findIndex((item: any) => item.name === apiFilename);
284
+ }
285
+
286
+ function createXTagGroups(apiFilename: string) {
287
+ if (!joinedDef.hasOwnProperty(xTagGroups)) {
288
+ joinedDef[xTagGroups] = [];
289
+ }
290
+
291
+ if (!joinedDef[xTagGroups].some((g: any) => g.name === apiFilename)) {
292
+ joinedDef[xTagGroups].push({ name: apiFilename, tags: [] });
293
+ }
294
+
295
+ const indexGroup = getIndexGroup(apiFilename);
296
+
297
+ if (!joinedDef[xTagGroups][indexGroup].hasOwnProperty(Tags)) {
298
+ joinedDef[xTagGroups][indexGroup][Tags] = [];
299
+ }
300
+ }
301
+
302
+ function populateXTagGroups(entrypointTagName: string, indexGroup: number) {
303
+ if (
304
+ !joinedDef[xTagGroups][indexGroup][Tags].find((t: Oas3Tag) => t.name === entrypointTagName)
305
+ ) {
306
+ joinedDef[xTagGroups][indexGroup][Tags].push(entrypointTagName);
307
+ }
308
+ }
309
+
310
+ function collectServers(openapi: Oas3Definition) {
311
+ const { servers } = openapi;
312
+ if (servers) {
313
+ if (!joinedDef.hasOwnProperty('servers')) {
314
+ joinedDef['servers'] = [];
315
+ }
316
+ for (const server of servers) {
317
+ if (!joinedDef.servers.some((s: any) => s.url === server.url)) {
318
+ joinedDef.servers.push(server);
319
+ }
320
+ }
321
+ }
322
+ }
323
+
324
+ function collectInfoDescriptions(
325
+ openapi: Oas3Definition,
326
+ { apiFilename, componentsPrefix }: JoinDocumentContext
327
+ ) {
328
+ const { info } = openapi;
329
+ if (info?.description) {
330
+ const groupIndex = joinedDef[xTagGroups] ? getIndexGroup(apiFilename) : -1;
331
+ if (
332
+ joinedDef.hasOwnProperty(xTagGroups) &&
333
+ groupIndex !== -1 &&
334
+ joinedDef[xTagGroups][groupIndex]['tags'] &&
335
+ joinedDef[xTagGroups][groupIndex]['tags'].length
336
+ ) {
337
+ joinedDef[xTagGroups][groupIndex]['description'] = addComponentsPrefix(
338
+ info.description,
339
+ componentsPrefix!
340
+ );
341
+ }
342
+ }
343
+ }
344
+
345
+ function collectExternalDocs(openapi: Oas3Definition, { api }: JoinDocumentContext) {
346
+ const { externalDocs } = openapi;
347
+ if (externalDocs) {
348
+ if (joinedDef.hasOwnProperty('externalDocs')) {
349
+ process.stderr.write(
350
+ yellow(`warning: skip externalDocs from ${blue(path.basename(api))} \n`)
351
+ );
352
+ return;
353
+ }
354
+ joinedDef['externalDocs'] = externalDocs;
355
+ }
356
+ }
357
+
358
+ function collectPaths(
359
+ openapi: Oas3Definition,
360
+ { apiFilename, api, potentialConflicts, tagsPrefix, componentsPrefix }: JoinDocumentContext
361
+ ) {
362
+ const { paths } = openapi;
363
+ const operationsSet = new Set(keysOf<typeof OPENAPI3_METHOD>(OPENAPI3_METHOD));
364
+ if (paths) {
365
+ if (!joinedDef.hasOwnProperty('paths')) {
366
+ joinedDef['paths'] = {};
367
+ }
368
+
369
+ for (const path of keysOf(paths)) {
370
+ if (!joinedDef.paths.hasOwnProperty(path)) {
371
+ joinedDef.paths[path] = {};
372
+ }
373
+ if (!potentialConflicts.paths.hasOwnProperty(path)) {
374
+ potentialConflicts.paths[path] = {};
375
+ }
376
+
377
+ const pathItem = paths[path] as Oas3PathItem;
378
+
379
+ for (const field of keysOf(pathItem)) {
380
+ if (operationsSet.has(field as OPENAPI3_METHOD)) {
381
+ collectPathOperation(pathItem, path, field as OPENAPI3_METHOD);
382
+ }
383
+ if (field === 'servers') {
384
+ collectPathServers(pathItem, path);
385
+ }
386
+ if (field === 'parameters') {
387
+ collectPathParameters(pathItem, path);
388
+ }
389
+ if (typeof pathItem[field] === 'string') {
390
+ collectPathStringFields(pathItem, path, field);
391
+ }
392
+ }
393
+ }
394
+ }
395
+
396
+ function collectPathStringFields(
397
+ pathItem: Oas3PathItem,
398
+ path: string | number,
399
+ field: keyof Oas3PathItem
400
+ ) {
401
+ const fieldValue = pathItem[field];
402
+ if (
403
+ joinedDef.paths[path].hasOwnProperty(field) &&
404
+ joinedDef.paths[path][field] !== fieldValue
405
+ ) {
406
+ process.stderr.write(yellow(`warning: different ${field} values in ${path}\n`));
407
+ return;
408
+ }
409
+ joinedDef.paths[path][field] = fieldValue;
410
+ }
411
+
412
+ function collectPathServers(pathItem: Oas3PathItem, path: string | number) {
413
+ if (!pathItem.servers) {
414
+ return;
415
+ }
416
+
417
+ if (!joinedDef.paths[path].hasOwnProperty('servers')) {
418
+ joinedDef.paths[path].servers = [];
419
+ }
420
+
421
+ for (const server of pathItem.servers) {
422
+ let isFoundServer = false;
423
+ for (const pathServer of joinedDef.paths[path].servers) {
424
+ if (pathServer.url === server.url) {
425
+ if (!isServersEqual(pathServer, server)) {
426
+ exitWithError(`Different server values for (${server.url}) in ${path}`);
427
+ }
428
+ isFoundServer = true;
429
+ }
430
+ }
431
+
432
+ if (!isFoundServer) {
433
+ joinedDef.paths[path].servers.push(server);
434
+ }
435
+ }
436
+ }
437
+
438
+ function collectPathParameters(pathItem: Oas3PathItem, path: string | number) {
439
+ if (!pathItem.parameters) {
440
+ return;
441
+ }
442
+ if (!joinedDef.paths[path].hasOwnProperty('parameters')) {
443
+ joinedDef.paths[path].parameters = [];
444
+ }
445
+
446
+ for (const parameter of pathItem.parameters as Referenced<Oas3Parameter>[]) {
447
+ let isFoundParameter = false;
448
+
449
+ for (const pathParameter of joinedDef.paths[path]
450
+ .parameters as Referenced<Oas3Parameter>[]) {
451
+ // Compare $ref only if both are reference objects
452
+ if (isRef(pathParameter) && isRef(parameter)) {
453
+ if (pathParameter['$ref'] === parameter['$ref']) {
454
+ isFoundParameter = true;
455
+ }
456
+ }
457
+ // Compare properties only if both are reference objects
458
+ if (!isRef(pathParameter) && !isRef(parameter)) {
459
+ if (pathParameter.name === parameter.name && pathParameter.in === parameter.in) {
460
+ if (!isEqual(pathParameter.schema, parameter.schema)) {
461
+ exitWithError(`Different parameter schemas for (${parameter.name}) in ${path}`);
462
+ }
463
+ isFoundParameter = true;
464
+ }
465
+ }
466
+ }
467
+
468
+ if (!isFoundParameter) {
469
+ joinedDef.paths[path].parameters.push(parameter);
470
+ }
471
+ }
472
+ }
473
+
474
+ function collectPathOperation(
475
+ pathItem: Oas3PathItem,
476
+ path: string | number,
477
+ operation: OPENAPI3_METHOD
478
+ ) {
479
+ const pathOperation = pathItem[operation];
480
+
481
+ if (!pathOperation) {
482
+ return;
483
+ }
484
+
485
+ joinedDef.paths[path][operation] = pathOperation;
486
+ potentialConflicts.paths[path][operation] = [
487
+ ...(potentialConflicts.paths[path][operation] || []),
488
+ api,
489
+ ];
490
+
491
+ const { operationId } = pathOperation;
492
+
493
+ if (operationId) {
494
+ if (!potentialConflicts.paths.hasOwnProperty('operationIds')) {
495
+ potentialConflicts.paths['operationIds'] = {};
496
+ }
497
+ potentialConflicts.paths.operationIds[operationId] = [
498
+ ...(potentialConflicts.paths.operationIds[operationId] || []),
499
+ api,
500
+ ];
501
+ }
502
+
503
+ const { tags, security } = joinedDef.paths[path][operation];
504
+
505
+ if (tags) {
506
+ joinedDef.paths[path][operation].tags = tags.map((tag: string) =>
507
+ addPrefix(tag, tagsPrefix)
508
+ );
509
+ populateTags({
510
+ api,
511
+ apiFilename,
512
+ tags: formatTags(tags),
513
+ potentialConflicts,
514
+ tagsPrefix,
515
+ componentsPrefix,
516
+ });
517
+ } else {
518
+ joinedDef.paths[path][operation]['tags'] = [addPrefix('other', tagsPrefix || apiFilename)];
519
+ populateTags({
520
+ api,
521
+ apiFilename,
522
+ tags: formatTags(['other']),
523
+ potentialConflicts,
524
+ tagsPrefix: tagsPrefix || apiFilename,
525
+ componentsPrefix,
526
+ });
527
+ }
528
+ if (!security && openapi.hasOwnProperty('security')) {
529
+ joinedDef.paths[path][operation]['security'] = addSecurityPrefix(
530
+ openapi.security,
531
+ componentsPrefix!
532
+ );
533
+ } else if (pathOperation.security) {
534
+ joinedDef.paths[path][operation].security = addSecurityPrefix(
535
+ pathOperation.security,
536
+ componentsPrefix!
537
+ );
538
+ }
539
+ }
540
+ }
541
+
542
+ function isServersEqual(serverOne: Oas3Server, serverTwo: Oas3Server) {
543
+ if (serverOne.description === serverTwo.description) {
544
+ return isEqual(serverOne.variables, serverTwo.variables);
545
+ }
546
+
547
+ return false;
548
+ }
549
+
550
+ function collectComponents(
551
+ openapi: Oas3Definition,
552
+ { api, potentialConflicts, componentsPrefix }: JoinDocumentContext
553
+ ) {
554
+ const { components } = openapi;
555
+ if (components) {
556
+ if (!joinedDef.hasOwnProperty(COMPONENTS)) {
557
+ joinedDef[COMPONENTS] = {};
558
+ }
559
+ for (const [component, componentObj] of Object.entries(components)) {
560
+ if (!potentialConflicts[COMPONENTS].hasOwnProperty(component)) {
561
+ potentialConflicts[COMPONENTS][component] = {};
562
+ joinedDef[COMPONENTS][component] = {};
563
+ }
564
+ for (const item of Object.keys(componentObj)) {
565
+ const componentPrefix = addPrefix(item, componentsPrefix!);
566
+ potentialConflicts.components[component][componentPrefix] = [
567
+ ...(potentialConflicts.components[component][item] || []),
568
+ { [api]: componentObj[item] },
569
+ ];
570
+ joinedDef.components[component][componentPrefix] = componentObj[item];
571
+ }
572
+ }
573
+ }
574
+ }
575
+
576
+ function collectXWebhooks(
577
+ openapi: Oas3Definition,
578
+ { apiFilename, api, potentialConflicts, tagsPrefix, componentsPrefix }: JoinDocumentContext
579
+ ) {
580
+ const xWebhooks = 'x-webhooks';
581
+ const openapiXWebhooks = openapi[xWebhooks];
582
+ if (openapiXWebhooks) {
583
+ if (!joinedDef.hasOwnProperty(xWebhooks)) {
584
+ joinedDef[xWebhooks] = {};
585
+ }
586
+ for (const webhook of Object.keys(openapiXWebhooks)) {
587
+ joinedDef[xWebhooks][webhook] = openapiXWebhooks[webhook];
588
+
589
+ if (!potentialConflicts.xWebhooks.hasOwnProperty(webhook)) {
590
+ potentialConflicts.xWebhooks[webhook] = {};
591
+ }
592
+ for (const operation of Object.keys(openapiXWebhooks[webhook])) {
593
+ potentialConflicts.xWebhooks[webhook][operation] = [
594
+ ...(potentialConflicts.xWebhooks[webhook][operation] || []),
595
+ api,
596
+ ];
597
+ }
598
+ for (const operationKey of Object.keys(joinedDef[xWebhooks][webhook])) {
599
+ const { tags } = joinedDef[xWebhooks][webhook][operationKey];
600
+ if (tags) {
601
+ joinedDef[xWebhooks][webhook][operationKey].tags = tags.map((tag: string) =>
602
+ addPrefix(tag, tagsPrefix)
603
+ );
604
+ populateTags({
605
+ api,
606
+ apiFilename,
607
+ tags: formatTags(tags),
608
+ potentialConflicts,
609
+ tagsPrefix,
610
+ componentsPrefix,
611
+ });
612
+ }
613
+ }
614
+ }
615
+ }
616
+ }
617
+
618
+ function addInfoSectionAndSpecVersion(
619
+ documents: any,
620
+ prefixComponentsWithInfoProp: string | undefined
621
+ ) {
622
+ const firstApi = documents[0];
623
+ const openapi = firstApi.parsed;
624
+ const componentsPrefix = getInfoPrefix(openapi.info, prefixComponentsWithInfoProp, COMPONENTS);
625
+ if (!openapi.openapi) exitWithError('Version of specification is not found in. \n');
626
+ if (!openapi.info) exitWithError('Info section is not found in specification. \n');
627
+ if (openapi.info?.description) {
628
+ openapi.info.description = addComponentsPrefix(openapi.info.description, componentsPrefix);
629
+ }
630
+ joinedDef.openapi = openapi.openapi;
631
+ joinedDef.info = openapi.info;
632
+ }
633
+ }
634
+
635
+ function doesComponentsDiffer(curr: object, next: object) {
636
+ return !isEqual(Object.values(curr)[0], Object.values(next)[0]);
637
+ }
638
+
639
+ function validateComponentsDifference(files: any) {
640
+ let isDiffer = false;
641
+ for (let i = 0, len = files.length; i < len; i++) {
642
+ const next = files[i + 1];
643
+ if (next && doesComponentsDiffer(files[i], next)) {
644
+ isDiffer = true;
645
+ }
646
+ }
647
+ return isDiffer;
648
+ }
649
+
650
+ function iteratePotentialConflicts(potentialConflicts: any, withoutXTagGroups?: boolean) {
651
+ for (const group of Object.keys(potentialConflicts)) {
652
+ for (const [key, value] of Object.entries(potentialConflicts[group])) {
653
+ const conflicts = filterConflicts(value as object);
654
+ if (conflicts.length) {
655
+ if (group === COMPONENTS) {
656
+ for (const [_, conflict] of Object.entries(conflicts)) {
657
+ if (validateComponentsDifference(conflict[1])) {
658
+ conflict[1] = conflict[1].map((c: string) => Object.keys(c)[0]);
659
+ showConflicts(green(group) + ' => ' + key, [conflict]);
660
+ potentialConflictsTotal += 1;
661
+ }
662
+ }
663
+ } else {
664
+ if (withoutXTagGroups && group === 'tags') {
665
+ duplicateTagDescriptionWarning(conflicts);
666
+ } else {
667
+ potentialConflictsTotal += conflicts.length;
668
+ showConflicts(green(group) + ' => ' + key, conflicts);
669
+ }
670
+ }
671
+
672
+ if (group === 'tags' && !withoutXTagGroups) {
673
+ prefixTagSuggestion(conflicts.length);
674
+ }
675
+ }
676
+ }
677
+ }
678
+ }
679
+
680
+ function duplicateTagDescriptionWarning(conflicts: [string, any][]) {
681
+ const tagsKeys = conflicts.map(([tagName]) => `\`${tagName}\``);
682
+ const joinString = yellow(', ');
683
+ process.stderr.write(
684
+ yellow(
685
+ `\nwarning: ${tagsKeys.length} conflict(s) on the ${red(
686
+ tagsKeys.join(joinString)
687
+ )} tags description.\n`
688
+ )
689
+ );
690
+ }
691
+
692
+ function prefixTagSuggestion(conflictsLength: number) {
693
+ process.stderr.write(
694
+ green(
695
+ `\n${conflictsLength} conflict(s) on tags.\nSuggestion: please use ${blue(
696
+ 'prefix-tags-with-filename'
697
+ )}, ${blue('prefix-tags-with-info-prop')} or ${blue(
698
+ 'without-x-tag-groups'
699
+ )} to prevent naming conflicts.\n\n`
700
+ )
701
+ );
702
+ }
703
+
704
+ function showConflicts(key: string, conflicts: any) {
705
+ for (const [path, files] of conflicts) {
706
+ process.stderr.write(yellow(`Conflict on ${key} : ${red(path)} in files: ${blue(files)} \n`));
707
+ }
708
+ }
709
+
710
+ function filterConflicts(entities: object) {
711
+ return Object.entries(entities).filter(([_, files]) => files.length > 1);
712
+ }
713
+
714
+ function getApiFilename(filePath: string) {
715
+ return path.basename(filePath, path.extname(filePath));
716
+ }
717
+
718
+ function addPrefix(tag: string, tagsPrefix: string) {
719
+ return tagsPrefix ? tagsPrefix + '_' + tag : tag;
720
+ }
721
+
722
+ function formatTags(tags: string[]) {
723
+ return tags.map((tag: string) => ({ name: tag }));
724
+ }
725
+
726
+ function addComponentsPrefix(description: string, componentsPrefix: string) {
727
+ return description.replace(/"(#\/components\/.*?)"/g, (match) => {
728
+ const componentName = path.basename(match);
729
+ return match.replace(componentName, addPrefix(componentName, componentsPrefix));
730
+ });
731
+ }
732
+
733
+ function addSecurityPrefix(security: any, componentsPrefix: string) {
734
+ return componentsPrefix
735
+ ? security?.map((s: any) => {
736
+ const key = Object.keys(s)[0];
737
+ return { [componentsPrefix + '_' + key]: s[key] };
738
+ })
739
+ : security;
740
+ }
741
+
742
+ function getInfoPrefix(info: any, prefixArg: string | undefined, type: string) {
743
+ if (!prefixArg) return '';
744
+ if (!info) exitWithError('Info section is not found in specification. \n');
745
+ if (!info[prefixArg])
746
+ exitWithError(
747
+ `${yellow(`prefix-${type}-with-info-prop`)} argument value is not found in info section. \n`
748
+ );
749
+ if (!isString(info[prefixArg]))
750
+ exitWithError(
751
+ `${yellow(`prefix-${type}-with-info-prop`)} argument value should be string. \n\n`
752
+ );
753
+ if (info[prefixArg].length > 50)
754
+ exitWithError(
755
+ `${yellow(
756
+ `prefix-${type}-with-info-prop`
757
+ )} argument value length should not exceed 50 characters. \n\n`
758
+ );
759
+ return info[prefixArg];
760
+ }
761
+
762
+ async function validateApi(
763
+ document: Document,
764
+ config: StyleguideConfig,
765
+ externalRefResolver: BaseResolver,
766
+ packageVersion: string
767
+ ) {
768
+ try {
769
+ const results = await lintDocument({ document, config, externalRefResolver });
770
+ const fileTotals = getTotals(results);
771
+ formatProblems(results, { format: 'stylish', totals: fileTotals, version: packageVersion });
772
+ printLintTotals(fileTotals, 2);
773
+ } catch (err) {
774
+ handleError(err, document.parsed);
775
+ }
776
+ }
777
+
778
+ function crawl(object: any, visitor: any) {
779
+ if (!isObject(object)) return;
780
+ for (const key of Object.keys(object)) {
781
+ visitor(object, key);
782
+ crawl(object[key], visitor);
783
+ }
784
+ }
785
+
786
+ function replace$Refs(obj: any, componentsPrefix: string) {
787
+ crawl(obj, (node: any) => {
788
+ if (node.$ref && isString(node.$ref) && node.$ref.startsWith(`#/${COMPONENTS}/`)) {
789
+ const name = path.basename(node.$ref);
790
+ node.$ref = node.$ref.replace(name, componentsPrefix + '_' + name);
791
+ } else if (
792
+ node.discriminator &&
793
+ node.discriminator.mapping &&
794
+ isObject(node.discriminator.mapping)
795
+ ) {
796
+ const { mapping } = node.discriminator;
797
+ for (const name of Object.keys(mapping)) {
798
+ if (isString(mapping[name]) && mapping[name].startsWith(`#/${COMPONENTS}/`)) {
799
+ mapping[name] = mapping[name]
800
+ .split('/')
801
+ .map((name: string, i: number, arr: []) => {
802
+ return arr.length - 1 === i && !name.includes(componentsPrefix)
803
+ ? componentsPrefix + '_' + name
804
+ : name;
805
+ })
806
+ .join('/');
807
+ }
808
+ }
809
+ }
810
+ });
811
+ }