Skip to content

Commit eda2c8f

Browse files
committed
fix: fix component change detection in endpoints with multiple methods
Updates the data structure to store paths affected by component changes. Now each path/method combo tracks its own list of component changes.
1 parent 0f1d167 commit eda2c8f

File tree

4 files changed

+304
-83
lines changed

4 files changed

+304
-83
lines changed

scripts/api-diff.js

Lines changed: 91 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const changes = {
1010
removed: {}, // Group by path
1111
modified: {}, // Group by path
1212
components: new Set(), // Track changed components
13-
affectedByComponents: {} // Track paths affected by component changes
13+
affectedByComponents: new Map() // Track path/method combinations affected by component changes
1414
};
1515

1616
// Helper function to track component references
@@ -61,22 +61,21 @@ function findAffectedPaths() {
6161
if (changes.components.size === 0) return;
6262

6363
Object.entries(currentSpec.paths || {}).forEach(([path, methods]) => {
64-
const affectedMethods = [];
6564
Object.entries(methods).forEach(([method, details]) => {
6665
const usedComponents = new Set();
6766
findComponentRefs(details, usedComponents);
6867

6968
for (const comp of usedComponents) {
7069
if (changes.components.has(comp)) {
71-
affectedMethods.push(method.toUpperCase());
72-
if (!changes.affectedByComponents[path]) {
73-
changes.affectedByComponents[path] = {
74-
methods: new Set(),
70+
const key = `${path}::${method.toUpperCase()}`;
71+
if (!changes.affectedByComponents.has(key)) {
72+
changes.affectedByComponents.set(key, {
73+
path,
74+
method: method.toUpperCase(),
7575
components: new Set()
76-
};
76+
});
7777
}
78-
changes.affectedByComponents[path].methods.add(method.toUpperCase());
79-
changes.affectedByComponents[path].components.add(comp);
78+
changes.affectedByComponents.get(key).components.add(comp);
8079
}
8180
}
8281
});
@@ -241,9 +240,6 @@ function findComponentUsage(details, componentName) {
241240

242241
// Generate markdown release notes
243242
function generateReleaseNotes() {
244-
let releaseDescription = '';
245-
246-
247243
const sections = [];
248244

249245
// Added endpoints
@@ -257,84 +253,99 @@ function generateReleaseNotes() {
257253
sections.push(section);
258254
}
259255

260-
// Helper function to generate route modification details
261-
function generateModifiedRouteDetails(path, changes) {
262-
let details = '';
263-
const methodsToProcess = new Set();
264-
265-
// Collect all affected methods
266-
if (changes.modified[path]) {
267-
changes.modified[path].forEach(({method}) => methodsToProcess.add(method));
268-
}
269-
if (changes.affectedByComponents[path]) {
270-
changes.affectedByComponents[path].methods.forEach(method => methodsToProcess.add(method));
271-
}
256+
// Modified endpoints
257+
if (Object.keys(changes.modified).length > 0 || changes.affectedByComponents.size > 0) {
258+
let section = '## Modified\n';
272259

273-
// Process each method
274-
Array.from(methodsToProcess)
275-
.sort()
276-
.forEach(method => {
277-
details += `- [${method}] \`${path}\`\n`;
278-
279-
// Add direct changes
280-
const directChanges = changes.modified[path]?.find(m => m.method === method);
281-
if (directChanges) {
282-
directChanges.changes.sort().forEach(change => {
283-
details += ` - ${change}\n`;
284-
});
285-
}
260+
// Group affected paths
261+
const affectedPaths = new Map();
286262

287-
// Add component changes
288-
if (changes.affectedByComponents[path]?.methods.has(method)) {
289-
const methodDetails = currentSpec.paths[path][method.toLowerCase()];
290-
Array.from(changes.affectedByComponents[path].components)
291-
.sort()
292-
.forEach(component => {
293-
const usageLocations = findComponentUsage(methodDetails, component).sort();
294-
details += ` - \`${component}\` modified in ${usageLocations.join(', ')}\n`;
295-
});
263+
// Add directly modified paths
264+
Object.entries(changes.modified).forEach(([path, methodChanges]) => {
265+
methodChanges.forEach(({method, changes: methodChanges}) => {
266+
if (!affectedPaths.has(path)) {
267+
affectedPaths.set(path, new Map());
296268
}
269+
affectedPaths.get(path).set(method, { direct: methodChanges });
297270
});
298-
return details;
299-
}
300-
301-
// Modified endpoints
302-
if (Object.keys(changes.modified).length > 0 || Object.keys(changes.affectedByComponents).length > 0) {
303-
let section = '## Modified\n';
304-
305-
// First show all directly modified paths
306-
const directlyModifiedPaths = Object.keys(changes.modified).sort();
307-
directlyModifiedPaths.forEach(path => {
308-
section += generateModifiedRouteDetails(path, changes);
309271
});
310272

311-
// Then show component-affected paths (but not ones that were directly modified)
312-
const componentAffectedEntries = Object.entries(changes.affectedByComponents)
313-
.filter(([path]) => !changes.modified[path]) // Only paths not already shown above
314-
.flatMap(([path, details]) =>
315-
Array.from(details.methods).map(method => ({path, method}))
316-
)
317-
.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
273+
// Add component-affected paths
274+
for (const [_, value] of changes.affectedByComponents) {
275+
const { path, method, components } = value;
276+
if (!affectedPaths.has(path)) {
277+
affectedPaths.set(path, new Map());
278+
}
279+
const existing = affectedPaths.get(path).get(method) || {};
280+
affectedPaths.get(path).set(method, {
281+
...existing,
282+
components: Array.from(components)
283+
});
284+
}
318285

319-
// Show first 5 component-affected method-path combinations
320-
const visibleEntries = componentAffectedEntries.slice(0, 5);
321-
const processedPaths = new Set();
286+
// Sort paths and generate output
287+
const sortedPaths = Array.from(affectedPaths.keys()).sort();
288+
const visiblePaths = sortedPaths.slice(0, 5);
322289

323-
visibleEntries.forEach(({path}) => {
324-
if (!processedPaths.has(path)) {
325-
section += generateModifiedRouteDetails(path, changes);
326-
processedPaths.add(path);
327-
}
290+
// Show first 5 paths
291+
visiblePaths.forEach(path => {
292+
const methods = affectedPaths.get(path);
293+
Array.from(methods.entries())
294+
.sort(([a], [b]) => a.localeCompare(b))
295+
.forEach(([method, info]) => {
296+
section += `- [${method}] \`${path}\`\n`;
297+
298+
// Show direct changes
299+
if (info.direct) {
300+
info.direct.sort().forEach(change => {
301+
section += ` - ${change}\n`;
302+
});
303+
}
304+
305+
// Show component changes
306+
if (info.components) {
307+
const methodDetails = currentSpec.paths[path][method.toLowerCase()];
308+
info.components
309+
.sort()
310+
.forEach(component => {
311+
const usageLocations = findComponentUsage(methodDetails, component).sort();
312+
if (usageLocations.length > 0) {
313+
section += ` - \`${component}\` modified in ${usageLocations.join(', ')}\n`;
314+
}
315+
});
316+
}
317+
});
328318
});
329319

330-
// Collapse any remaining entries
331-
const remainingEntries = componentAffectedEntries.slice(5);
332-
if (remainingEntries.length > 0) {
320+
// Collapse remaining paths
321+
const remainingPaths = sortedPaths.slice(5);
322+
if (remainingPaths.length > 0) {
333323
section += '\n<details><summary>Show more routes affected by component changes...</summary>\n\n';
334-
const remainingPaths = new Set();
335-
remainingEntries.forEach(({path}) => remainingPaths.add(path));
336-
Array.from(remainingPaths).sort().forEach(path => {
337-
section += generateModifiedRouteDetails(path, changes);
324+
remainingPaths.forEach(path => {
325+
const methods = affectedPaths.get(path);
326+
Array.from(methods.entries())
327+
.sort(([a], [b]) => a.localeCompare(b))
328+
.forEach(([method, info]) => {
329+
section += `- [${method}] \`${path}\`\n`;
330+
331+
if (info.direct) {
332+
info.direct.sort().forEach(change => {
333+
section += ` - ${change}\n`;
334+
});
335+
}
336+
337+
if (info.components) {
338+
const methodDetails = currentSpec.paths[path][method.toLowerCase()];
339+
info.components
340+
.sort()
341+
.forEach(component => {
342+
const usageLocations = findComponentUsage(methodDetails, component).sort();
343+
if (usageLocations.length > 0) {
344+
section += ` - \`${component}\` modified in ${usageLocations.join(', ')}\n`;
345+
}
346+
});
347+
}
348+
});
338349
});
339350
section += '</details>\n';
340351
}
@@ -353,17 +364,14 @@ function generateReleaseNotes() {
353364
sections.push(section);
354365
}
355366

356-
357367
// Sort sections alphabetically and combine
358368
sections.sort((a, b) => {
359369
const titleA = a.split('\n')[0];
360370
const titleB = b.split('\n')[0];
361371
return titleA.localeCompare(titleB);
362372
});
363373

364-
releaseDescription += sections.join('\n');
365-
366-
return releaseDescription;
374+
return sections.join('\n');
367375
}
368376

369377
// Main execution
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
{
2+
"openapi": "3.0.0",
3+
"info": {
4+
"title": "Test API",
5+
"description": "A sample API for testing"
6+
},
7+
"servers": [
8+
{
9+
"url": "https://api.example.com/v1"
10+
}
11+
],
12+
"paths": {
13+
"/user": {
14+
"get": {
15+
"summary": "Get a list of users",
16+
"responses": {
17+
"200": {
18+
"description": "A list of users",
19+
"content": {
20+
"application/json": {
21+
"schema": {
22+
"type": "array",
23+
"items": {
24+
"$ref": "#/components/schemas/User"
25+
}
26+
}
27+
}
28+
}
29+
}
30+
}
31+
},
32+
"post": {
33+
"summary": "Create a new user profile",
34+
"requestBody": {
35+
"required": true,
36+
"content": {
37+
"application/json": {
38+
"schema": {
39+
"$ref": "#/components/schemas/NewUser"
40+
}
41+
}
42+
}
43+
},
44+
"responses": {
45+
"201": {
46+
"description": "User created successfully",
47+
"content": {
48+
"application/json": {
49+
"schema": {
50+
"$ref": "#/components/schemas/UserCreated"
51+
}
52+
}
53+
}
54+
}
55+
}
56+
}
57+
}
58+
},
59+
"components": {
60+
"schemas": {
61+
"User": {
62+
"type": "object",
63+
"properties": {
64+
"userId": {
65+
"type": "string"
66+
},
67+
"name": {
68+
"type": "string"
69+
},
70+
"email": {
71+
"$ref": "#/components/schemas/Email"
72+
}
73+
}
74+
},
75+
"UserCreated": {
76+
"type": "object",
77+
"properties": {
78+
"userId": {
79+
"type": "string"
80+
}
81+
}
82+
},
83+
"Email": {
84+
"type": "string",
85+
"format": "email"
86+
},
87+
"NewUser": {
88+
"type": "object",
89+
"required": [
90+
"name",
91+
"email"
92+
],
93+
"properties": {
94+
"name": {
95+
"type": "string"
96+
},
97+
"email": {
98+
"type": "string"
99+
}
100+
}
101+
}
102+
}
103+
}
104+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
## Modified
2+
- [GET] `/user`
3+
- `User` modified in responses
4+
- [POST] `/user`
5+
- `UserCreated` modified in responses

0 commit comments

Comments
 (0)