@@ -14,13 +14,21 @@ const changes = {
1414} ;
1515
1616// Helper function to track component references
17- function findComponentRefs ( obj , components ) {
17+ function findComponentRefs ( obj , components , spec = currentSpec ) {
1818 if ( ! obj ) return ;
1919 if ( typeof obj === 'object' ) {
2020 if ( obj [ '$ref' ] && obj [ '$ref' ] . startsWith ( '#/components/' ) ) {
21- components . add ( obj [ '$ref' ] . split ( '/' ) . pop ( ) ) ;
21+ const componentName = obj [ '$ref' ] . split ( '/' ) . pop ( ) ;
22+ components . add ( componentName ) ;
23+
24+ // Follow the reference to check nested components
25+ const [ _ , category , name ] = obj [ '$ref' ] . split ( '/' ) ;
26+ const referencedComponent = spec . components ?. [ category ] ?. [ name ] ;
27+ if ( referencedComponent ) {
28+ findComponentRefs ( referencedComponent , components , spec ) ;
29+ }
2230 }
23- Object . values ( obj ) . forEach ( value => findComponentRefs ( value , components ) ) ;
31+ Object . values ( obj ) . forEach ( value => findComponentRefs ( value , components , spec ) ) ;
2432 }
2533}
2634
@@ -31,9 +39,18 @@ function compareComponents() {
3139
3240 for ( const [ category , components ] of Object . entries ( currComps ) ) {
3341 for ( const [ name , def ] of Object . entries ( components ) ) {
34- if ( ! prevComps [ category ] ?. [ name ] ||
35- JSON . stringify ( prevComps [ category ] [ name ] ) !== JSON . stringify ( def ) ) {
42+ const prevDef = prevComps [ category ] ?. [ name ] ;
43+ if ( ! prevDef || JSON . stringify ( prevDef ) !== JSON . stringify ( def ) ) {
3644 changes . components . add ( name ) ;
45+
46+ // Also check which components reference this changed component
47+ Object . entries ( currComps [ category ] || { } ) . forEach ( ( [ otherName , otherDef ] ) => {
48+ const refsSet = new Set ( ) ;
49+ findComponentRefs ( otherDef , refsSet ) ;
50+ if ( refsSet . has ( name ) ) {
51+ changes . components . add ( otherName ) ;
52+ }
53+ } ) ;
3754 }
3855 }
3956 }
@@ -110,33 +127,91 @@ function getChanges(previous, current) {
110127 return changes ;
111128}
112129
130+ // Helper function to check if a schema references a component or its dependencies
131+ function schemaReferencesComponent ( schema , componentName , visitedRefs = new Set ( ) ) {
132+ if ( ! schema ) return false ;
133+
134+ // Prevent infinite recursion
135+ const schemaKey = JSON . stringify ( schema ) ;
136+ if ( visitedRefs . has ( schemaKey ) ) return false ;
137+ visitedRefs . add ( schemaKey ) ;
138+
139+ // Direct reference check
140+ if ( schema . $ref ) {
141+ const refPath = schema . $ref ;
142+ if ( refPath === `#/components/schemas/${ componentName } ` ) return true ;
143+
144+ // Follow the reference to check nested components
145+ const [ _ , category , name ] = refPath . split ( '/' ) ;
146+ const referencedComponent = currentSpec . components ?. [ category ] ?. [ name ] ;
147+ if ( referencedComponent && schemaReferencesComponent ( referencedComponent , componentName , visitedRefs ) ) {
148+ return true ;
149+ }
150+ }
151+
152+ // Check combiners (oneOf, anyOf, allOf)
153+ for ( const combiner of [ 'oneOf' , 'anyOf' , 'allOf' ] ) {
154+ if ( schema [ combiner ] && Array . isArray ( schema [ combiner ] ) ) {
155+ if ( schema [ combiner ] . some ( s => schemaReferencesComponent ( s , componentName , visitedRefs ) ) ) {
156+ return true ;
157+ }
158+ }
159+ }
160+
161+ // Check properties if it's an object
162+ if ( schema . properties ) {
163+ if ( Object . values ( schema . properties ) . some ( prop =>
164+ schemaReferencesComponent ( prop , componentName , visitedRefs ) ) ) {
165+ return true ;
166+ }
167+ }
168+
169+ // Check array items
170+ if ( schema . items && schemaReferencesComponent ( schema . items , componentName , visitedRefs ) ) {
171+ return true ;
172+ }
173+
174+ return false ;
175+ }
176+
113177// Helper function to detect where a component is used in an endpoint
114178function findComponentUsage ( details , componentName ) {
115179 const usage = [ ] ;
116180
117181 // Check parameters
118182 if ( details . parameters ) {
119183 const hasComponent = details . parameters . some ( p =>
120- ( p . $ref && p . $ref . includes ( componentName ) ) ||
121- ( p . schema && p . schema . $ref && p . schema . $ref . includes ( componentName ) )
184+ ( p . $ref && schemaReferencesComponent ( { $ref : p . $ref } , componentName ) ) ||
185+ ( p . schema && schemaReferencesComponent ( p . schema , componentName ) )
122186 ) ;
123187 if ( hasComponent ) usage . push ( 'parameters' ) ;
124188 }
125189
126190 // Check requestBody
127- if ( details . requestBody &&
128- details . requestBody . content &&
129- Object . values ( details . requestBody . content ) . some ( c =>
130- c . schema && c . schema . $ref && c . schema . $ref . includes ( componentName ) ) ) {
131- usage . push ( 'requestBody' ) ;
191+ if ( details . requestBody ) {
192+ let hasComponent = false ;
193+ if ( details . requestBody . $ref ) {
194+ hasComponent = schemaReferencesComponent ( { $ref : details . requestBody . $ref } , componentName ) ;
195+ } else if ( details . requestBody . content ) {
196+ hasComponent = Object . values ( details . requestBody . content ) . some ( c =>
197+ c . schema && schemaReferencesComponent ( c . schema , componentName )
198+ ) ;
199+ }
200+ if ( hasComponent ) usage . push ( 'requestBody' ) ;
132201 }
133202
134203 // Check responses
135- if ( details . responses &&
136- Object . values ( details . responses ) . some ( r =>
137- r . content && Object . values ( r . content ) . some ( c =>
138- c . schema && c . schema . $ref && c . schema . $ref . includes ( componentName ) ) ) ) {
139- usage . push ( 'responses' ) ;
204+ if ( details . responses ) {
205+ const hasComponent = Object . entries ( details . responses ) . some ( ( [ code , r ] ) => {
206+ if ( r . $ref ) return schemaReferencesComponent ( { $ref : r . $ref } , componentName ) ;
207+ if ( r . content ) {
208+ return Object . values ( r . content ) . some ( c =>
209+ c . schema && schemaReferencesComponent ( c . schema , componentName )
210+ ) ;
211+ }
212+ return false ;
213+ } ) ;
214+ if ( hasComponent ) usage . push ( 'responses' ) ;
140215 }
141216
142217 return usage ;
@@ -160,56 +235,88 @@ function generateReleaseNotes() {
160235 sections . push ( section ) ;
161236 }
162237
163- // Modified endpoints
164- if ( Object . keys ( changes . modified ) . length > 0 || Object . keys ( changes . affectedByComponents ) . length > 0 ) {
165- let section = '## Modified\n' ;
238+ // Helper function to generate route modification details
239+ function generateModifiedRouteDetails ( path , changes ) {
240+ let details = '' ;
241+ const methodsToProcess = new Set ( ) ;
166242
167- // Combine and sort all modified paths
168- const allModifiedPaths = new Set ( [
169- ...Object . keys ( changes . modified ) ,
170- ...Object . keys ( changes . affectedByComponents )
171- ] ) ;
243+ // Collect all affected methods
244+ if ( changes . modified [ path ] ) {
245+ changes . modified [ path ] . forEach ( ( { method} ) => methodsToProcess . add ( method ) ) ;
246+ }
247+ if ( changes . affectedByComponents [ path ] ) {
248+ changes . affectedByComponents [ path ] . methods . forEach ( method => methodsToProcess . add ( method ) ) ;
249+ }
172250
173- Array . from ( allModifiedPaths )
251+ // Process each method
252+ Array . from ( methodsToProcess )
174253 . sort ( )
175- . forEach ( path => {
176- // Handle both direct modifications and component changes for each path
177- const methodsToProcess = new Set ( ) ;
254+ . forEach ( method => {
255+ details += `- [${ method } ] \`${ path } \`\n` ;
178256
179- // Collect all affected methods
180- if ( changes . modified [ path ] ) {
181- changes . modified [ path ] . forEach ( ( { method} ) => methodsToProcess . add ( method ) ) ;
257+ // Add direct changes
258+ const directChanges = changes . modified [ path ] ?. find ( m => m . method === method ) ;
259+ if ( directChanges ) {
260+ directChanges . changes . sort ( ) . forEach ( change => {
261+ details += ` - ${ change } \n` ;
262+ } ) ;
182263 }
183- if ( changes . affectedByComponents [ path ] ) {
184- changes . affectedByComponents [ path ] . methods . forEach ( method => methodsToProcess . add ( method ) ) ;
264+
265+ // Add component changes
266+ if ( changes . affectedByComponents [ path ] ?. methods . has ( method ) ) {
267+ const methodDetails = currentSpec . paths [ path ] [ method . toLowerCase ( ) ] ;
268+ Array . from ( changes . affectedByComponents [ path ] . components )
269+ . sort ( )
270+ . forEach ( component => {
271+ const usageLocations = findComponentUsage ( methodDetails , component ) . sort ( ) ;
272+ details += ` - \`${ component } \` modified in ${ usageLocations . join ( ', ' ) } \n` ;
273+ } ) ;
185274 }
275+ } ) ;
276+ return details ;
277+ }
186278
187- // Process each method
188- Array . from ( methodsToProcess )
189- . sort ( )
190- . forEach ( method => {
191- section += `- [${ method } ] \`${ path } \`\n` ;
192-
193- // Add direct changes
194- const directChanges = changes . modified [ path ] ?. find ( m => m . method === method ) ;
195- if ( directChanges ) {
196- directChanges . changes . sort ( ) . forEach ( change => {
197- section += ` - ${ change } \n` ;
198- } ) ;
199- }
200-
201- // Add component changes
202- if ( changes . affectedByComponents [ path ] ?. methods . has ( method ) ) {
203- const methodDetails = currentSpec . paths [ path ] [ method . toLowerCase ( ) ] ;
204- Array . from ( changes . affectedByComponents [ path ] . components )
205- . sort ( )
206- . forEach ( component => {
207- const usageLocations = findComponentUsage ( methodDetails , component ) . sort ( ) ;
208- section += ` - \`${ component } \` modified in ${ usageLocations . join ( ', ' ) } \n` ;
209- } ) ;
210- }
211- } ) ;
279+ // Modified endpoints
280+ if ( Object . keys ( changes . modified ) . length > 0 || Object . keys ( changes . affectedByComponents ) . length > 0 ) {
281+ let section = '## Modified\n' ;
282+
283+ // First show all directly modified paths
284+ const directlyModifiedPaths = Object . keys ( changes . modified ) . sort ( ) ;
285+ directlyModifiedPaths . forEach ( path => {
286+ section += generateModifiedRouteDetails ( path , changes ) ;
287+ } ) ;
288+
289+ // Then show component-affected paths (but not ones that were directly modified)
290+ const componentAffectedEntries = Object . entries ( changes . affectedByComponents )
291+ . filter ( ( [ path ] ) => ! changes . modified [ path ] ) // Only paths not already shown above
292+ . flatMap ( ( [ path , details ] ) =>
293+ Array . from ( details . methods ) . map ( method => ( { path, method} ) )
294+ )
295+ . sort ( ( a , b ) => a . path . localeCompare ( b . path ) || a . method . localeCompare ( b . method ) ) ;
296+
297+ // Show first 5 component-affected method-path combinations
298+ const visibleEntries = componentAffectedEntries . slice ( 0 , 5 ) ;
299+ const processedPaths = new Set ( ) ;
300+
301+ visibleEntries . forEach ( ( { path} ) => {
302+ if ( ! processedPaths . has ( path ) ) {
303+ section += generateModifiedRouteDetails ( path , changes ) ;
304+ processedPaths . add ( path ) ;
305+ }
306+ } ) ;
307+
308+ // Collapse any remaining entries
309+ const remainingEntries = componentAffectedEntries . slice ( 5 ) ;
310+ if ( remainingEntries . length > 0 ) {
311+ section += '\n<details><summary>Show more routes affected by component changes...</summary>\n\n' ;
312+ const remainingPaths = new Set ( ) ;
313+ remainingEntries . forEach ( ( { path} ) => remainingPaths . add ( path ) ) ;
314+ Array . from ( remainingPaths ) . sort ( ) . forEach ( path => {
315+ section += generateModifiedRouteDetails ( path , changes ) ;
212316 } ) ;
317+ section += '</details>\n' ;
318+ }
319+
213320 sections . push ( section ) ;
214321 }
215322
0 commit comments