@@ -34,14 +34,6 @@ export class CompileRule implements Rule {
3434 // Rule output is easier to read if "tsp compile" stderr is redirected to stdOutput
3535 stdOutput += stderr ;
3636
37- if (
38- stdout . toLowerCase ( ) . includes ( "no emitter was configured" ) ||
39- stdout . toLowerCase ( ) . includes ( "no output was generated" )
40- ) {
41- success = false ;
42- errorOutput += "No emitter was configured and/or no output was generated." ;
43- }
44-
4537 if ( success ) {
4638 if ( ! err ) {
4739 // Check for *extra* typespec-generated swagger files under the output folder, which
@@ -75,145 +67,145 @@ export class CompileRule implements Rule {
7567 stdOutput += outputSwaggers . join ( "\n" ) + "\n" ;
7668
7769 if ( outputSwaggers . length === 0 ) {
78- throw new Error ( "No generated swaggers found in output of 'tsp compile'" ) ;
79- }
70+ stdOutput += "No generated swaggers found, skipping extra swagger check.\n" ;
71+ } else {
72+ // ../resource-manager/Microsoft.Contoso
73+ const outputFolder = dirname ( dirname ( dirname ( outputSwaggers [ 0 ] ) ) ) ;
74+ const outputFilename = basename ( outputSwaggers [ 0 ] ) ;
75+
76+ stdOutput += "\nOutput folder:\n" ;
77+ stdOutput += outputFolder + "\n" ;
78+
79+ // Filter to only specs matching the folder and filename extracted from the first output-file.
80+ // Necessary to handle multi-project specs like keyvault.
81+ //
82+ // Globby only accepts patterns like posix paths.
83+ const pattern = path . posix . join ( ...outputFolder . split ( path . sep ) , "**" , outputFilename ) ;
84+ const allSwaggers = ( await globby ( pattern , { ignore : [ "**/examples/**" ] } ) ) . map (
85+ // Globby always returns posix paths
86+ ( p ) => normalize ( p ) ,
87+ ) ;
88+
89+ // Filter to files generated by TypeSpec
90+ const tspGeneratedSwaggers = await filterAsync (
91+ allSwaggers ,
92+ async ( swaggerPath : string ) => {
93+ const swaggerText = await readFile ( swaggerPath , { encoding : "utf8" } ) ;
94+ const swaggerObj = JSON . parse ( swaggerText ) as {
95+ info ?: Record < string , unknown > ;
96+ } ;
97+ return (
98+ swaggerObj [ "info" ] ?. [ "x-typespec-generated" ] !== undefined ||
99+ swaggerObj [ "info" ] ?. [ "x-cadl-generated" ] !== undefined
100+ ) ;
101+ } ,
102+ ) ;
103+
104+ stdOutput += `\nSwaggers matching output folder and filename:\n` ;
105+ stdOutput += tspGeneratedSwaggers . join ( "\n" ) + "\n" ;
106+
107+ const suppressedSwaggers = await filterAsync (
108+ tspGeneratedSwaggers ,
109+ async ( swaggerPath : string ) => {
110+ const suppressions = await getSuppressions ( swaggerPath ) ;
111+
112+ const extraSwaggerSuppressions = suppressions . filter (
113+ ( s ) => s . rules ?. includes ( this . name ) && s . subRules ?. includes ( "ExtraSwagger" ) ,
114+ ) ;
115+
116+ // Each path must specify a single version (without wildcards) under "preview|stable"
117+ //
118+ // Allowed: data-plane/Azure.Contoso.WidgetManager/preview/2022-11-01-preview/**/*.json
119+ // Disallowed: data-plane/Azure.Contoso.WidgetManager/preview/**/*.json
120+ // Disallowed: data-plane/**/*.json
121+ //
122+ // Include "." since a few specs use versions like "X.Y" instead of "YYYY-MM-DD"
123+ const singleVersionPattern = "/(preview|stable)/[A-Za-z0-9._-]+/" ;
124+
125+ for ( const suppression of extraSwaggerSuppressions ) {
126+ for ( const p of suppression . paths ) {
127+ if ( ! p . match ( singleVersionPattern ) ) {
128+ throw new Error (
129+ `Invalid path '${ p } '. Path must only include one version per suppression.` ,
130+ ) ;
131+ }
132+ }
133+ }
80134
81- // ../resource-manager/Microsoft.Contoso
82- const outputFolder = dirname ( dirname ( dirname ( outputSwaggers [ 0 ] ) ) ) ;
83- const outputFilename = basename ( outputSwaggers [ 0 ] ) ;
135+ return extraSwaggerSuppressions . length > 0 ;
136+ } ,
137+ ) ;
84138
85- stdOutput += "\nOutput folder :\n" ;
86- stdOutput += outputFolder + "\n" ;
139+ stdOutput += `\nSwaggers excluded via suppressions.yaml :\n` ;
140+ stdOutput += suppressedSwaggers . join ( "\n" ) + "\n" ;
87141
88- // Filter to only specs matching the folder and filename extracted from the first output-file.
89- // Necessary to handle multi-project specs like keyvault.
90- //
91- // Globby only accepts patterns like posix paths.
92- const pattern = path . posix . join ( ...outputFolder . split ( path . sep ) , "**" , outputFilename ) ;
93- const allSwaggers = ( await globby ( pattern , { ignore : [ "**/examples/**" ] } ) ) . map (
94- // Globby always returns posix paths
95- ( p ) => normalize ( p ) ,
96- ) ;
97-
98- // Filter to files generated by TypeSpec
99- const tspGeneratedSwaggers = await filterAsync (
100- allSwaggers ,
101- async ( swaggerPath : string ) => {
102- const swaggerText = await readFile ( swaggerPath , { encoding : "utf8" } ) ;
103- const swaggerObj = JSON . parse ( swaggerText ) as {
104- info ?: Record < string , unknown > ;
142+ const remainingSwaggers = tspGeneratedSwaggers . filter (
143+ ( s ) => ! suppressedSwaggers . includes ( s ) ,
144+ ) ;
145+
146+ stdOutput += `\nRemaining swaggers:\n` ;
147+ stdOutput += remainingSwaggers . join ( "\n" ) + "\n" ;
148+
149+ const extraSwaggers = remainingSwaggers . filter ( ( s ) => ! outputSwaggers . includes ( s ) ) ;
150+
151+ if ( extraSwaggers . length > 0 ) {
152+ // Helper function to extract version from swagger path
153+ // Normalize to POSIX path for consistent pattern matching
154+ const extractVersion = ( swaggerPath : string ) : string | null => {
155+ const posixPath = swaggerPath . split ( path . sep ) . join ( path . posix . sep ) ;
156+ const match = posixPath . match ( / \/ ( p r e v i e w | s t a b l e ) \/ ( [ ^ / ] + ) \/ / ) ;
157+ return match ? match [ 2 ] : null ;
105158 } ;
106- return (
107- swaggerObj [ "info" ] ?. [ "x-typespec-generated" ] !== undefined ||
108- swaggerObj [ "info" ] ?. [ "x-cadl-generated" ] !== undefined
109- ) ;
110- } ,
111- ) ;
112-
113- stdOutput += `\nSwaggers matching output folder and filename:\n` ;
114- stdOutput += tspGeneratedSwaggers . join ( "\n" ) + "\n" ;
115-
116- const suppressedSwaggers = await filterAsync (
117- tspGeneratedSwaggers ,
118- async ( swaggerPath : string ) => {
119- const suppressions = await getSuppressions ( swaggerPath ) ;
120-
121- const extraSwaggerSuppressions = suppressions . filter (
122- ( s ) => s . rules ?. includes ( this . name ) && s . subRules ?. includes ( "ExtraSwagger" ) ,
123- ) ;
124-
125- // Each path must specify a single version (without wildcards) under "preview|stable"
126- //
127- // Allowed: data-plane/Azure.Contoso.WidgetManager/preview/2022-11-01-preview/**/*.json
128- // Disallowed: data-plane/Azure.Contoso.WidgetManager/preview/**/*.json
129- // Disallowed: data-plane/**/*.json
130- //
131- // Include "." since a few specs use versions like "X.Y" instead of "YYYY-MM-DD"
132- const singleVersionPattern = "/(preview|stable)/[A-Za-z0-9._-]+/" ;
133-
134- for ( const suppression of extraSwaggerSuppressions ) {
135- for ( const p of suppression . paths ) {
136- if ( ! p . match ( singleVersionPattern ) ) {
137- throw new Error (
138- `Invalid path '${ p } '. Path must only include one version per suppression.` ,
139- ) ;
140- }
159+
160+ // Check if all extra swaggers are preview versions
161+ const allArePreview = extraSwaggers . every ( ( s ) => {
162+ const posixPath = s . split ( path . sep ) . join ( path . posix . sep ) ;
163+ return posixPath . includes ( "/preview/" ) ;
164+ } ) ;
165+
166+ let isOnlyOlderPreviews = false ;
167+ if ( allArePreview ) {
168+ // Get all preview versions from tspGeneratedSwaggers
169+ const previewVersions = tspGeneratedSwaggers
170+ . filter ( ( s ) => {
171+ const posixPath = s . split ( path . sep ) . join ( path . posix . sep ) ;
172+ return posixPath . includes ( "/preview/" ) ;
173+ } )
174+ . map ( extractVersion )
175+ . filter ( ( v ) : v is string => v !== null ) ;
176+
177+ if ( previewVersions . length > 0 ) {
178+ // Find the latest preview version (sort descending)
179+ const sortedVersions = [ ...new Set ( previewVersions ) ] . sort ( ) . reverse ( ) ;
180+ const latestPreview = sortedVersions [ 0 ] ;
181+
182+ // Check if any extraSwagger is from the latest preview
183+ const hasLatestPreview = extraSwaggers . some ( ( s ) => {
184+ const version = extractVersion ( s ) ;
185+ return version === latestPreview ;
186+ } ) ;
187+
188+ isOnlyOlderPreviews = ! hasLatestPreview ;
141189 }
142190 }
143191
144- return extraSwaggerSuppressions . length > 0 ;
145- } ,
146- ) ;
147-
148- stdOutput += `\nSwaggers excluded via suppressions.yaml:\n` ;
149- stdOutput += suppressedSwaggers . join ( "\n" ) + "\n" ;
150-
151- const remainingSwaggers = tspGeneratedSwaggers . filter (
152- ( s ) => ! suppressedSwaggers . includes ( s ) ,
153- ) ;
154-
155- stdOutput += `\nRemaining swaggers:\n` ;
156- stdOutput += remainingSwaggers . join ( "\n" ) + "\n" ;
157-
158- const extraSwaggers = remainingSwaggers . filter ( ( s ) => ! outputSwaggers . includes ( s ) ) ;
159-
160- if ( extraSwaggers . length > 0 ) {
161- // Helper function to extract version from swagger path
162- // Normalize to POSIX path for consistent pattern matching
163- const extractVersion = ( swaggerPath : string ) : string | null => {
164- const posixPath = swaggerPath . split ( path . sep ) . join ( path . posix . sep ) ;
165- const match = posixPath . match ( / \/ ( p r e v i e w | s t a b l e ) \/ ( [ ^ / ] + ) \/ / ) ;
166- return match ? match [ 2 ] : null ;
167- } ;
168-
169- // Check if all extra swaggers are preview versions
170- const allArePreview = extraSwaggers . every ( ( s ) => {
171- const posixPath = s . split ( path . sep ) . join ( path . posix . sep ) ;
172- return posixPath . includes ( "/preview/" ) ;
173- } ) ;
174-
175- let isOnlyOlderPreviews = false ;
176- if ( allArePreview ) {
177- // Get all preview versions from tspGeneratedSwaggers
178- const previewVersions = tspGeneratedSwaggers
179- . filter ( ( s ) => {
180- const posixPath = s . split ( path . sep ) . join ( path . posix . sep ) ;
181- return posixPath . includes ( "/preview/" ) ;
182- } )
183- . map ( extractVersion )
184- . filter ( ( v ) : v is string => v !== null ) ;
185-
186- if ( previewVersions . length > 0 ) {
187- // Find the latest preview version (sort descending)
188- const sortedVersions = [ ...new Set ( previewVersions ) ] . sort ( ) . reverse ( ) ;
189- const latestPreview = sortedVersions [ 0 ] ;
190-
191- // Check if any extraSwagger is from the latest preview
192- const hasLatestPreview = extraSwaggers . some ( ( s ) => {
193- const version = extractVersion ( s ) ;
194- return version === latestPreview ;
195- } ) ;
196-
197- isOnlyOlderPreviews = ! hasLatestPreview ;
192+ if ( ! isOnlyOlderPreviews ) {
193+ success = false ;
194+ errorOutput += pc . red (
195+ `\nOutput folder '${ outputFolder } ' appears to contain TypeSpec-generated ` +
196+ `swagger files, not generated from the current TypeSpec sources. ` +
197+ `Perhaps you deleted a version from your TypeSpec, but didn't delete ` +
198+ `the associated swaggers?\n\n` ,
199+ ) ;
200+ errorOutput += pc . red ( extraSwaggers . join ( "\n" ) + "\n" ) ;
201+ } else {
202+ stdOutput += pc . yellow (
203+ `\nNote: Found extra preview swaggers from older versions (not the latest preview). ` +
204+ `These are allowed to remain:\n` ,
205+ ) ;
206+ stdOutput += pc . yellow ( extraSwaggers . join ( "\n" ) + "\n" ) ;
198207 }
199208 }
200-
201- if ( ! isOnlyOlderPreviews ) {
202- success = false ;
203- errorOutput += pc . red (
204- `\nOutput folder '${ outputFolder } ' appears to contain TypeSpec-generated ` +
205- `swagger files, not generated from the current TypeSpec sources. ` +
206- `Perhaps you deleted a version from your TypeSpec, but didn't delete ` +
207- `the associated swaggers?\n\n` ,
208- ) ;
209- errorOutput += pc . red ( extraSwaggers . join ( "\n" ) + "\n" ) ;
210- } else {
211- stdOutput += pc . yellow (
212- `\nNote: Found extra preview swaggers from older versions (not the latest preview). ` +
213- `These are allowed to remain:\n` ,
214- ) ;
215- stdOutput += pc . yellow ( extraSwaggers . join ( "\n" ) + "\n" ) ;
216- }
217209 }
218210 } else {
219211 success = false ;
0 commit comments