@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,106 @@
1
+ import { tmpdir } from 'os';
2
+ import { join } from 'path';
3
+ import { existsSync, writeFileSync, readFileSync, statSync } from 'fs';
4
+ import { compare } from 'semver';
5
+ import fetch from './fetch-with-timeout';
6
+ import { cyan, green, yellow } from 'colorette';
7
+ import { cleanColors } from './utils';
8
+
9
+ export const { version, name } = require('../package.json');
10
+
11
+ const VERSION_CACHE_FILE = 'redocly-cli-version';
12
+ const SPACE_TO_BORDER = 4;
13
+
14
+ const INTERVAL_TO_CHECK = 1000 * 60 * 60 * 12;
15
+ const SHOULD_NOT_NOTIFY =
16
+ process.env.NODE_ENV === 'test' || process.env.CI || !!process.env.LAMBDA_TASK_ROOT;
17
+
18
+ export const notifyUpdateCliVersion = () => {
19
+ if (SHOULD_NOT_NOTIFY) {
20
+ return;
21
+ }
22
+ try {
23
+ const latestVersion = readFileSync(join(tmpdir(), VERSION_CACHE_FILE)).toString();
24
+
25
+ if (isNewVersionAvailable(version, latestVersion)) {
26
+ renderUpdateBanner(version, latestVersion);
27
+ }
28
+ } catch (e) {
29
+ return;
30
+ }
31
+ };
32
+
33
+ const isNewVersionAvailable = (current: string, latest: string) => compare(current, latest) < 0;
34
+
35
+ const getLatestVersion = async (packageName: string): Promise<string | undefined> => {
36
+ const latestUrl = `http://registry.npmjs.org/${packageName}/latest`;
37
+ const response = await fetch(latestUrl);
38
+ if (!response) return;
39
+ const info = await response.json();
40
+ return info.version;
41
+ };
42
+
43
+ export const cacheLatestVersion = () => {
44
+ if (!isNeedToBeCached() || SHOULD_NOT_NOTIFY) {
45
+ return;
46
+ }
47
+
48
+ getLatestVersion(name)
49
+ .then((version) => {
50
+ if (version) {
51
+ const lastCheckFile = join(tmpdir(), VERSION_CACHE_FILE);
52
+ writeFileSync(lastCheckFile, version);
53
+ }
54
+ })
55
+ .catch(() => {});
56
+ };
57
+
58
+ const renderUpdateBanner = (current: string, latest: string) => {
59
+ const messageLines = [
60
+ `A new version of ${cyan('Redocly CLI')} (${green(latest)}) is available.`,
61
+ `Update now: \`${cyan('npm i -g @redocly/cli@latest')}\`.`,
62
+ `Changelog: https://redocly.com/docs/cli/changelog/`,
63
+ ];
64
+ const maxLength = Math.max(...messageLines.map((line) => cleanColors(line).length));
65
+
66
+ const border = yellow('═'.repeat(maxLength + SPACE_TO_BORDER));
67
+
68
+ const banner = `
69
+ ${yellow('╔' + border + '╗')}
70
+ ${yellow('║' + ' '.repeat(maxLength + SPACE_TO_BORDER) + '║')}
71
+ ${messageLines
72
+ .map((line, index) => {
73
+ return getLineWithPadding(maxLength, line, index);
74
+ })
75
+ .join('\n')}
76
+ ${yellow('║' + ' '.repeat(maxLength + SPACE_TO_BORDER) + '║')}
77
+ ${yellow('╚' + border + '╝')}
78
+ `;
79
+
80
+ process.stderr.write(banner);
81
+ };
82
+
83
+ const getLineWithPadding = (maxLength: number, line: string, index: number): string => {
84
+ const padding = ' '.repeat(maxLength - cleanColors(line).length);
85
+ const extraSpaces = index !== 0 ? ' '.repeat(SPACE_TO_BORDER) : '';
86
+ return `${extraSpaces}${yellow('║')} ${line}${padding} ${yellow('║')}`;
87
+ };
88
+
89
+ const isNeedToBeCached = (): boolean => {
90
+ try {
91
+ // Last version from npm is stored in a file in the OS temp folder
92
+ const versionFile = join(tmpdir(), VERSION_CACHE_FILE);
93
+
94
+ if (!existsSync(versionFile)) {
95
+ return true;
96
+ }
97
+
98
+ const now = new Date().getTime();
99
+ const stats = statSync(versionFile);
100
+ const lastCheck = stats.mtime.getTime();
101
+
102
+ return now - lastCheck >= INTERVAL_TO_CHECK;
103
+ } catch (e) {
104
+ return false;
105
+ }
106
+ };
package/src/utils.ts ADDED
@@ -0,0 +1,590 @@
1
+ import fetch from './fetch-with-timeout';
2
+ import { basename, dirname, extname, join, resolve, relative, isAbsolute } from 'path';
3
+ import { blue, gray, green, red, yellow } from 'colorette';
4
+ import { performance } from 'perf_hooks';
5
+ import * as glob from 'glob-promise';
6
+ import * as fs from 'fs';
7
+ import * as readline from 'readline';
8
+ import { Writable } from 'stream';
9
+ import {
10
+ BundleOutputFormat,
11
+ StyleguideConfig,
12
+ ResolveError,
13
+ YamlParseError,
14
+ ResolvedApi,
15
+ parseYaml,
16
+ stringifyYaml,
17
+ isAbsoluteUrl,
18
+ loadConfig,
19
+ RawConfig,
20
+ Region,
21
+ Config,
22
+ Oas3Definition,
23
+ Oas2Definition,
24
+ RedoclyClient,
25
+ } from '@redocly/openapi-core';
26
+ import { Totals, outputExtensions, Entrypoint, ConfigApis, CommandOptions } from './types';
27
+ import { isEmptyObject } from '@redocly/openapi-core/lib/utils';
28
+ import { Arguments } from 'yargs';
29
+ import { version } from './update-version-notifier';
30
+ import { DESTINATION_REGEX } from './commands/push';
31
+
32
+ export async function getFallbackApisOrExit(
33
+ argsApis: string[] | undefined,
34
+ config: ConfigApis
35
+ ): Promise<Entrypoint[]> {
36
+ const { apis } = config;
37
+ const shouldFallbackToAllDefinitions =
38
+ !isNotEmptyArray(argsApis) && apis && Object.keys(apis).length > 0;
39
+ const res = shouldFallbackToAllDefinitions
40
+ ? fallbackToAllDefinitions(apis, config)
41
+ : await expandGlobsInEntrypoints(argsApis!, config);
42
+
43
+ const filteredInvalidEntrypoints = res.filter(({ path }) => !isApiPathValid(path));
44
+ if (isNotEmptyArray(filteredInvalidEntrypoints)) {
45
+ for (const { path } of filteredInvalidEntrypoints) {
46
+ process.stderr.write(
47
+ yellow(`\n${relative(process.cwd(), path)} ${red(`does not exist or is invalid.\n\n`)}`)
48
+ );
49
+ }
50
+ exitWithError('Please provide a valid path.');
51
+ }
52
+ return res;
53
+ }
54
+
55
+ function getConfigDirectory(config: ConfigApis) {
56
+ return config.configFile ? dirname(config.configFile) : process.cwd();
57
+ }
58
+
59
+ function isNotEmptyArray<T>(args?: T[]): boolean {
60
+ return Array.isArray(args) && !!args.length;
61
+ }
62
+
63
+ function isApiPathValid(apiPath: string): string | void {
64
+ if (!apiPath.trim()) {
65
+ exitWithError('Path cannot be empty.');
66
+ return;
67
+ }
68
+ return fs.existsSync(apiPath) || isAbsoluteUrl(apiPath) ? apiPath : undefined;
69
+ }
70
+
71
+ function fallbackToAllDefinitions(
72
+ apis: Record<string, ResolvedApi>,
73
+ config: ConfigApis
74
+ ): Entrypoint[] {
75
+ return Object.entries(apis).map(([alias, { root }]) => ({
76
+ path: isAbsoluteUrl(root) ? root : resolve(getConfigDirectory(config), root),
77
+ alias,
78
+ }));
79
+ }
80
+
81
+ function getAliasOrPath(config: ConfigApis, aliasOrPath: string): Entrypoint {
82
+ return config.apis[aliasOrPath]
83
+ ? { path: config.apis[aliasOrPath]?.root, alias: aliasOrPath }
84
+ : {
85
+ path: aliasOrPath,
86
+ // find alias by path, take the first match
87
+ alias:
88
+ Object.entries(config.apis).find(([_alias, api]) => {
89
+ return resolve(api.root) === resolve(aliasOrPath);
90
+ })?.[0] ?? undefined,
91
+ };
92
+ }
93
+
94
+ async function expandGlobsInEntrypoints(args: string[], config: ConfigApis) {
95
+ return (
96
+ await Promise.all(
97
+ (args as string[]).map(async (aliasOrPath) => {
98
+ return glob.hasMagic(aliasOrPath) && !isAbsoluteUrl(aliasOrPath)
99
+ ? (await glob(aliasOrPath)).map((g: string) => getAliasOrPath(config, g))
100
+ : getAliasOrPath(config, aliasOrPath);
101
+ })
102
+ )
103
+ ).flat();
104
+ }
105
+
106
+ export function getExecutionTime(startedAt: number) {
107
+ return process.env.NODE_ENV === 'test'
108
+ ? '<test>ms'
109
+ : `${Math.ceil(performance.now() - startedAt)}ms`;
110
+ }
111
+
112
+ export function printExecutionTime(commandName: string, startedAt: number, api: string) {
113
+ const elapsed = getExecutionTime(startedAt);
114
+ process.stderr.write(gray(`\n${api}: ${commandName} processed in ${elapsed}\n\n`));
115
+ }
116
+
117
+ export function pathToFilename(path: string, pathSeparator: string) {
118
+ return path
119
+ .replace(/~1/g, '/')
120
+ .replace(/~0/g, '~')
121
+ .replace(/^\//, '')
122
+ .replace(/\//g, pathSeparator);
123
+ }
124
+
125
+ export function escapeLanguageName(lang: string) {
126
+ return lang.replace(/#/g, '_sharp').replace(/\//, '_').replace(/\s/g, '');
127
+ }
128
+
129
+ export function langToExt(lang: string) {
130
+ const langObj: any = {
131
+ php: '.php',
132
+ 'c#': '.cs',
133
+ shell: '.sh',
134
+ curl: '.sh',
135
+ bash: '.sh',
136
+ javascript: '.js',
137
+ js: '.js',
138
+ python: '.py',
139
+ };
140
+ return langObj[lang.toLowerCase()];
141
+ }
142
+
143
+ export class CircularJSONNotSupportedError extends Error {
144
+ constructor(public originalError: Error) {
145
+ super(originalError.message);
146
+ // Set the prototype explicitly.
147
+ Object.setPrototypeOf(this, CircularJSONNotSupportedError.prototype);
148
+ }
149
+ }
150
+
151
+ export function dumpBundle(obj: any, format: BundleOutputFormat, dereference?: boolean): string {
152
+ if (format === 'json') {
153
+ try {
154
+ return JSON.stringify(obj, null, 2);
155
+ } catch (e) {
156
+ if (e.message.indexOf('circular') > -1) {
157
+ throw new CircularJSONNotSupportedError(e);
158
+ }
159
+ throw e;
160
+ }
161
+ } else {
162
+ return stringifyYaml(obj, {
163
+ noRefs: !dereference,
164
+ lineWidth: -1,
165
+ });
166
+ }
167
+ }
168
+
169
+ export function saveBundle(filename: string, output: string) {
170
+ fs.mkdirSync(dirname(filename), { recursive: true });
171
+ fs.writeFileSync(filename, output);
172
+ }
173
+
174
+ export async function promptUser(query: string, hideUserInput = false): Promise<string> {
175
+ return new Promise((resolve) => {
176
+ let output: Writable = process.stdout;
177
+ let isOutputMuted = false;
178
+
179
+ if (hideUserInput) {
180
+ output = new Writable({
181
+ write: (chunk, encoding, callback) => {
182
+ if (!isOutputMuted) {
183
+ process.stdout.write(chunk, encoding);
184
+ }
185
+ callback();
186
+ },
187
+ });
188
+ }
189
+
190
+ const rl = readline.createInterface({
191
+ input: process.stdin,
192
+ output,
193
+ terminal: true,
194
+ historySize: hideUserInput ? 0 : 30,
195
+ });
196
+
197
+ rl.question(`${query}:\n\n `, (answer) => {
198
+ rl.close();
199
+ resolve(answer);
200
+ });
201
+
202
+ isOutputMuted = hideUserInput;
203
+ });
204
+ }
205
+
206
+ export function readYaml(filename: string) {
207
+ return parseYaml(fs.readFileSync(filename, 'utf-8'), { filename });
208
+ }
209
+
210
+ export function writeYaml(data: any, filename: string, noRefs = false) {
211
+ const content = stringifyYaml(data, { noRefs });
212
+
213
+ if (process.env.NODE_ENV === 'test') {
214
+ process.stderr.write(content);
215
+ return;
216
+ }
217
+ fs.mkdirSync(dirname(filename), { recursive: true });
218
+ fs.writeFileSync(filename, content);
219
+ }
220
+
221
+ export function pluralize(label: string, num: number) {
222
+ if (label.endsWith('is')) {
223
+ [label] = label.split(' ');
224
+ return num === 1 ? `${label} is` : `${label}s are`;
225
+ }
226
+ return num === 1 ? `${label}` : `${label}s`;
227
+ }
228
+
229
+ export function handleError(e: Error, ref: string) {
230
+ switch (e.constructor) {
231
+ case HandledError: {
232
+ throw e;
233
+ }
234
+ case ResolveError:
235
+ return exitWithError(`Failed to resolve api definition at ${ref}:\n\n - ${e.message}.`);
236
+ case YamlParseError:
237
+ return exitWithError(`Failed to parse api definition at ${ref}:\n\n - ${e.message}.`);
238
+ // TODO: codeframe
239
+ case CircularJSONNotSupportedError: {
240
+ return exitWithError(
241
+ `Detected circular reference which can't be converted to JSON.\n` +
242
+ `Try to use ${blue('yaml')} output or remove ${blue('--dereferenced')}.`
243
+ );
244
+ }
245
+ case SyntaxError:
246
+ return exitWithError(`Syntax error: ${e.message} ${e.stack?.split('\n\n')?.[0]}`);
247
+ default: {
248
+ exitWithError(`Something went wrong when processing ${ref}:\n\n - ${e.message}.`);
249
+ }
250
+ }
251
+ }
252
+
253
+ export class HandledError extends Error {}
254
+
255
+ export function printLintTotals(totals: Totals, definitionsCount: number) {
256
+ const ignored = totals.ignored
257
+ ? yellow(`${totals.ignored} ${pluralize('problem is', totals.ignored)} explicitly ignored.\n\n`)
258
+ : '';
259
+
260
+ if (totals.errors > 0) {
261
+ process.stderr.write(
262
+ red(
263
+ `❌ Validation failed with ${totals.errors} ${pluralize('error', totals.errors)}${
264
+ totals.warnings > 0
265
+ ? ` and ${totals.warnings} ${pluralize('warning', totals.warnings)}`
266
+ : ''
267
+ }.\n${ignored}`
268
+ )
269
+ );
270
+ } else if (totals.warnings > 0) {
271
+ process.stderr.write(
272
+ green(`Woohoo! Your OpenAPI ${pluralize('definition is', definitionsCount)} valid. 🎉\n`)
273
+ );
274
+ process.stderr.write(
275
+ yellow(`You have ${totals.warnings} ${pluralize('warning', totals.warnings)}.\n${ignored}`)
276
+ );
277
+ } else {
278
+ process.stderr.write(
279
+ green(
280
+ `Woohoo! Your OpenAPI ${pluralize('definition is', definitionsCount)} valid. 🎉\n${ignored}`
281
+ )
282
+ );
283
+ }
284
+
285
+ if (totals.errors > 0) {
286
+ process.stderr.write(
287
+ gray(`run \`redocly lint --generate-ignore-file\` to add all problems to the ignore file.\n`)
288
+ );
289
+ }
290
+
291
+ process.stderr.write('\n');
292
+ }
293
+
294
+ export function printConfigLintTotals(totals: Totals): void {
295
+ if (totals.errors > 0) {
296
+ process.stderr.write(
297
+ red(
298
+ `❌ Your config has ${totals.errors} ${pluralize('error', totals.errors)}${
299
+ totals.warnings > 0
300
+ ? ` and ${totals.warnings} ${pluralize('warning', totals.warnings)}`
301
+ : ''
302
+ }.\n`
303
+ )
304
+ );
305
+ } else if (totals.warnings > 0) {
306
+ process.stderr.write(
307
+ yellow(`You have ${totals.warnings} ${pluralize('warning', totals.warnings)}.\n`)
308
+ );
309
+ }
310
+ }
311
+
312
+ export function getOutputFileName(
313
+ entrypoint: string,
314
+ entries: number,
315
+ output?: string,
316
+ ext?: BundleOutputFormat
317
+ ) {
318
+ if (!output) {
319
+ return { outputFile: 'stdout', ext: ext || 'yaml' };
320
+ }
321
+
322
+ let outputFile = output;
323
+ if (entries > 1) {
324
+ ext = ext || (extname(entrypoint).substring(1) as BundleOutputFormat);
325
+ if (!outputExtensions.includes(ext as any)) {
326
+ throw new Error(`Invalid file extension: ${ext}.`);
327
+ }
328
+ outputFile = join(output, basename(entrypoint, extname(entrypoint))) + '.' + ext;
329
+ } else {
330
+ if (output) {
331
+ ext = ext || (extname(output).substring(1) as BundleOutputFormat);
332
+ }
333
+ ext = ext || (extname(entrypoint).substring(1) as BundleOutputFormat);
334
+ if (!outputExtensions.includes(ext as any)) {
335
+ throw new Error(`Invalid file extension: ${ext}.`);
336
+ }
337
+ outputFile = join(dirname(outputFile), basename(outputFile, extname(outputFile))) + '.' + ext;
338
+ }
339
+ return { outputFile, ext };
340
+ }
341
+
342
+ export function printUnusedWarnings(config: StyleguideConfig) {
343
+ const { preprocessors, rules, decorators } = config.getUnusedRules();
344
+ if (rules.length) {
345
+ process.stderr.write(
346
+ yellow(
347
+ `[WARNING] Unused rules found in ${blue(config.configFile || '')}: ${rules.join(', ')}.\n`
348
+ )
349
+ );
350
+ }
351
+ if (preprocessors.length) {
352
+ process.stderr.write(
353
+ yellow(
354
+ `[WARNING] Unused preprocessors found in ${blue(
355
+ config.configFile || ''
356
+ )}: ${preprocessors.join(', ')}.\n`
357
+ )
358
+ );
359
+ }
360
+ if (decorators.length) {
361
+ process.stderr.write(
362
+ yellow(
363
+ `[WARNING] Unused decorators found in ${blue(config.configFile || '')}: ${decorators.join(
364
+ ', '
365
+ )}.\n`
366
+ )
367
+ );
368
+ }
369
+
370
+ if (rules.length || preprocessors.length) {
371
+ process.stderr.write(`Check the spelling and verify the added plugin prefix.\n`);
372
+ }
373
+ }
374
+
375
+ export function exitWithError(message: string) {
376
+ process.stderr.write(red(message) + '\n\n');
377
+ throw new HandledError(message);
378
+ }
379
+
380
+ /**
381
+ * Checks if dir is subdir of parent
382
+ */
383
+ export function isSubdir(parent: string, dir: string): boolean {
384
+ const relativePath = relative(parent, dir);
385
+ return !!relativePath && !/^..($|\/)/.test(relativePath) && !isAbsolute(relativePath);
386
+ }
387
+
388
+ export async function loadConfigAndHandleErrors(
389
+ options: {
390
+ configPath?: string;
391
+ customExtends?: string[];
392
+ processRawConfig?: (rawConfig: RawConfig) => void | Promise<void>;
393
+ files?: string[];
394
+ region?: Region;
395
+ } = {}
396
+ ): Promise<Config | void> {
397
+ try {
398
+ return await loadConfig(options);
399
+ } catch (e) {
400
+ handleError(e, '');
401
+ }
402
+ }
403
+
404
+ export function sortTopLevelKeysForOas(
405
+ document: Oas3Definition | Oas2Definition
406
+ ): Oas3Definition | Oas2Definition {
407
+ if ('swagger' in document) {
408
+ return sortOas2Keys(document);
409
+ }
410
+ return sortOas3Keys(document as Oas3Definition);
411
+ }
412
+
413
+ function sortOas2Keys(document: Oas2Definition): Oas2Definition {
414
+ const orderedKeys = [
415
+ 'swagger',
416
+ 'info',
417
+ 'host',
418
+ 'basePath',
419
+ 'schemes',
420
+ 'consumes',
421
+ 'produces',
422
+ 'security',
423
+ 'tags',
424
+ 'externalDocs',
425
+ 'paths',
426
+ 'definitions',
427
+ 'parameters',
428
+ 'responses',
429
+ 'securityDefinitions',
430
+ ];
431
+ const result: any = {};
432
+ for (const key of orderedKeys as (keyof Oas2Definition)[]) {
433
+ if (document.hasOwnProperty(key)) {
434
+ result[key] = document[key];
435
+ }
436
+ }
437
+ // merge any other top-level keys (e.g. vendor extensions)
438
+ return Object.assign(result, document);
439
+ }
440
+ function sortOas3Keys(document: Oas3Definition): Oas3Definition {
441
+ const orderedKeys = [
442
+ 'openapi',
443
+ 'info',
444
+ 'jsonSchemaDialect',
445
+ 'servers',
446
+ 'security',
447
+ 'tags',
448
+ 'externalDocs',
449
+ 'paths',
450
+ 'webhooks',
451
+ 'x-webhooks',
452
+ 'components',
453
+ ];
454
+ const result: any = {};
455
+ for (const key of orderedKeys as (keyof Oas3Definition)[]) {
456
+ if (document.hasOwnProperty(key)) {
457
+ result[key] = document[key];
458
+ }
459
+ }
460
+ // merge any other top-level keys (e.g. vendor extensions)
461
+ return Object.assign(result, document);
462
+ }
463
+
464
+ export function checkIfRulesetExist(rules: typeof StyleguideConfig.prototype.rules) {
465
+ const ruleset = {
466
+ ...rules.oas2,
467
+ ...rules.oas3_0,
468
+ ...rules.oas3_0,
469
+ };
470
+
471
+ if (isEmptyObject(ruleset)) {
472
+ exitWithError(
473
+ '⚠️ No rules were configured. Learn how to configure rules: https://redocly.com/docs/cli/rules/'
474
+ );
475
+ }
476
+ }
477
+
478
+ export function cleanColors(input: string): string {
479
+ // eslint-disable-next-line no-control-regex
480
+ return input.replace(/\x1b\[\d+m/g, '');
481
+ }
482
+
483
+ export async function sendTelemetry(
484
+ argv: Arguments | undefined,
485
+ exit_code: ExitCode,
486
+ has_config: boolean | undefined
487
+ ): Promise<void> {
488
+ try {
489
+ if (!argv) {
490
+ return;
491
+ }
492
+ const {
493
+ _: [command],
494
+ $0: _,
495
+ ...args
496
+ } = argv;
497
+ const event_time = new Date().toISOString();
498
+ const redoclyClient = new RedoclyClient();
499
+ const logged_in = redoclyClient.hasTokens();
500
+ const data: Analytics = {
501
+ event: 'cli_command',
502
+ event_time,
503
+ logged_in,
504
+ command,
505
+ arguments: cleanArgs(args),
506
+ node_version: process.version,
507
+ version,
508
+ exit_code,
509
+ environment: process.env.REDOCLY_ENVIRONMENT,
510
+ environment_ci: process.env.CI,
511
+ raw_input: cleanRawInput(process.argv.slice(2)),
512
+ has_config,
513
+ };
514
+ await fetch(`https://api.redocly.com/registry/telemetry/cli`, {
515
+ method: 'POST',
516
+ headers: {
517
+ 'content-type': 'application/json',
518
+ },
519
+ body: JSON.stringify(data),
520
+ });
521
+ } catch (err) {
522
+ // Do nothing.
523
+ }
524
+ }
525
+
526
+ export type ExitCode = 0 | 1 | 2;
527
+
528
+ export type Analytics = {
529
+ event: string;
530
+ event_time: string;
531
+ logged_in: boolean;
532
+ command: string | number;
533
+ arguments: Record<string, unknown>;
534
+ node_version: string;
535
+ version: string;
536
+ exit_code: ExitCode;
537
+ environment?: string;
538
+ environment_ci?: string;
539
+ raw_input: string;
540
+ has_config?: boolean;
541
+ };
542
+
543
+ function isFile(value: string) {
544
+ return fs.existsSync(value) && fs.statSync(value).isFile();
545
+ }
546
+
547
+ function isDirectory(value: string) {
548
+ return fs.existsSync(value) && fs.statSync(value).isDirectory();
549
+ }
550
+
551
+ function cleanString(value?: string): string | undefined {
552
+ if (!value) {
553
+ return value;
554
+ }
555
+ if (isAbsoluteUrl(value)) {
556
+ return value.split('://')[0] + '://url';
557
+ }
558
+ if (isFile(value)) {
559
+ return value.replace(/.+\.([^.]+)$/, (_, ext) => 'file-' + ext);
560
+ }
561
+ if (isDirectory(value)) {
562
+ return 'folder';
563
+ }
564
+ if (DESTINATION_REGEX.test(value)) {
565
+ return value.startsWith('@') ? '@organization/api-name@api-version' : 'api-name@api-version';
566
+ }
567
+ return value;
568
+ }
569
+
570
+ export function cleanArgs(args: CommandOptions) {
571
+ const keysToClean = ['organization', 'o'];
572
+
573
+ const result: Record<string, unknown> = {};
574
+ for (const [key, value] of Object.entries(args)) {
575
+ if (keysToClean.includes(key)) {
576
+ result[key] = '***';
577
+ } else if (typeof value === 'string') {
578
+ result[key] = cleanString(value);
579
+ } else if (Array.isArray(value)) {
580
+ result[key] = value.map(cleanString);
581
+ } else {
582
+ result[key] = value;
583
+ }
584
+ }
585
+ return result;
586
+ }
587
+
588
+ export function cleanRawInput(argv: string[]) {
589
+ return argv.map((entry) => entry.split('=').map(cleanString).join('=')).join(' ');
590
+ }