@redocly/openapi-core 1.0.0-beta.106 → 1.0.0-beta.109

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 (193) hide show
  1. package/lib/benchmark/benches/lint-with-top-level-rule-report.bench.js +0 -1
  2. package/lib/benchmark/benches/resolve-with-no-external.bench.js +1 -1
  3. package/lib/bundle.d.ts +1 -1
  4. package/lib/bundle.js +9 -6
  5. package/lib/config/all.js +5 -3
  6. package/lib/config/config-resolvers.js +32 -14
  7. package/lib/config/config.d.ts +3 -5
  8. package/lib/config/config.js +7 -4
  9. package/lib/config/load.d.ts +7 -0
  10. package/lib/config/load.js +14 -6
  11. package/lib/config/minimal.js +7 -4
  12. package/lib/config/recommended.js +7 -4
  13. package/lib/config/rules.d.ts +1 -1
  14. package/lib/config/rules.js +1 -1
  15. package/lib/config/types.d.ts +7 -0
  16. package/lib/config/utils.d.ts +2 -2
  17. package/lib/config/utils.js +49 -11
  18. package/lib/decorators/common/registry-dependencies.js +2 -2
  19. package/lib/env.d.ts +3 -0
  20. package/lib/env.js +8 -0
  21. package/lib/format/codeframes.js +16 -10
  22. package/lib/format/format.d.ts +1 -1
  23. package/lib/format/format.js +49 -26
  24. package/lib/index.d.ts +5 -5
  25. package/lib/index.js +3 -1
  26. package/lib/js-yaml/index.js +1 -0
  27. package/lib/lint.js +2 -2
  28. package/lib/logger.d.ts +10 -0
  29. package/lib/logger.js +31 -0
  30. package/lib/output.d.ts +3 -0
  31. package/lib/output.js +9 -0
  32. package/lib/redocly/index.js +10 -9
  33. package/lib/redocly/registry-api-types.d.ts +28 -30
  34. package/lib/redocly/registry-api.d.ts +3 -3
  35. package/lib/redocly/registry-api.js +7 -1
  36. package/lib/ref-utils.js +2 -1
  37. package/lib/resolve.d.ts +1 -1
  38. package/lib/resolve.js +4 -2
  39. package/lib/rules/ajv.d.ts +1 -1
  40. package/lib/rules/ajv.js +7 -7
  41. package/lib/rules/common/assertions/asserts.js +4 -4
  42. package/lib/rules/common/assertions/index.js +1 -1
  43. package/lib/rules/common/no-ambiguous-paths.js +1 -1
  44. package/lib/rules/common/no-identical-paths.js +1 -1
  45. package/lib/rules/common/no-invalid-parameter-examples.js +3 -3
  46. package/lib/rules/common/no-invalid-schema-examples.js +3 -3
  47. package/lib/rules/common/operation-2xx-response.js +1 -1
  48. package/lib/rules/common/operation-4xx-response.js +1 -1
  49. package/lib/rules/common/operation-operationId.js +1 -1
  50. package/lib/rules/common/operation-tag-defined.js +1 -1
  51. package/lib/rules/common/path-not-include-query.js +1 -1
  52. package/lib/rules/common/security-defined.d.ts +2 -0
  53. package/lib/rules/common/{operation-security-defined.js → security-defined.js} +19 -5
  54. package/lib/rules/common/spec.js +14 -3
  55. package/lib/rules/common/tags-alphabetical.js +1 -1
  56. package/lib/rules/oas2/index.d.ts +1 -1
  57. package/lib/rules/oas2/index.js +2 -2
  58. package/lib/rules/oas2/remove-unused-components.js +3 -3
  59. package/lib/rules/oas2/request-mime-type.js +1 -1
  60. package/lib/rules/oas2/response-mime-type.js +1 -1
  61. package/lib/rules/oas3/index.js +8 -4
  62. package/lib/rules/oas3/no-empty-servers.js +1 -1
  63. package/lib/rules/oas3/no-invalid-media-type-examples.js +2 -2
  64. package/lib/rules/oas3/no-server-variables-empty-enum.d.ts +2 -0
  65. package/lib/rules/oas3/{no-servers-empty-enum.js → no-server-variables-empty-enum.js} +5 -5
  66. package/lib/rules/oas3/no-unused-components.js +2 -2
  67. package/lib/rules/oas3/operation-4xx-problem-details-rfc7807.d.ts +5 -0
  68. package/lib/rules/oas3/operation-4xx-problem-details-rfc7807.js +36 -0
  69. package/lib/rules/oas3/remove-unused-components.js +4 -4
  70. package/lib/rules/oas3/request-mime-type.js +1 -1
  71. package/lib/rules/oas3/response-mime-type.js +1 -1
  72. package/lib/rules/oas3/spec-components-invalid-map-name.d.ts +2 -0
  73. package/lib/rules/oas3/spec-components-invalid-map-name.js +46 -0
  74. package/lib/rules/other/stats.d.ts +2 -2
  75. package/lib/rules/other/stats.js +2 -2
  76. package/lib/rules/utils.d.ts +3 -2
  77. package/lib/rules/utils.js +16 -4
  78. package/lib/types/oas2.js +5 -5
  79. package/lib/types/oas3.js +27 -20
  80. package/lib/types/oas3_1.js +3 -3
  81. package/lib/types/redocly-yaml.js +47 -56
  82. package/lib/utils.d.ts +6 -1
  83. package/lib/utils.js +24 -7
  84. package/lib/visitors.d.ts +12 -12
  85. package/lib/visitors.js +15 -3
  86. package/lib/walk.d.ts +2 -1
  87. package/lib/walk.js +6 -3
  88. package/package.json +2 -2
  89. package/src/__tests__/__snapshots__/bundle.test.ts.snap +3 -3
  90. package/src/__tests__/fixtures/extension.js +3 -3
  91. package/src/__tests__/format.test.ts +76 -0
  92. package/src/__tests__/lint.test.ts +106 -131
  93. package/src/__tests__/logger-browser.test.ts +53 -0
  94. package/src/__tests__/logger.test.ts +47 -0
  95. package/src/__tests__/output-browser.test.ts +18 -0
  96. package/src/__tests__/output.test.ts +15 -0
  97. package/src/__tests__/resolve-http.test.ts +1 -1
  98. package/src/__tests__/resolve.test.ts +9 -9
  99. package/src/__tests__/utils-browser.test.ts +11 -0
  100. package/src/__tests__/utils.test.ts +7 -0
  101. package/src/__tests__/walk.test.ts +78 -10
  102. package/src/benchmark/benches/lint-with-top-level-rule-report.bench.ts +0 -1
  103. package/src/benchmark/benches/resolve-with-no-external.bench.ts +1 -1
  104. package/src/bundle.ts +10 -7
  105. package/src/config/__tests__/__snapshots__/config-resolvers.test.ts.snap +12 -6
  106. package/src/config/__tests__/config.test.ts +35 -0
  107. package/src/config/__tests__/fixtures/plugin-config.yaml +2 -3
  108. package/src/config/__tests__/fixtures/resolve-config/api/nested-config.yaml +11 -12
  109. package/src/config/__tests__/fixtures/resolve-config/local-config-with-circular.yaml +7 -8
  110. package/src/config/__tests__/fixtures/resolve-config/local-config-with-file.yaml +18 -19
  111. package/src/config/__tests__/fixtures/resolve-config/local-config.yaml +9 -10
  112. package/src/config/__tests__/fixtures/resolve-remote-configs/nested-remote-config.yaml +3 -4
  113. package/src/config/__tests__/fixtures/resolve-remote-configs/remote-config.yaml +4 -5
  114. package/src/config/__tests__/load.test.ts +76 -1
  115. package/src/config/__tests__/utils.test.ts +64 -4
  116. package/src/config/all.ts +5 -3
  117. package/src/config/config-resolvers.ts +45 -19
  118. package/src/config/config.ts +10 -8
  119. package/src/config/load.ts +31 -7
  120. package/src/config/minimal.ts +7 -4
  121. package/src/config/recommended.ts +7 -4
  122. package/src/config/rules.ts +2 -2
  123. package/src/config/types.ts +11 -0
  124. package/src/config/utils.ts +115 -25
  125. package/src/decorators/common/registry-dependencies.ts +2 -2
  126. package/src/env.ts +5 -0
  127. package/src/format/codeframes.ts +15 -9
  128. package/src/format/format.ts +59 -34
  129. package/src/index.ts +6 -4
  130. package/src/js-yaml/index.ts +1 -0
  131. package/src/lint.ts +2 -2
  132. package/src/logger.ts +34 -0
  133. package/src/output.ts +7 -0
  134. package/src/redocly/index.ts +7 -4
  135. package/src/redocly/registry-api-types.ts +27 -29
  136. package/src/redocly/registry-api.ts +18 -7
  137. package/src/ref-utils.ts +2 -1
  138. package/src/resolve.ts +7 -5
  139. package/src/rules/__tests__/utils.test.ts +39 -1
  140. package/src/rules/ajv.ts +7 -7
  141. package/src/rules/common/__tests__/no-enum-type-mismatch.test.ts +1 -0
  142. package/src/rules/common/__tests__/operation-2xx-response.test.ts +1 -1
  143. package/src/rules/common/__tests__/operation-4xx-response.test.ts +26 -3
  144. package/src/rules/common/__tests__/security-defined.test.ts +175 -0
  145. package/src/rules/common/__tests__/spec.test.ts +79 -0
  146. package/src/rules/common/assertions/__tests__/utils.test.ts +2 -2
  147. package/src/rules/common/assertions/asserts.ts +4 -4
  148. package/src/rules/common/assertions/index.ts +1 -1
  149. package/src/rules/common/no-ambiguous-paths.ts +1 -1
  150. package/src/rules/common/no-identical-paths.ts +1 -1
  151. package/src/rules/common/no-invalid-parameter-examples.ts +4 -4
  152. package/src/rules/common/no-invalid-schema-examples.ts +4 -4
  153. package/src/rules/common/operation-2xx-response.ts +1 -1
  154. package/src/rules/common/operation-4xx-response.ts +1 -1
  155. package/src/rules/common/operation-operationId.ts +1 -1
  156. package/src/rules/common/operation-tag-defined.ts +1 -1
  157. package/src/rules/common/path-not-include-query.ts +1 -1
  158. package/src/rules/common/{operation-security-defined.ts → security-defined.ts} +20 -5
  159. package/src/rules/common/spec.ts +17 -3
  160. package/src/rules/common/tags-alphabetical.ts +1 -1
  161. package/src/rules/oas2/index.ts +2 -2
  162. package/src/rules/oas2/remove-unused-components.ts +3 -3
  163. package/src/rules/oas2/request-mime-type.ts +1 -1
  164. package/src/rules/oas2/response-mime-type.ts +1 -1
  165. package/src/rules/oas3/__tests__/no-empty-enum-servers.com.test.ts +16 -16
  166. package/src/rules/oas3/__tests__/no-invalid-media-type-examples.test.ts +5 -5
  167. package/src/rules/oas3/__tests__/operation-4xx-problem-details-rfc7807.test.ts +145 -0
  168. package/src/rules/oas3/__tests__/spec/spec.test.ts +10 -0
  169. package/src/rules/oas3/__tests__/spec-components-invalid-map-name.test.ts +217 -0
  170. package/src/rules/oas3/index.ts +8 -4
  171. package/src/rules/oas3/no-empty-servers.ts +1 -1
  172. package/src/rules/oas3/no-invalid-media-type-examples.ts +3 -3
  173. package/src/rules/oas3/{no-servers-empty-enum.ts → no-server-variables-empty-enum.ts} +3 -3
  174. package/src/rules/oas3/no-unused-components.ts +2 -2
  175. package/src/rules/oas3/operation-4xx-problem-details-rfc7807.ts +36 -0
  176. package/src/rules/oas3/remove-unused-components.ts +5 -5
  177. package/src/rules/oas3/request-mime-type.ts +1 -1
  178. package/src/rules/oas3/response-mime-type.ts +1 -1
  179. package/src/rules/oas3/spec-components-invalid-map-name.ts +53 -0
  180. package/src/rules/other/stats.ts +2 -2
  181. package/src/rules/utils.ts +17 -3
  182. package/src/types/index.ts +2 -2
  183. package/src/types/oas2.ts +5 -5
  184. package/src/types/oas3.ts +27 -20
  185. package/src/types/oas3_1.ts +3 -3
  186. package/src/types/redocly-yaml.ts +53 -41
  187. package/src/utils.ts +31 -4
  188. package/src/visitors.ts +34 -18
  189. package/src/walk.ts +15 -11
  190. package/tsconfig.tsbuildinfo +1 -1
  191. package/lib/rules/common/operation-security-defined.d.ts +0 -2
  192. package/lib/rules/oas3/no-servers-empty-enum.d.ts +0 -2
  193. package/src/rules/common/__tests__/operation-security-defined.test.ts +0 -69
package/src/env.ts ADDED
@@ -0,0 +1,5 @@
1
+ export const isBrowser =
2
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
3
+ // @ts-ignore
4
+ typeof window !== 'undefined' || typeof self !== 'undefined' || typeof process === 'undefined'; // main and worker thread
5
+ export const env = isBrowser ? {} : process.env || {};
@@ -1,7 +1,7 @@
1
- import { gray, red, options as colorOptions } from 'colorette';
2
1
  import * as yamlAst from 'yaml-ast-parser';
3
2
  import { unescapePointer } from '../ref-utils';
4
3
  import { LineColLocationObject, Loc, LocationObject } from '../walk';
4
+ import { colorize, colorOptions } from '../logger';
5
5
 
6
6
  type YAMLMapping = yamlAst.YAMLMapping & { kind: yamlAst.Kind.MAPPING };
7
7
  type YAMLMap = yamlAst.YamlMap & { kind: yamlAst.Kind.MAP };
@@ -36,17 +36,23 @@ export function getCodeframe(location: LineColLocationObject, color: boolean) {
36
36
  if (skipLines > 0 && i >= endLineNum - skipLines) break;
37
37
  const line = lines[i - 1] || '';
38
38
  if (line !== '') currentPad = padSize(line);
39
- let startIdx = i === startLineNum ? start.col - 1 : currentPad;
40
- let endIdx = i === endLineNum ? end.col - 1 : line.length;
39
+ const startIdx = i === startLineNum ? start.col - 1 : currentPad;
40
+ const endIdx = i === endLineNum ? end.col - 1 : line.length;
41
41
 
42
- prefixedLines.push([`${i}`, markLine(line, startIdx, endIdx, red)]);
42
+ prefixedLines.push([`${i}`, markLine(line, startIdx, endIdx, colorize.red)]);
43
43
  if (!color) prefixedLines.push(['', underlineLine(line, startIdx, endIdx)]);
44
44
  }
45
45
 
46
46
  if (skipLines > 0) {
47
- prefixedLines.push([`…`, `${whitespace(currentPad)}${gray(`< ${skipLines} more lines >`)}`]);
47
+ prefixedLines.push([
48
+ `…`,
49
+ `${whitespace(currentPad)}${colorize.gray(`< ${skipLines} more lines >`)}`,
50
+ ]);
48
51
  // print last line
49
- prefixedLines.push([`${endLineNum}`, markLine(lines[endLineNum - 1], -1, end.col - 1, red)]);
52
+ prefixedLines.push([
53
+ `${endLineNum}`,
54
+ markLine(lines[endLineNum - 1], -1, end.col - 1, colorize.red),
55
+ ]);
50
56
 
51
57
  if (!color) prefixedLines.push(['', underlineLine(lines[endLineNum - 1], -1, end.col - 1)]);
52
58
  }
@@ -63,7 +69,7 @@ export function getCodeframe(location: LineColLocationObject, color: boolean) {
63
69
  line: string,
64
70
  startIdx: number = -1,
65
71
  endIdx: number = +Infinity,
66
- variant = gray
72
+ variant = colorize.gray
67
73
  ) {
68
74
  if (!color) return line;
69
75
  if (!line) return line;
@@ -90,7 +96,7 @@ function printPrefixedLines(lines: [string, string][]): string {
90
96
  return existingLines
91
97
  .map(
92
98
  ([prefix, line]) =>
93
- gray(leftPad(padLen, prefix) + ' |') +
99
+ colorize.gray(leftPad(padLen, prefix) + ' |') +
94
100
  (line ? ' ' + limitLineLength(line.substring(dedentLen)) : '')
95
101
  )
96
102
  .join('\n');
@@ -99,7 +105,7 @@ function printPrefixedLines(lines: [string, string][]): string {
99
105
  function limitLineLength(line: string, maxLen: number = MAX_LINE_LENGTH) {
100
106
  const overflowLen = line.length - maxLen;
101
107
  if (overflowLen > 0) {
102
- const charsMoreText = gray(`...<${overflowLen} chars>`);
108
+ const charsMoreText = colorize.gray(`...<${overflowLen} chars>`);
103
109
  return line.substring(0, maxLen - charsMoreText.length) + charsMoreText;
104
110
  } else {
105
111
  return line;
@@ -1,20 +1,12 @@
1
1
  import * as path from 'path';
2
- import {
3
- options as colorOptions,
4
- gray,
5
- blue,
6
- bgRed,
7
- bgYellow,
8
- black,
9
- yellow,
10
- red,
11
- } from 'colorette';
2
+ import { colorOptions, colorize, logger } from '../logger';
3
+ import { output } from '../output';
12
4
 
13
5
  const coreVersion = require('../../package.json').version;
14
6
 
15
7
  import { NormalizedProblem, ProblemSeverity, LineColLocationObject, LocationObject } from '../walk';
16
8
  import { getCodeframe, getLineColLocation } from './codeframes';
17
- import { env } from '../config';
9
+ import { env } from '../env';
18
10
 
19
11
  export type Totals = {
20
12
  errors: number;
@@ -27,13 +19,13 @@ const ERROR_MESSAGE = {
27
19
  };
28
20
 
29
21
  const BG_COLORS = {
30
- warn: (str: string) => bgYellow(black(str)),
31
- error: bgRed,
22
+ warn: (str: string) => colorize.bgYellow(colorize.black(str)),
23
+ error: colorize.bgRed,
32
24
  };
33
25
 
34
26
  const COLORS = {
35
- warn: yellow,
36
- error: red,
27
+ warn: colorize.yellow,
28
+ error: colorize.red,
37
29
  };
38
30
 
39
31
  const SEVERITY_NAMES = {
@@ -52,7 +44,13 @@ function severityToNumber(severity: ProblemSeverity) {
52
44
  return severity === 'error' ? 1 : 2;
53
45
  }
54
46
 
55
- export type OutputFormat = 'codeframe' | 'stylish' | 'json' | 'checkstyle' | 'codeclimate';
47
+ export type OutputFormat =
48
+ | 'codeframe'
49
+ | 'stylish'
50
+ | 'json'
51
+ | 'checkstyle'
52
+ | 'codeclimate'
53
+ | 'summary';
56
54
 
57
55
  export function getTotals(problems: (NormalizedProblem & { ignored?: boolean })[]): Totals {
58
56
  let errors = 0;
@@ -114,7 +112,7 @@ export function formatProblems(
114
112
  case 'codeframe':
115
113
  for (let i = 0; i < problems.length; i++) {
116
114
  const problem = problems[i];
117
- process.stderr.write(`${formatCodeframe(problem, i)}\n`);
115
+ logger.info(`${formatCodeframe(problem, i)}\n`);
118
116
  }
119
117
  break;
120
118
  case 'stylish': {
@@ -122,40 +120,43 @@ export function formatProblems(
122
120
  for (const [file, { ruleIdPad, locationPad: positionPad, fileProblems }] of Object.entries(
123
121
  groupedByFile
124
122
  )) {
125
- process.stderr.write(`${blue(path.relative(cwd, file))}:\n`);
123
+ logger.info(`${colorize.blue(path.relative(cwd, file))}:\n`);
126
124
 
127
125
  for (let i = 0; i < fileProblems.length; i++) {
128
126
  const problem = fileProblems[i];
129
- process.stderr.write(`${formatStylish(problem, positionPad, ruleIdPad)}\n`);
127
+ logger.info(`${formatStylish(problem, positionPad, ruleIdPad)}\n`);
130
128
  }
131
129
 
132
- process.stderr.write('\n');
130
+ logger.info('\n');
133
131
  }
134
132
  break;
135
133
  }
136
134
  case 'checkstyle': {
137
135
  const groupedByFile = groupByFiles(problems);
138
136
 
139
- process.stdout.write('<?xml version="1.0" encoding="UTF-8"?>\n');
140
- process.stdout.write('<checkstyle version="4.3">\n');
137
+ output.write('<?xml version="1.0" encoding="UTF-8"?>\n');
138
+ output.write('<checkstyle version="4.3">\n');
141
139
 
142
140
  for (const [file, { fileProblems }] of Object.entries(groupedByFile)) {
143
- process.stdout.write(`<file name="${xmlEscape(path.relative(cwd, file))}">\n`);
141
+ output.write(`<file name="${xmlEscape(path.relative(cwd, file))}">\n`);
144
142
  fileProblems.forEach(formatCheckstyle);
145
- process.stdout.write(`</file>\n`);
143
+ output.write(`</file>\n`);
146
144
  }
147
145
 
148
- process.stdout.write(`</checkstyle>\n`);
146
+ output.write(`</checkstyle>\n`);
149
147
  break;
150
148
  }
151
149
  case 'codeclimate':
152
150
  outputForCodeClimate();
153
151
  break;
152
+ case 'summary':
153
+ formatSummary(problems);
154
+ break;
154
155
  }
155
156
 
156
157
  if (totalProblems - ignoredProblems > maxProblems) {
157
- process.stderr.write(
158
- `< ... ${totalProblems - maxProblems} more problems hidden > ${gray(
158
+ logger.info(
159
+ `< ... ${totalProblems - maxProblems} more problems hidden > ${colorize.gray(
159
160
  'increase with `--max-problems N`'
160
161
  )}\n`
161
162
  );
@@ -177,7 +178,7 @@ export function formatProblems(
177
178
  fingerprint: `${p.ruleId}${p.location.length > 0 ? '-' + p.location[0].pointer : ''}`,
178
179
  };
179
180
  });
180
- process.stdout.write(JSON.stringify(issues, null, 2));
181
+ output.write(JSON.stringify(issues, null, 2));
181
182
  }
182
183
 
183
184
  function outputJSON() {
@@ -185,7 +186,7 @@ export function formatProblems(
185
186
  totals,
186
187
  version,
187
188
  problems: problems.map((p) => {
188
- let problem = {
189
+ const problem = {
189
190
  ...p,
190
191
  location: p.location.map((location: any) => ({
191
192
  ...location,
@@ -211,7 +212,7 @@ export function formatProblems(
211
212
  return problem;
212
213
  }),
213
214
  };
214
- process.stdout.write(JSON.stringify(resultObject, null, 2));
215
+ output.write(JSON.stringify(resultObject, null, 2));
215
216
  }
216
217
 
217
218
  function getBgColor(problem: NormalizedProblem) {
@@ -227,7 +228,7 @@ export function formatProblems(
227
228
  const location = problem.location[0]; // TODO: support multiple locations
228
229
  const relativePath = path.relative(cwd, location.source.absoluteRef);
229
230
  const loc = getLineColLocation(location);
230
- const atPointer = location.pointer ? gray(`at ${location.pointer}`) : '';
231
+ const atPointer = location.pointer ? colorize.gray(`at ${location.pointer}`) : '';
231
232
  const fileWithLoc = `${relativePath}:${loc.start.line}:${loc.start.col}`;
232
233
  return (
233
234
  `[${idx + 1}] ${bgColor(fileWithLoc)} ${atPointer}\n\n` +
@@ -236,7 +237,9 @@ export function formatProblems(
236
237
  getCodeframe(loc, color) +
237
238
  '\n\n' +
238
239
  formatFrom(cwd, problem.from) +
239
- `${SEVERITY_NAMES[problem.severity]} was generated by the ${blue(problem.ruleId)} rule.\n\n`
240
+ `${SEVERITY_NAMES[problem.severity]} was generated by the ${colorize.blue(
241
+ problem.ruleId
242
+ )} rule.\n\n`
240
243
  );
241
244
  }
242
245
 
@@ -257,19 +260,40 @@ export function formatProblems(
257
260
  const severity = problem.severity == 'warn' ? 'warning' : 'error';
258
261
  const message = xmlEscape(problem.message);
259
262
  const source = xmlEscape(problem.ruleId);
260
- process.stdout.write(
263
+ output.write(
261
264
  `<error line="${line}" column="${col}" severity="${severity}" message="${message}" source="${source}" />\n`
262
265
  );
263
266
  }
264
267
  }
265
268
 
269
+ function formatSummary(problems: NormalizedProblem[]): void {
270
+ const counts: Record<string, { count: number; severity: ProblemSeverity }> = {};
271
+ for (const problem of problems) {
272
+ counts[problem.ruleId] = counts[problem.ruleId] || { count: 0, severity: problem.severity };
273
+ counts[problem.ruleId].count++;
274
+ }
275
+ const sorted = Object.entries(counts).sort(([, a], [, b]) => {
276
+ const severityDiff = severityToNumber(a.severity) - severityToNumber(b.severity);
277
+ return severityDiff || b.count - a.count;
278
+ });
279
+
280
+ for (const [ruleId, info] of sorted) {
281
+ const color = COLORS[info.severity];
282
+ const severityName = color(SEVERITY_NAMES[info.severity].toLowerCase().padEnd(7));
283
+ logger.info(`${severityName} ${ruleId}: ${info.count}\n`);
284
+ }
285
+
286
+ logger.info('\n');
287
+ }
288
+
266
289
  function formatFrom(cwd: string, location?: LocationObject) {
267
290
  if (!location) return '';
268
291
  const relativePath = path.relative(cwd, location.source.absoluteRef);
269
292
  const loc = getLineColLocation(location);
270
293
  const fileWithLoc = `${relativePath}:${loc.start.line}:${loc.start.col}`;
294
+ const atPointer = location.pointer ? colorize.gray(`at ${location.pointer}`) : '';
271
295
 
272
- return `referenced from ${blue(fileWithLoc)}\n\n`;
296
+ return `referenced from ${colorize.blue(fileWithLoc)} ${atPointer} \n\n`;
273
297
  }
274
298
 
275
299
  function formatDidYouMean(problem: NormalizedProblem) {
@@ -320,6 +344,7 @@ const groupByFiles = (problems: NormalizedProblem[]) => {
320
344
  };
321
345
 
322
346
  function xmlEscape(s: string): string {
347
+ // eslint-disable-next-line no-control-regex
323
348
  return s.replace(/[<>&"'\x00-\x1F\x7F\u0080-\uFFFF]/gu, (char) => {
324
349
  switch (char) {
325
350
  case '<':
package/src/index.ts CHANGED
@@ -1,9 +1,9 @@
1
- export { BundleOutputFormat, readFileFromUrl, slash, doesYamlFileExist } from './utils';
1
+ export { BundleOutputFormat, readFileFromUrl, slash, doesYamlFileExist, isTruthy } from './utils';
2
2
  export { Oas3_1Types } from './types/oas3_1';
3
3
  export { Oas3Types } from './types/oas3';
4
4
  export { Oas2Types } from './types/oas2';
5
5
  export { ConfigTypes } from './types/redocly-yaml';
6
- export {
6
+ export type {
7
7
  Oas3Definition,
8
8
  Oas3_1Definition,
9
9
  Oas3Components,
@@ -15,9 +15,10 @@ export {
15
15
  Oas3Tag,
16
16
  Oas3_1Webhooks,
17
17
  Referenced,
18
+ OasRef,
18
19
  } from './typings/openapi';
19
- export { Oas2Definition } from './typings/swagger';
20
- export { StatsAccumulator, StatsName } from './typings/common';
20
+ export type { Oas2Definition } from './typings/swagger';
21
+ export type { StatsAccumulator, StatsName } from './typings/common';
21
22
  export { normalizeTypes } from './types';
22
23
  export { Stats } from './rules/other/stats';
23
24
 
@@ -34,6 +35,7 @@ export {
34
35
  findConfig,
35
36
  CONFIG_FILE_NAMES,
36
37
  RuleSeverity,
38
+ createConfig,
37
39
  } from './config';
38
40
 
39
41
  export { RedoclyClient, isRedoclyRegistryURL } from './redocly';
@@ -1,4 +1,5 @@
1
1
  // TODO: add a type for "types" https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/js-yaml/index.d.ts
2
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
2
3
  // @ts-ignore
3
4
  import { JSON_SCHEMA, types, LoadOptions, DumpOptions, load, dump } from 'js-yaml';
4
5
 
package/src/lint.ts CHANGED
@@ -80,13 +80,13 @@ export async function lintDocument(opts: {
80
80
  const normalizedVisitors = normalizeVisitors([...preprocessors, ...regularRules] as any, types);
81
81
  const resolvedRefMap = await resolveDocument({
82
82
  rootDocument: document,
83
- rootType: types.DefinitionRoot,
83
+ rootType: types.Root,
84
84
  externalRefResolver,
85
85
  });
86
86
 
87
87
  walkDocument({
88
88
  document,
89
- rootType: types.DefinitionRoot,
89
+ rootType: types.Root,
90
90
  normalizedVisitors,
91
91
  resolvedRefMap,
92
92
  ctx,
package/src/logger.ts ADDED
@@ -0,0 +1,34 @@
1
+ import * as colorette from 'colorette';
2
+ export { options as colorOptions } from 'colorette';
3
+
4
+ import { isBrowser } from './env';
5
+ import { identity } from './utils';
6
+
7
+ export const colorize = new Proxy(colorette, {
8
+ get(target: typeof colorette, prop: string): typeof identity {
9
+ if (isBrowser) {
10
+ return identity;
11
+ }
12
+
13
+ return (target as any)[prop];
14
+ },
15
+ });
16
+ class Logger {
17
+ protected stderr(str: string) {
18
+ return process.stderr.write(str);
19
+ }
20
+
21
+ info(str: string) {
22
+ return isBrowser ? console.log(str) : this.stderr(str);
23
+ }
24
+
25
+ warn(str: string) {
26
+ return isBrowser ? console.warn(str) : this.stderr(colorize.yellow(str));
27
+ }
28
+
29
+ error(str: string) {
30
+ return isBrowser ? console.error(str) : this.stderr(colorize.red(str));
31
+ }
32
+ }
33
+
34
+ export const logger = new Logger();
package/src/output.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { isBrowser } from './env';
2
+
3
+ export const output = {
4
+ write(str: string) {
5
+ return isBrowser ? undefined : process.stdout.write(str);
6
+ },
7
+ };
@@ -1,11 +1,12 @@
1
1
  import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
2
2
  import { resolve } from 'path';
3
3
  import { homedir } from 'os';
4
- import { green } from 'colorette';
5
4
  import { RegistryApi } from './registry-api';
6
- import { DEFAULT_REGION, DOMAINS, AVAILABLE_REGIONS, env } from '../config/config';
5
+ import { DEFAULT_REGION, DOMAINS, AVAILABLE_REGIONS } from '../config/config';
6
+ import { env } from '../env';
7
7
  import { RegionalToken, RegionalTokenWithValidity } from './redocly-client-types';
8
8
  import { isNotEmptyObject } from '../utils';
9
+ import { colorize } from '../logger';
9
10
 
10
11
  import type { AccessTokens, Region } from '../config/types';
11
12
 
@@ -29,7 +30,9 @@ export class RedoclyClient {
29
30
  loadRegion(region?: Region) {
30
31
  if (region && !DOMAINS[region]) {
31
32
  throw new Error(
32
- `Invalid argument: region in config file.\nGiven: ${green(region)}, choices: "us", "eu".`
33
+ `Invalid argument: region in config file.\nGiven: ${colorize.green(
34
+ region
35
+ )}, choices: "us", "eu".`
33
36
  );
34
37
  }
35
38
 
@@ -152,7 +155,7 @@ export class RedoclyClient {
152
155
 
153
156
  const credentials = {
154
157
  ...this.readCredentialsFile(credentialsPath),
155
- [this.region!]: accessToken,
158
+ [this.region]: accessToken,
156
159
  token: accessToken, // FIXME: backward compatibility, remove on 1.0.0
157
160
  };
158
161
  this.accessTokens = credentials;
@@ -1,34 +1,32 @@
1
- export namespace RegistryApiTypes {
2
- interface VersionParams {
3
- organizationId: string;
4
- name: string;
5
- version: string;
6
- }
1
+ interface VersionParams {
2
+ organizationId: string;
3
+ name: string;
4
+ version: string;
5
+ }
7
6
 
8
- export interface PrepareFileuploadParams extends VersionParams {
9
- filesHash: string;
10
- filename: string;
11
- isUpsert?: boolean;
12
- }
7
+ export interface PrepareFileuploadParams extends VersionParams {
8
+ filesHash: string;
9
+ filename: string;
10
+ isUpsert?: boolean;
11
+ }
13
12
 
14
- export interface PushApiParams extends VersionParams {
15
- rootFilePath: string;
16
- filePaths: string[];
17
- branch?: string;
18
- isUpsert?: boolean;
19
- isPublic?: boolean;
20
- batchId?: string;
21
- batchSize?: number;
22
- }
13
+ export interface PushApiParams extends VersionParams {
14
+ rootFilePath: string;
15
+ filePaths: string[];
16
+ branch?: string;
17
+ isUpsert?: boolean;
18
+ isPublic?: boolean;
19
+ batchId?: string;
20
+ batchSize?: number;
21
+ }
23
22
 
24
- export interface PrepareFileuploadOKResponse {
25
- filePath: string;
26
- signedUploadUrl: string;
27
- }
23
+ export interface PrepareFileuploadOKResponse {
24
+ filePath: string;
25
+ signedUploadUrl: string;
26
+ }
28
27
 
29
- export interface NotFoundProblemResponse {
30
- status: 404;
31
- title: 'Not Found';
32
- code: 'ORGANIZATION_NOT_FOUND' | 'API_VERSION_NOT_FOUND';
33
- }
28
+ export interface NotFoundProblemResponse {
29
+ status: 404;
30
+ title: 'Not Found';
31
+ code: 'ORGANIZATION_NOT_FOUND' | 'API_VERSION_NOT_FOUND';
34
32
  }
@@ -1,11 +1,15 @@
1
1
  import fetch, { RequestInit, HeadersInit } from 'node-fetch';
2
- import { RegistryApiTypes } from './registry-api-types';
2
+ import type {
3
+ NotFoundProblemResponse,
4
+ PrepareFileuploadOKResponse,
5
+ PrepareFileuploadParams,
6
+ PushApiParams,
7
+ } from './registry-api-types';
8
+ import type { AccessTokens, Region } from '../config/types';
3
9
  import { DEFAULT_REGION, DOMAINS } from '../config/config';
4
10
  import { isNotEmptyObject } from '../utils';
5
11
  const version = require('../../package.json').version;
6
12
 
7
- import type { AccessTokens, Region } from '../config/types';
8
-
9
13
  export class RegistryApi {
10
14
  constructor(private accessTokens: AccessTokens, private region: Region) {}
11
15
 
@@ -23,7 +27,14 @@ export class RegistryApi {
23
27
  }
24
28
 
25
29
  private async request(path = '', options: RequestInit = {}, region?: Region) {
26
- const headers = Object.assign({}, options.headers || {}, { 'x-redocly-cli-version': version });
30
+ const currentCommand =
31
+ typeof process !== 'undefined' ? process.env?.REDOCLY_CLI_COMMAND || '' : '';
32
+ const redoclyEnv = typeof process !== 'undefined' ? process.env?.REDOCLY_ENVIRONMENT || '' : '';
33
+
34
+ const headers = Object.assign({}, options.headers || {}, {
35
+ 'x-redocly-cli-version': version,
36
+ 'user-agent': `redocly-cli / ${version} ${currentCommand} ${redoclyEnv}`,
37
+ });
27
38
 
28
39
  if (!headers.hasOwnProperty('authorization')) {
29
40
  throw new Error('Unauthorized');
@@ -39,7 +50,7 @@ export class RegistryApi {
39
50
  }
40
51
 
41
52
  if (response.status === 404) {
42
- const body: RegistryApiTypes.NotFoundProblemResponse = await response.json();
53
+ const body: NotFoundProblemResponse = await response.json();
43
54
  throw new Error(body.code);
44
55
  }
45
56
 
@@ -71,7 +82,7 @@ export class RegistryApi {
71
82
  filesHash,
72
83
  filename,
73
84
  isUpsert,
74
- }: RegistryApiTypes.PrepareFileuploadParams): Promise<RegistryApiTypes.PrepareFileuploadOKResponse> {
85
+ }: PrepareFileuploadParams): Promise<PrepareFileuploadOKResponse> {
75
86
  const response = await this.request(
76
87
  `/${organizationId}/${name}/${version}/prepare-file-upload`,
77
88
  {
@@ -107,7 +118,7 @@ export class RegistryApi {
107
118
  isPublic,
108
119
  batchId,
109
120
  batchSize,
110
- }: RegistryApiTypes.PushApiParams) {
121
+ }: PushApiParams) {
111
122
  const response = await this.request(
112
123
  `/${organizationId}/${name}/${version}`,
113
124
  {
package/src/ref-utils.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Source } from './resolve';
2
2
  import { OasRef } from './typings/openapi';
3
+ import { isTruthy } from './utils';
3
4
 
4
5
  export function joinPointer(base: string, key: string | number) {
5
6
  if (base === '') base = '#/';
@@ -45,7 +46,7 @@ export function parseRef(ref: string): { uri: string | null; pointer: string[] }
45
46
  const [uri, pointer] = ref.split('#/');
46
47
  return {
47
48
  uri: uri || null,
48
- pointer: pointer ? pointer.split('/').map(unescapePointer).filter(Boolean) : [],
49
+ pointer: pointer ? pointer.split('/').map(unescapePointer).filter(isTruthy) : [],
49
50
  };
50
51
  }
51
52
 
package/src/resolve.ts CHANGED
@@ -91,7 +91,7 @@ export function makeDocumentFromString(sourceString: string, absoluteRef: string
91
91
  export class BaseResolver {
92
92
  cache: Map<string, Promise<Document | ResolveError>> = new Map();
93
93
 
94
- constructor(private config: ResolveConfig = { http: { headers: [] } }) {}
94
+ constructor(protected config: ResolveConfig = { http: { headers: [] } }) {}
95
95
 
96
96
  getFiles() {
97
97
  return new Set(Array.from(this.cache.keys()));
@@ -115,7 +115,9 @@ export class BaseResolver {
115
115
  const { body, mimeType } = await readFileFromUrl(absoluteRef, this.config.http);
116
116
  return new Source(absoluteRef, body, mimeType);
117
117
  } else {
118
- return new Source(absoluteRef, await fs.promises.readFile(absoluteRef, 'utf-8'));
118
+ const content = await fs.promises.readFile(absoluteRef, 'utf-8');
119
+ // In some cases file have \r\n line delimeters like on windows, we should skip it.
120
+ return new Source(absoluteRef, content.replace(/\r\n/g, '\n'));
119
121
  }
120
122
  } catch (error) {
121
123
  throw new ResolveError(error);
@@ -323,7 +325,7 @@ export async function resolveDocument(opts: {
323
325
  : document;
324
326
  } catch (error) {
325
327
  const resolvedRef = {
326
- resolved: false as false,
328
+ resolved: false as const,
327
329
  isRemote,
328
330
  document: undefined,
329
331
  error: error,
@@ -334,7 +336,7 @@ export async function resolveDocument(opts: {
334
336
  }
335
337
 
336
338
  let resolvedRef: ResolvedRef = {
337
- resolved: true as true,
339
+ resolved: true as const,
338
340
  document: targetDoc,
339
341
  isRemote,
340
342
  node: document.parsed,
@@ -344,7 +346,7 @@ export async function resolveDocument(opts: {
344
346
  let target = targetDoc.parsed as any;
345
347
 
346
348
  const segments = pointer;
347
- for (let segment of segments) {
349
+ for (const segment of segments) {
348
350
  if (typeof target !== 'object') {
349
351
  target = undefined;
350
352
  break;
@@ -1,4 +1,10 @@
1
- import { fieldNonEmpty, matchesJsonSchemaType, missingRequiredField, oasTypeOf } from '../utils';
1
+ import {
2
+ fieldNonEmpty,
3
+ matchesJsonSchemaType,
4
+ missingRequiredField,
5
+ oasTypeOf,
6
+ getAdditionalPropertiesOption,
7
+ } from '../utils';
2
8
 
3
9
  describe('field-non-empty', () => {
4
10
  it('should match expected message', () => {
@@ -120,3 +126,35 @@ describe('oas-type-of', () => {
120
126
  expect(results).toBe('object');
121
127
  });
122
128
  });
129
+
130
+ describe('get-additional-properties-option', () => {
131
+ it('should return actual option', () => {
132
+ const options = {
133
+ allowAdditionalProperties: true,
134
+ };
135
+ expect(getAdditionalPropertiesOption(options)).toBeTruthy();
136
+ });
137
+
138
+ it('should reverse option', () => {
139
+ const options = {
140
+ disallowAdditionalProperties: true,
141
+ };
142
+ expect(getAdditionalPropertiesOption(options)).toBeFalsy();
143
+ });
144
+
145
+ it('should throw error with message', () => {
146
+ const options = {
147
+ allowAdditionalProperties: true,
148
+ disallowAdditionalProperties: false,
149
+ };
150
+
151
+ try {
152
+ getAdditionalPropertiesOption(options);
153
+ } catch (error) {
154
+ expect(error).toBeInstanceOf(Error);
155
+ expect(error.message).toEqual(
156
+ "Do not use 'disallowAdditionalProperties' field. Use 'allowAdditionalProperties' instead. \n"
157
+ );
158
+ }
159
+ });
160
+ });