@apidiff/core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/dist/ast/hash.d.ts +2 -0
  2. package/dist/ast/hash.d.ts.map +1 -0
  3. package/dist/ast/index.d.ts +3 -0
  4. package/dist/ast/index.d.ts.map +1 -0
  5. package/dist/ast/traverse.d.ts +13 -0
  6. package/dist/ast/traverse.d.ts.map +1 -0
  7. package/dist/config/index.d.ts +9 -0
  8. package/dist/config/index.d.ts.map +1 -0
  9. package/dist/diff/auth-differ.d.ts +4 -0
  10. package/dist/diff/auth-differ.d.ts.map +1 -0
  11. package/dist/diff/endpoint-differ.d.ts +3 -0
  12. package/dist/diff/endpoint-differ.d.ts.map +1 -0
  13. package/dist/diff/index.d.ts +3 -0
  14. package/dist/diff/index.d.ts.map +1 -0
  15. package/dist/diff/schema-differ.d.ts +3 -0
  16. package/dist/diff/schema-differ.d.ts.map +1 -0
  17. package/dist/diff/server-differ.d.ts +3 -0
  18. package/dist/diff/server-differ.d.ts.map +1 -0
  19. package/dist/index.cjs +2902 -0
  20. package/dist/index.d.ts +15 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +2855 -0
  23. package/dist/loader/file-loader.d.ts +5 -0
  24. package/dist/loader/file-loader.d.ts.map +1 -0
  25. package/dist/loader/git-loader.d.ts +5 -0
  26. package/dist/loader/git-loader.d.ts.map +1 -0
  27. package/dist/loader/index.d.ts +7 -0
  28. package/dist/loader/index.d.ts.map +1 -0
  29. package/dist/loader/url-loader.d.ts +5 -0
  30. package/dist/loader/url-loader.d.ts.map +1 -0
  31. package/dist/output/html.d.ts +3 -0
  32. package/dist/output/html.d.ts.map +1 -0
  33. package/dist/output/index.d.ts +3 -0
  34. package/dist/output/index.d.ts.map +1 -0
  35. package/dist/output/json.d.ts +3 -0
  36. package/dist/output/json.d.ts.map +1 -0
  37. package/dist/output/markdown.d.ts +3 -0
  38. package/dist/output/markdown.d.ts.map +1 -0
  39. package/dist/output/terminal.d.ts +3 -0
  40. package/dist/output/terminal.d.ts.map +1 -0
  41. package/dist/parsers/base.d.ts +8 -0
  42. package/dist/parsers/base.d.ts.map +1 -0
  43. package/dist/parsers/graphql/index.d.ts +13 -0
  44. package/dist/parsers/graphql/index.d.ts.map +1 -0
  45. package/dist/parsers/index.d.ts +7 -0
  46. package/dist/parsers/index.d.ts.map +1 -0
  47. package/dist/parsers/openapi2/index.d.ts +9 -0
  48. package/dist/parsers/openapi2/index.d.ts.map +1 -0
  49. package/dist/parsers/openapi3/index.d.ts +9 -0
  50. package/dist/parsers/openapi3/index.d.ts.map +1 -0
  51. package/dist/parsers/openapi3/ref-resolver.d.ts +2 -0
  52. package/dist/parsers/openapi3/ref-resolver.d.ts.map +1 -0
  53. package/dist/parsers/openapi3/schema-normalizer.d.ts +3 -0
  54. package/dist/parsers/openapi3/schema-normalizer.d.ts.map +1 -0
  55. package/dist/parsers/openapi3/security-normalizer.d.ts +3 -0
  56. package/dist/parsers/openapi3/security-normalizer.d.ts.map +1 -0
  57. package/dist/parsers/protobuf/index.d.ts +14 -0
  58. package/dist/parsers/protobuf/index.d.ts.map +1 -0
  59. package/dist/rules/auth/oauth-scope-removed.d.ts +9 -0
  60. package/dist/rules/auth/oauth-scope-removed.d.ts.map +1 -0
  61. package/dist/rules/auth/security-added.d.ts +9 -0
  62. package/dist/rules/auth/security-added.d.ts.map +1 -0
  63. package/dist/rules/auth/security-removed.d.ts +9 -0
  64. package/dist/rules/auth/security-removed.d.ts.map +1 -0
  65. package/dist/rules/auth/security-scheme-type-changed.d.ts +9 -0
  66. package/dist/rules/auth/security-scheme-type-changed.d.ts.map +1 -0
  67. package/dist/rules/base.d.ts +19 -0
  68. package/dist/rules/base.d.ts.map +1 -0
  69. package/dist/rules/endpoint/endpoint-added.d.ts +9 -0
  70. package/dist/rules/endpoint/endpoint-added.d.ts.map +1 -0
  71. package/dist/rules/endpoint/endpoint-deprecated.d.ts +9 -0
  72. package/dist/rules/endpoint/endpoint-deprecated.d.ts.map +1 -0
  73. package/dist/rules/endpoint/endpoint-removed.d.ts +9 -0
  74. package/dist/rules/endpoint/endpoint-removed.d.ts.map +1 -0
  75. package/dist/rules/endpoint/http-method-changed.d.ts +9 -0
  76. package/dist/rules/endpoint/http-method-changed.d.ts.map +1 -0
  77. package/dist/rules/endpoint/path-changed.d.ts +9 -0
  78. package/dist/rules/endpoint/path-changed.d.ts.map +1 -0
  79. package/dist/rules/index.d.ts +4 -0
  80. package/dist/rules/index.d.ts.map +1 -0
  81. package/dist/rules/meta/server-removed.d.ts +9 -0
  82. package/dist/rules/meta/server-removed.d.ts.map +1 -0
  83. package/dist/rules/param/param-added.d.ts +9 -0
  84. package/dist/rules/param/param-added.d.ts.map +1 -0
  85. package/dist/rules/param/param-deprecated.d.ts +9 -0
  86. package/dist/rules/param/param-deprecated.d.ts.map +1 -0
  87. package/dist/rules/param/param-enum-value-added.d.ts +9 -0
  88. package/dist/rules/param/param-enum-value-added.d.ts.map +1 -0
  89. package/dist/rules/param/param-enum-value-removed.d.ts +9 -0
  90. package/dist/rules/param/param-enum-value-removed.d.ts.map +1 -0
  91. package/dist/rules/param/param-location-changed.d.ts +9 -0
  92. package/dist/rules/param/param-location-changed.d.ts.map +1 -0
  93. package/dist/rules/param/param-removed.d.ts +9 -0
  94. package/dist/rules/param/param-removed.d.ts.map +1 -0
  95. package/dist/rules/param/param-required-added.d.ts +9 -0
  96. package/dist/rules/param/param-required-added.d.ts.map +1 -0
  97. package/dist/rules/param/param-required-true-to-false.d.ts +9 -0
  98. package/dist/rules/param/param-required-true-to-false.d.ts.map +1 -0
  99. package/dist/rules/param/param-type-changed.d.ts +9 -0
  100. package/dist/rules/param/param-type-changed.d.ts.map +1 -0
  101. package/dist/rules/request/request-body-added-required.d.ts +9 -0
  102. package/dist/rules/request/request-body-added-required.d.ts.map +1 -0
  103. package/dist/rules/request/request-body-removed.d.ts +9 -0
  104. package/dist/rules/request/request-body-removed.d.ts.map +1 -0
  105. package/dist/rules/request/request-content-type-added.d.ts +9 -0
  106. package/dist/rules/request/request-content-type-added.d.ts.map +1 -0
  107. package/dist/rules/request/request-content-type-removed.d.ts +9 -0
  108. package/dist/rules/request/request-content-type-removed.d.ts.map +1 -0
  109. package/dist/rules/request/request-field-added-required.d.ts +9 -0
  110. package/dist/rules/request/request-field-added-required.d.ts.map +1 -0
  111. package/dist/rules/request/request-field-removed.d.ts +9 -0
  112. package/dist/rules/request/request-field-removed.d.ts.map +1 -0
  113. package/dist/rules/request/request-field-type-changed.d.ts +9 -0
  114. package/dist/rules/request/request-field-type-changed.d.ts.map +1 -0
  115. package/dist/rules/request/request-required-false-to-true.d.ts +9 -0
  116. package/dist/rules/request/request-required-false-to-true.d.ts.map +1 -0
  117. package/dist/rules/response/response-field-added.d.ts +9 -0
  118. package/dist/rules/response/response-field-added.d.ts.map +1 -0
  119. package/dist/rules/response/response-field-removed.d.ts +9 -0
  120. package/dist/rules/response/response-field-removed.d.ts.map +1 -0
  121. package/dist/rules/response/response-field-type-changed.d.ts +9 -0
  122. package/dist/rules/response/response-field-type-changed.d.ts.map +1 -0
  123. package/dist/rules/response/response-header-added-required.d.ts +9 -0
  124. package/dist/rules/response/response-header-added-required.d.ts.map +1 -0
  125. package/dist/rules/response/response-header-removed.d.ts +9 -0
  126. package/dist/rules/response/response-header-removed.d.ts.map +1 -0
  127. package/dist/rules/response/response-media-type-added.d.ts +9 -0
  128. package/dist/rules/response/response-media-type-added.d.ts.map +1 -0
  129. package/dist/rules/response/response-media-type-removed.d.ts +9 -0
  130. package/dist/rules/response/response-media-type-removed.d.ts.map +1 -0
  131. package/dist/rules/response/response-status-added.d.ts +9 -0
  132. package/dist/rules/response/response-status-added.d.ts.map +1 -0
  133. package/dist/rules/response/response-status-removed.d.ts +9 -0
  134. package/dist/rules/response/response-status-removed.d.ts.map +1 -0
  135. package/dist/types/ast.d.ts +140 -0
  136. package/dist/types/ast.d.ts.map +1 -0
  137. package/dist/types/config.d.ts +17 -0
  138. package/dist/types/config.d.ts.map +1 -0
  139. package/dist/types/diff.d.ts +43 -0
  140. package/dist/types/diff.d.ts.map +1 -0
  141. package/dist/types/errors.d.ts +21 -0
  142. package/dist/types/errors.d.ts.map +1 -0
  143. package/dist/types/index.d.ts +6 -0
  144. package/dist/types/index.d.ts.map +1 -0
  145. package/dist/types/semantic.d.ts +45 -0
  146. package/dist/types/semantic.d.ts.map +1 -0
  147. package/package.json +31 -0
  148. package/src/ast/hash.ts +26 -0
  149. package/src/ast/index.ts +2 -0
  150. package/src/ast/traverse.ts +59 -0
  151. package/src/config/index.ts +44 -0
  152. package/src/diff/auth-differ.ts +72 -0
  153. package/src/diff/endpoint-differ.ts +144 -0
  154. package/src/diff/index.ts +13 -0
  155. package/src/diff/schema-differ.ts +70 -0
  156. package/src/diff/server-differ.ts +22 -0
  157. package/src/index.ts +92 -0
  158. package/src/loader/file-loader.ts +19 -0
  159. package/src/loader/git-loader.ts +22 -0
  160. package/src/loader/index.ts +32 -0
  161. package/src/loader/url-loader.ts +25 -0
  162. package/src/output/html.ts +177 -0
  163. package/src/output/index.ts +16 -0
  164. package/src/output/json.ts +5 -0
  165. package/src/output/markdown.ts +29 -0
  166. package/src/output/terminal.ts +37 -0
  167. package/src/parsers/base.ts +8 -0
  168. package/src/parsers/graphql/index.ts +181 -0
  169. package/src/parsers/index.ts +61 -0
  170. package/src/parsers/openapi2/index.ts +218 -0
  171. package/src/parsers/openapi3/index.ts +223 -0
  172. package/src/parsers/openapi3/ref-resolver.ts +101 -0
  173. package/src/parsers/openapi3/schema-normalizer.ts +52 -0
  174. package/src/parsers/openapi3/security-normalizer.ts +18 -0
  175. package/src/parsers/protobuf/index.ts +208 -0
  176. package/src/rules/auth/oauth-scope-removed.ts +34 -0
  177. package/src/rules/auth/security-added.ts +32 -0
  178. package/src/rules/auth/security-removed.ts +47 -0
  179. package/src/rules/auth/security-scheme-type-changed.ts +30 -0
  180. package/src/rules/base.ts +29 -0
  181. package/src/rules/endpoint/endpoint-added.ts +26 -0
  182. package/src/rules/endpoint/endpoint-deprecated.ts +29 -0
  183. package/src/rules/endpoint/endpoint-removed.ts +26 -0
  184. package/src/rules/endpoint/http-method-changed.ts +29 -0
  185. package/src/rules/endpoint/path-changed.ts +29 -0
  186. package/src/rules/index.ts +83 -0
  187. package/src/rules/meta/server-removed.ts +26 -0
  188. package/src/rules/param/param-added.ts +52 -0
  189. package/src/rules/param/param-deprecated.ts +34 -0
  190. package/src/rules/param/param-enum-value-added.ts +32 -0
  191. package/src/rules/param/param-enum-value-removed.ts +32 -0
  192. package/src/rules/param/param-location-changed.ts +43 -0
  193. package/src/rules/param/param-removed.ts +52 -0
  194. package/src/rules/param/param-required-added.ts +34 -0
  195. package/src/rules/param/param-required-true-to-false.ts +34 -0
  196. package/src/rules/param/param-type-changed.ts +32 -0
  197. package/src/rules/request/request-body-added-required.ts +33 -0
  198. package/src/rules/request/request-body-removed.ts +30 -0
  199. package/src/rules/request/request-content-type-added.ts +31 -0
  200. package/src/rules/request/request-content-type-removed.ts +31 -0
  201. package/src/rules/request/request-field-added-required.ts +41 -0
  202. package/src/rules/request/request-field-removed.ts +35 -0
  203. package/src/rules/request/request-field-type-changed.ts +37 -0
  204. package/src/rules/request/request-required-false-to-true.ts +56 -0
  205. package/src/rules/response/response-field-added.ts +34 -0
  206. package/src/rules/response/response-field-removed.ts +34 -0
  207. package/src/rules/response/response-field-type-changed.ts +37 -0
  208. package/src/rules/response/response-header-added-required.ts +47 -0
  209. package/src/rules/response/response-header-removed.ts +32 -0
  210. package/src/rules/response/response-media-type-added.ts +32 -0
  211. package/src/rules/response/response-media-type-removed.ts +32 -0
  212. package/src/rules/response/response-status-added.ts +31 -0
  213. package/src/rules/response/response-status-removed.ts +31 -0
  214. package/src/types/ast.ts +164 -0
  215. package/src/types/config.ts +31 -0
  216. package/src/types/diff.ts +49 -0
  217. package/src/types/errors.ts +26 -0
  218. package/src/types/index.ts +5 -0
  219. package/src/types/semantic.ts +60 -0
  220. package/test/integration/stripe-v1.yaml +36 -0
  221. package/test/integration/stripe-v2.yaml +35 -0
  222. package/tests/ast.test.ts +60 -0
  223. package/tests/config.test.ts +43 -0
  224. package/tests/diff/auth-differ.test.ts +55 -0
  225. package/tests/diff/endpoint-differ.test.ts +42 -0
  226. package/tests/diff/schema-differ.test.ts +46 -0
  227. package/tests/diff/server-differ.test.ts +23 -0
  228. package/tests/diff.test.ts +116 -0
  229. package/tests/fixtures/openapi3/auth-added/v1.yaml +11 -0
  230. package/tests/fixtures/openapi3/auth-added/v2.yaml +18 -0
  231. package/tests/integration.test.ts +21 -0
  232. package/tests/loader-more.test.ts +75 -0
  233. package/tests/loader.test.ts +61 -0
  234. package/tests/output.test.ts +58 -0
  235. package/tests/parsers/openapi3-coverage.test.ts +77 -0
  236. package/tests/parsers/openapi3.test.ts +41 -0
  237. package/tests/parsers/parsers-other.test.ts +135 -0
  238. package/tests/parsers/ref-resolver.test.ts +78 -0
  239. package/tests/parsers/schema-normalizer.test.ts +30 -0
  240. package/tests/rules/auth-meta.test.ts +87 -0
  241. package/tests/rules/base.test.ts +44 -0
  242. package/tests/rules/endpoint.test.ts +122 -0
  243. package/tests/rules/param.test.ts +242 -0
  244. package/tests/rules/request.test.ts +147 -0
  245. package/tests/rules/response.test.ts +161 -0
  246. package/tests/rules/rules-coverage.test.ts +64 -0
  247. package/tests/types-errors.test.ts +22 -0
  248. package/tsconfig.json +8 -0
  249. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,44 @@
1
+ import { readFileSync, existsSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import type { ApidiffConfig } from '../types/index.js';
4
+
5
+ export const DEFAULT_CONFIG: ApidiffConfig = {
6
+ failOn: 'breaking',
7
+ ignorePaths: [],
8
+ ruleSeverityOverrides: {},
9
+ disabledRules: [],
10
+ customRules: [],
11
+ output: {
12
+ format: 'terminal',
13
+ color: true,
14
+ summary: false,
15
+ quiet: false
16
+ }
17
+ };
18
+
19
+ export async function loadConfig(options: { config?: string, format?: string, failOn?: string, ignorePath?: string[] }): Promise<ApidiffConfig> {
20
+ let config = { ...DEFAULT_CONFIG, output: { ...DEFAULT_CONFIG.output } };
21
+
22
+ if (options.config) {
23
+ const configPath = resolve(process.cwd(), options.config);
24
+ if (existsSync(configPath)) {
25
+ const fileContent = readFileSync(configPath, 'utf8');
26
+ try {
27
+ const fileConfig = JSON.parse(fileContent);
28
+ config = { ...config, ...fileConfig, output: { ...config.output, ...(fileConfig.output || {}) } };
29
+ } catch (e) {
30
+ throw new Error(`Failed to parse config file at ${configPath}`);
31
+ }
32
+ } else {
33
+ throw new Error(`Config file not found at ${configPath}`);
34
+ }
35
+ }
36
+
37
+ if (options.format) config.output.format = options.format as any;
38
+ if (options.failOn) config.failOn = options.failOn as any;
39
+ if (options.ignorePath && options.ignorePath.length > 0) {
40
+ config.ignorePaths = [...config.ignorePaths, ...options.ignorePath];
41
+ }
42
+
43
+ return config;
44
+ }
@@ -0,0 +1,72 @@
1
+ import type { SecurityScheme, SecurityRequirement, SecurityDiff, FieldChange } from '../types/index.js';
2
+
3
+ export function diffSecuritySchemes(oldSchemes: SecurityScheme[], newSchemes: SecurityScheme[]): SecurityDiff[] {
4
+ const diffs: SecurityDiff[] = [];
5
+ const oldMap = new Map(oldSchemes.map(s => [s.id, s]));
6
+ const newMap = new Map(newSchemes.map(s => [s.id, s]));
7
+
8
+ const allIds = new Set([...oldMap.keys(), ...newMap.keys()]);
9
+
10
+ for (const id of allIds) {
11
+ const oldScheme = oldMap.get(id);
12
+ const newScheme = newMap.get(id);
13
+
14
+ if (oldScheme && !newScheme) {
15
+ diffs.push({ schemeId: id, changeType: 'removed', fieldChanges: [] });
16
+ } else if (!oldScheme && newScheme) {
17
+ diffs.push({ schemeId: id, changeType: 'added', fieldChanges: [] });
18
+ } else if (oldScheme && newScheme) {
19
+ const fieldChanges: FieldChange[] = [];
20
+ if (oldScheme.type !== newScheme.type) {
21
+ fieldChanges.push({ fieldPath: ['type'], changeType: 'changed', oldValue: oldScheme.type, newValue: newScheme.type });
22
+ }
23
+ if (oldScheme.scheme !== newScheme.scheme) {
24
+ fieldChanges.push({ fieldPath: ['scheme'], changeType: 'changed', oldValue: oldScheme.scheme, newValue: newScheme.scheme });
25
+ }
26
+ if (oldScheme.in !== newScheme.in) {
27
+ fieldChanges.push({ fieldPath: ['in'], changeType: 'changed', oldValue: oldScheme.in, newValue: newScheme.in });
28
+ }
29
+ if (fieldChanges.length > 0) {
30
+ diffs.push({ schemeId: id, changeType: 'changed', fieldChanges });
31
+ }
32
+ }
33
+ }
34
+
35
+ return diffs;
36
+ }
37
+
38
+ export function diffSecurityRequirements(oldReqs: SecurityRequirement[], newReqs: SecurityRequirement[], path: string[], changes: FieldChange[]): void {
39
+ const oldSet = new Set(oldReqs.map(r => r.schemeId));
40
+ const newSet = new Set(newReqs.map(r => r.schemeId));
41
+
42
+ for (const schemeId of newSet) {
43
+ if (!oldSet.has(schemeId)) {
44
+ changes.push({ fieldPath: [...path, schemeId], changeType: 'added', newValue: schemeId });
45
+ }
46
+ }
47
+
48
+ for (const schemeId of oldSet) {
49
+ if (!newSet.has(schemeId)) {
50
+ changes.push({ fieldPath: [...path, schemeId], changeType: 'removed', oldValue: schemeId });
51
+ }
52
+ }
53
+
54
+ const oldMap = new Map(oldReqs.map(r => [r.schemeId, new Set(r.scopes)]));
55
+ const newMap = new Map(newReqs.map(r => [r.schemeId, new Set(r.scopes)]));
56
+
57
+ for (const [schemeId, oldScopes] of oldMap.entries()) {
58
+ const newScopes = newMap.get(schemeId);
59
+ if (newScopes) {
60
+ for (const scope of newScopes) {
61
+ if (!oldScopes.has(scope)) {
62
+ changes.push({ fieldPath: [...path, schemeId, 'scopes', scope], changeType: 'added', newValue: scope });
63
+ }
64
+ }
65
+ for (const scope of oldScopes) {
66
+ if (!newScopes.has(scope)) {
67
+ changes.push({ fieldPath: [...path, schemeId, 'scopes', scope], changeType: 'removed', oldValue: scope });
68
+ }
69
+ }
70
+ }
71
+ }
72
+ }
@@ -0,0 +1,144 @@
1
+ import type { Endpoint, EndpointDiff, FieldChange, Parameter, ResponseDef, RequestBody } from '../types/index.js';
2
+ import { diffSchema } from './schema-differ.js';
3
+ import { diffSecurityRequirements } from './auth-differ.js';
4
+
5
+ export function diffEndpoints(oldEndpoints: Endpoint[], newEndpoints: Endpoint[]): EndpointDiff[] {
6
+ const diffs: EndpointDiff[] = [];
7
+ const oldMap = new Map(oldEndpoints.map(e => [e.id, e]));
8
+ const newMap = new Map(newEndpoints.map(e => [e.id, e]));
9
+
10
+ const allIds = new Set([...oldMap.keys(), ...newMap.keys()]);
11
+
12
+ for (const id of allIds) {
13
+ const oldE = oldMap.get(id);
14
+ const newE = newMap.get(id);
15
+
16
+ if (oldE && !newE) {
17
+ diffs.push({ type: 'removed', endpointId: id, path: oldE.path, method: oldE.method, oldEndpoint: oldE, fieldChanges: [] });
18
+ } else if (!oldE && newE) {
19
+ diffs.push({ type: 'added', endpointId: id, path: newE.path, method: newE.method, newEndpoint: newE, fieldChanges: [] });
20
+ } else if (oldE && newE) {
21
+ const fieldChanges: FieldChange[] = [];
22
+
23
+ if (oldE.method !== newE.method) {
24
+ fieldChanges.push({ fieldPath: ['method'], changeType: 'changed', oldValue: oldE.method, newValue: newE.method });
25
+ }
26
+
27
+ if (oldE.path !== newE.path) {
28
+ fieldChanges.push({ fieldPath: ['path'], changeType: 'changed', oldValue: oldE.path, newValue: newE.path });
29
+ }
30
+
31
+ if (!!oldE.deprecated !== !!newE.deprecated) {
32
+ fieldChanges.push({ fieldPath: ['deprecated'], changeType: 'changed', oldValue: !!oldE.deprecated, newValue: !!newE.deprecated });
33
+ }
34
+
35
+ diffSecurityRequirements(oldE.security, newE.security, ['security'], fieldChanges);
36
+ diffParameters(oldE.parameters, newE.parameters, ['parameters'], fieldChanges);
37
+ diffRequestBody(oldE.requestBody, newE.requestBody, ['requestBody'], fieldChanges);
38
+ diffResponses(oldE.responses, newE.responses, ['responses'], fieldChanges);
39
+
40
+ if (fieldChanges.length > 0) {
41
+ diffs.push({ type: 'changed', endpointId: id, path: newE.path, method: newE.method, oldEndpoint: oldE, newEndpoint: newE, fieldChanges });
42
+ }
43
+ }
44
+ }
45
+
46
+ return diffs;
47
+ }
48
+
49
+ function diffParameters(oldParams: Parameter[], newParams: Parameter[], path: string[], changes: FieldChange[]) {
50
+ const oldMap = new Map(oldParams.map(p => [`${p.name}:${p.in}`, p]));
51
+ const newMap = new Map(newParams.map(p => [`${p.name}:${p.in}`, p]));
52
+ const allIds = new Set([...oldMap.keys(), ...newMap.keys()]);
53
+
54
+ for (const id of allIds) {
55
+ const o = oldMap.get(id);
56
+ const n = newMap.get(id);
57
+ if (o && !n) {
58
+ changes.push({ fieldPath: [...path, id], changeType: 'removed', oldValue: o });
59
+ } else if (!o && n) {
60
+ changes.push({ fieldPath: [...path, id], changeType: 'added', newValue: n });
61
+ } else if (o && n) {
62
+ if (!!o.required !== !!n.required) {
63
+ changes.push({ fieldPath: [...path, id, 'required'], changeType: 'changed', oldValue: !!o.required, newValue: !!n.required });
64
+ }
65
+ if (!!o.deprecated !== !!n.deprecated) {
66
+ changes.push({ fieldPath: [...path, id, 'deprecated'], changeType: 'changed', oldValue: !!o.deprecated, newValue: !!n.deprecated });
67
+ }
68
+ diffSchema(o.schema, n.schema, [...path, id, 'schema'], changes);
69
+ }
70
+ }
71
+ }
72
+
73
+ function diffRequestBody(oldReq: RequestBody | undefined, newReq: RequestBody | undefined, path: string[], changes: FieldChange[]) {
74
+ if (!oldReq && !newReq) return;
75
+ if (oldReq && !newReq) {
76
+ changes.push({ fieldPath: path, changeType: 'removed', oldValue: oldReq });
77
+ return;
78
+ }
79
+ if (!oldReq && newReq) {
80
+ changes.push({ fieldPath: path, changeType: 'added', newValue: newReq });
81
+ return;
82
+ }
83
+ if (oldReq && newReq) {
84
+ if (!!oldReq.required !== !!newReq.required) {
85
+ changes.push({ fieldPath: [...path, 'required'], changeType: 'changed', oldValue: !!oldReq.required, newValue: !!newReq.required });
86
+ }
87
+ const oldMedia = new Set(Object.keys(oldReq.content));
88
+ const newMedia = new Set(Object.keys(newReq.content));
89
+ for (const m of newMedia) {
90
+ if (!oldMedia.has(m)) changes.push({ fieldPath: [...path, 'content', m], changeType: 'added', newValue: newReq.content[m] });
91
+ }
92
+ for (const m of oldMedia) {
93
+ if (!newMedia.has(m)) {
94
+ changes.push({ fieldPath: [...path, 'content', m], changeType: 'removed', oldValue: oldReq.content[m] });
95
+ } else {
96
+ diffSchema(oldReq.content[m].schema, newReq.content[m].schema, [...path, 'content', m, 'schema'], changes);
97
+ }
98
+ }
99
+ }
100
+ }
101
+
102
+ function diffResponses(oldRes: ResponseDef[], newRes: ResponseDef[], path: string[], changes: FieldChange[]) {
103
+ const oldMap = new Map(oldRes.map(r => [r.statusCode, r]));
104
+ const newMap = new Map(newRes.map(r => [r.statusCode, r]));
105
+ const allCodes = new Set([...oldMap.keys(), ...newMap.keys()]);
106
+
107
+ for (const code of allCodes) {
108
+ const o = oldMap.get(code);
109
+ const n = newMap.get(code);
110
+ if (o && !n) {
111
+ changes.push({ fieldPath: [...path, code], changeType: 'removed', oldValue: o });
112
+ } else if (!o && n) {
113
+ changes.push({ fieldPath: [...path, code], changeType: 'added', newValue: n });
114
+ } else if (o && n) {
115
+ const oldMedia = new Set(Object.keys(o.content || {}));
116
+ const newMedia = new Set(Object.keys(n.content || {}));
117
+ for (const m of newMedia) {
118
+ if (!oldMedia.has(m)) changes.push({ fieldPath: [...path, code, 'content', m], changeType: 'added', newValue: n.content[m] });
119
+ }
120
+ for (const m of oldMedia) {
121
+ if (!newMedia.has(m)) {
122
+ changes.push({ fieldPath: [...path, code, 'content', m], changeType: 'removed', oldValue: o.content[m] });
123
+ } else {
124
+ diffSchema(o.content[m].schema, n.content[m].schema, [...path, code, 'content', m, 'schema'], changes);
125
+ }
126
+ }
127
+ const oldHeaders = new Set(Object.keys(o.headers || {}));
128
+ const newHeaders = new Set(Object.keys(n.headers || {}));
129
+ for (const h of newHeaders) {
130
+ if (!oldHeaders.has(h)) changes.push({ fieldPath: [...path, code, 'headers', h], changeType: 'added', newValue: n.headers![h] });
131
+ }
132
+ for (const h of oldHeaders) {
133
+ if (!newHeaders.has(h)) {
134
+ changes.push({ fieldPath: [...path, code, 'headers', h], changeType: 'removed', oldValue: o.headers![h] });
135
+ } else {
136
+ if (!!o.headers![h].required !== !!n.headers![h].required) {
137
+ changes.push({ fieldPath: [...path, code, 'headers', h, 'required'], changeType: 'changed', oldValue: !!o.headers![h].required, newValue: !!n.headers![h].required });
138
+ }
139
+ diffSchema(o.headers![h].schema, n.headers![h].schema, [...path, code, 'headers', h, 'schema'], changes);
140
+ }
141
+ }
142
+ }
143
+ }
144
+ }
@@ -0,0 +1,13 @@
1
+ import type { NormalizedAST, DiffSet } from '../types/index.js';
2
+ import { diffEndpoints } from './endpoint-differ.js';
3
+ import { diffSecuritySchemes } from './auth-differ.js';
4
+ import { diffServers } from './server-differ.js';
5
+
6
+ export function computeDiff(oldAST: NormalizedAST, newAST: NormalizedAST): DiffSet {
7
+ return {
8
+ endpointDiffs: diffEndpoints(oldAST.endpoints, newAST.endpoints),
9
+ schemaDiffs: [],
10
+ securityDiffs: diffSecuritySchemes(oldAST.security, newAST.security),
11
+ serverDiffs: diffServers(oldAST.servers, newAST.servers)
12
+ };
13
+ }
@@ -0,0 +1,70 @@
1
+ import type { Schema, FieldChange } from '../types/index.js';
2
+
3
+ export function diffSchema(oldSchema: Schema | undefined, newSchema: Schema | undefined, path: string[], changes: FieldChange[]): void {
4
+ if (!oldSchema && !newSchema) return;
5
+
6
+ if (!oldSchema && newSchema) {
7
+ changes.push({ fieldPath: path, changeType: 'added', newValue: newSchema });
8
+ return;
9
+ }
10
+
11
+ if (oldSchema && !newSchema) {
12
+ changes.push({ fieldPath: path, changeType: 'removed', oldValue: oldSchema });
13
+ return;
14
+ }
15
+
16
+ if (oldSchema && newSchema) {
17
+ if (oldSchema.type !== newSchema.type) {
18
+ const oldTypes = Array.isArray(oldSchema.type) ? oldSchema.type : [oldSchema.type].filter(Boolean);
19
+ const newTypes = Array.isArray(newSchema.type) ? newSchema.type : [newSchema.type].filter(Boolean);
20
+
21
+ const oldTypeStr = oldTypes.sort().join(',');
22
+ const newTypeStr = newTypes.sort().join(',');
23
+
24
+ if (oldTypeStr !== newTypeStr) {
25
+ changes.push({ fieldPath: [...path, 'type'], changeType: 'changed', oldValue: oldSchema.type, newValue: newSchema.type });
26
+ }
27
+ }
28
+
29
+ if (oldSchema.nullable !== newSchema.nullable) {
30
+ changes.push({ fieldPath: [...path, 'nullable'], changeType: 'changed', oldValue: oldSchema.nullable, newValue: newSchema.nullable });
31
+ }
32
+
33
+ const oldReq = new Set(oldSchema.required || []);
34
+ const newReq = new Set(newSchema.required || []);
35
+ for (const req of newReq) {
36
+ if (!oldReq.has(req)) {
37
+ changes.push({ fieldPath: [...path, 'required', req], changeType: 'added', newValue: req });
38
+ }
39
+ }
40
+ for (const req of oldReq) {
41
+ if (!newReq.has(req)) {
42
+ changes.push({ fieldPath: [...path, 'required', req], changeType: 'removed', oldValue: req });
43
+ }
44
+ }
45
+
46
+ const oldEnum = new Set(oldSchema.enum || []);
47
+ const newEnum = new Set(newSchema.enum || []);
48
+ for (const val of newEnum) {
49
+ if (!oldEnum.has(val)) {
50
+ changes.push({ fieldPath: [...path, 'enum', String(val)], changeType: 'added', newValue: val });
51
+ }
52
+ }
53
+ for (const val of oldEnum) {
54
+ if (!newEnum.has(val)) {
55
+ changes.push({ fieldPath: [...path, 'enum', String(val)], changeType: 'removed', oldValue: val });
56
+ }
57
+ }
58
+
59
+ const oldProps = oldSchema.properties || {};
60
+ const newProps = newSchema.properties || {};
61
+ const allProps = new Set([...Object.keys(oldProps), ...Object.keys(newProps)]);
62
+ for (const prop of allProps) {
63
+ diffSchema(oldProps[prop], newProps[prop], [...path, 'properties', prop], changes);
64
+ }
65
+
66
+ if (oldSchema.items || newSchema.items) {
67
+ diffSchema(oldSchema.items, newSchema.items, [...path, 'items'], changes);
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,22 @@
1
+ import type { Server, ServerDiff } from '../types/index.js';
2
+
3
+ export function diffServers(oldServers: Server[], newServers: Server[]): ServerDiff[] {
4
+ const diffs: ServerDiff[] = [];
5
+ const oldUrls = new Set(oldServers.map(s => s.url));
6
+ const newUrls = new Set(newServers.map(s => s.url));
7
+
8
+ for (const server of oldServers) {
9
+ if (!newUrls.has(server.url)) {
10
+ diffs.push({ changeType: 'removed', oldServer: server });
11
+ }
12
+ }
13
+
14
+ for (const server of newServers) {
15
+ if (!oldUrls.has(server.url)) {
16
+ diffs.push({ changeType: 'added', newServer: server });
17
+ }
18
+ }
19
+
20
+ // We could diff server variables or descriptions, but the main semantic change is added/removed
21
+ return diffs;
22
+ }
package/src/index.ts ADDED
@@ -0,0 +1,92 @@
1
+ import type { ApidiffConfig, RunResult, SemanticChange } from './types/index.js';
2
+ import { loadSpec } from './loader/index.js';
3
+ import { parseSpec } from './parsers/index.js';
4
+ import { computeDiff } from './diff/index.js';
5
+ import { runRules } from './rules/index.js';
6
+ import { DEFAULT_CONFIG } from './config/index.js';
7
+
8
+ export * from './types/index.js';
9
+ export { loadConfig } from './config/index.js';
10
+ export { formatOutput } from './output/index.js';
11
+
12
+ /**
13
+ * Runs the semantic API diffing process between two specifications.
14
+ *
15
+ * @param oldSource - The path, URL, git ref, or raw string of the base specification
16
+ * @param newSource - The path, URL, git ref, or raw string of the head specification
17
+ * @param config - Optional configuration to override default rules and output settings
18
+ * @returns A promise that resolves to the RunResult containing the changes and statistics
19
+ */
20
+ export async function runContent(
21
+ oldContent: string,
22
+ newContent: string,
23
+ config: Partial<ApidiffConfig> = {}
24
+ ): Promise<RunResult> {
25
+ const fullConfig = { ...DEFAULT_CONFIG, ...config } as ApidiffConfig;
26
+ const startMs = Date.now();
27
+
28
+ const [oldAST, newAST] = await Promise.all([
29
+ parseSpec({ content: oldContent }),
30
+ parseSpec({ content: newContent }),
31
+ ]);
32
+
33
+ const diffSet = computeDiff(oldAST, newAST);
34
+ const allChanges = runRules(diffSet, { oldAST, newAST, config: fullConfig });
35
+ const changes = filterChanges(allChanges, fullConfig);
36
+
37
+ return {
38
+ changes,
39
+ stats: computeStats(changes),
40
+ oldSpecMeta: oldAST.meta,
41
+ newSpecMeta: newAST.meta,
42
+ durationMs: Date.now() - startMs,
43
+ };
44
+ }
45
+
46
+ export async function run(
47
+ oldSource: string,
48
+ newSource: string,
49
+ config: Partial<ApidiffConfig> = {}
50
+ ): Promise<RunResult> {
51
+ const fullConfig = { ...DEFAULT_CONFIG, ...config } as ApidiffConfig;
52
+ const startMs = Date.now();
53
+
54
+ const [oldRaw, newRaw] = await Promise.all([
55
+ loadSpec(oldSource),
56
+ loadSpec(newSource),
57
+ ]);
58
+
59
+ const [oldAST, newAST] = await Promise.all([
60
+ parseSpec(oldRaw),
61
+ parseSpec(newRaw),
62
+ ]);
63
+
64
+ const diffSet = computeDiff(oldAST, newAST);
65
+
66
+ const allChanges = runRules(diffSet, { oldAST, newAST, config: fullConfig });
67
+
68
+ const changes = filterChanges(allChanges, fullConfig);
69
+
70
+ return {
71
+ changes,
72
+ stats: computeStats(changes),
73
+ oldSpecMeta: oldAST.meta,
74
+ newSpecMeta: newAST.meta,
75
+ durationMs: Date.now() - startMs,
76
+ };
77
+ }
78
+
79
+ function filterChanges(changes: SemanticChange[], config: ApidiffConfig): SemanticChange[] {
80
+ return changes;
81
+ }
82
+
83
+ function computeStats(changes: SemanticChange[]) {
84
+ const stats = { breaking: 0, warning: 0, info: 0, total: 0 };
85
+ for (const c of changes) {
86
+ if (c.severity === 'breaking') stats.breaking++;
87
+ else if (c.severity === 'warning') stats.warning++;
88
+ else stats.info++;
89
+ stats.total++;
90
+ }
91
+ return stats;
92
+ }
@@ -0,0 +1,19 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { LoadError } from '../types/errors.js';
4
+
5
+ export function loadFile(source: string): { content: string; sourcePath: string } {
6
+ try {
7
+ const sourcePath = resolve(source);
8
+ const content = readFileSync(sourcePath, 'utf8');
9
+ return { content, sourcePath };
10
+ } catch (err: any) {
11
+ if (err.code === 'ENOENT') {
12
+ throw new LoadError(`file not found: ${source}`, err);
13
+ }
14
+ if (err.code === 'EACCES') {
15
+ throw new LoadError(`permission denied: ${source}`, err);
16
+ }
17
+ throw new LoadError(`failed to read file: ${source}`, err);
18
+ }
19
+ }
@@ -0,0 +1,22 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { LoadError } from '../types/errors.js';
3
+
4
+ export function loadGit(source: string): { content: string; sourcePath: string } {
5
+ // source format: "git:HEAD~1:path/to/api.yaml"
6
+ const match = source.match(/^git:([^:]+):(.+)$/);
7
+ if (!match) {
8
+ throw new LoadError(`invalid git source format: ${source}`);
9
+ }
10
+ const [, ref, filePath] = match;
11
+
12
+ try {
13
+ const content = execSync(`git show ${ref}:${filePath}`, { encoding: 'utf-8', stdio: 'pipe' });
14
+ return { content, sourcePath: filePath };
15
+ } catch (err: any) {
16
+ const stderr = err.stderr?.toString() || '';
17
+ if (stderr.includes('Not a valid object name') || stderr.includes('does not exist')) {
18
+ throw new LoadError(`git ref or path not found: ${ref}:${filePath}`, err);
19
+ }
20
+ throw new LoadError(`git command failed: ${source}`, err);
21
+ }
22
+ }
@@ -0,0 +1,32 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { loadFile } from './file-loader.js';
3
+ import { loadUrl } from './url-loader.js';
4
+ import { loadGit } from './git-loader.js';
5
+ import { LoadError } from '../types/errors.js';
6
+
7
+ export interface RawSpec {
8
+ content: string;
9
+ sourcePath?: string;
10
+ format?: 'openapi3' | 'openapi2' | 'protobuf' | 'graphql';
11
+ }
12
+
13
+ export async function loadSpec(source: string): Promise<RawSpec> {
14
+ if (source === '-') {
15
+ try {
16
+ const content = readFileSync(0, 'utf-8');
17
+ return { content, sourcePath: undefined };
18
+ } catch (err: any) {
19
+ throw new LoadError('failed to read from stdin', err);
20
+ }
21
+ }
22
+
23
+ if (source.startsWith('http://') || source.startsWith('https://')) {
24
+ return await loadUrl(source);
25
+ }
26
+
27
+ if (source.startsWith('git:')) {
28
+ return loadGit(source);
29
+ }
30
+
31
+ return loadFile(source);
32
+ }
@@ -0,0 +1,25 @@
1
+ import { LoadError } from '../types/errors.js';
2
+
3
+ export async function loadUrl(source: string): Promise<{ content: string; sourcePath: string }> {
4
+ try {
5
+ const controller = new AbortController();
6
+ const timeoutId = setTimeout(() => controller.abort(), 10000);
7
+ const response = await fetch(source, { signal: controller.signal });
8
+ clearTimeout(timeoutId);
9
+
10
+ if (!response.ok) {
11
+ throw new LoadError(`failed to fetch url: ${source} (status: ${response.status})`);
12
+ }
13
+
14
+ const content = await response.text();
15
+ return { content, sourcePath: source };
16
+ } catch (err: any) {
17
+ if (err instanceof LoadError) {
18
+ throw err;
19
+ }
20
+ if (err.name === 'AbortError') {
21
+ throw new LoadError(`timeout fetching url: ${source}`, err);
22
+ }
23
+ throw new LoadError(`network error fetching url: ${source}`, err instanceof Error ? err : undefined);
24
+ }
25
+ }