@@ -147,9 +147,11 @@ function topLevelBindings(statements: Statements): {
147147}
148148
149149/** Resolves exported handler references and tracing callbacks without importing server code. */
150- export function wrappedRouteSites ( source : string ) : Array < { verb : string ; optionsStart : number } > {
150+ export function wrappedRouteSites (
151+ source : string
152+ ) : Array < { verb : string ; builder : string ; optionsStart : number } > {
151153 const statements = parse ( source , { sourceType : 'module' , plugins : [ 'typescript' ] } ) . program . body
152- const handlers = new Map < string , number > ( )
154+ const handlers = new Map < string , { builder : string ; optionsStart : number } > ( )
153155 for ( const statement of statements ) {
154156 if ( statement . type !== 'VariableDeclaration' || statement . kind !== 'const' ) continue
155157 for ( const declaration of statement . declarations ) {
@@ -159,15 +161,18 @@ export function wrappedRouteSites(source: string): Array<{ verb: string; options
159161 continue
160162 const options = call . arguments [ 0 ]
161163 if ( options ?. type === 'ObjectExpression' && typeof options . start === 'number' ) {
162- handlers . set ( declaration . id . name , options . start + 1 )
164+ handlers . set ( declaration . id . name , {
165+ builder : call . callee . name ,
166+ optionsStart : options . start + 1 ,
167+ } )
163168 }
164169 }
165170 }
166171 const { locals, exports } = topLevelBindings ( statements )
167- const sites : Array < { verb : string ; optionsStart : number } > = [ ]
172+ const sites : Array < { verb : string ; builder : string ; optionsStart : number } > = [ ]
168173 for ( const { exported : verb , local } of exports ) {
169174 if ( ! isVerb ( verb ) ) continue
170- const found = new Set < number > ( )
175+ const found = new Set < { builder : string ; optionsStart : number } > ( )
171176 const direct = handlers . get ( local )
172177 if ( direct !== undefined ) found . add ( direct )
173178 const visit = ( value : unknown ) : void => {
@@ -178,13 +183,13 @@ export function wrappedRouteSites(source: string): Array<{ verb: string; options
178183 }
179184 const node = value as Record < string , unknown >
180185 if ( node . type === 'Identifier' && typeof node . name === 'string' ) {
181- const start = handlers . get ( node . name )
182- if ( start !== undefined ) found . add ( start )
186+ const handler = handlers . get ( node . name )
187+ if ( handler !== undefined ) found . add ( handler )
183188 }
184189 Object . values ( node ) . forEach ( visit )
185190 }
186191 if ( direct === undefined ) visit ( locals . get ( local ) )
187- for ( const optionsStart of found ) sites . push ( { verb, optionsStart } )
192+ for ( const handler of found ) sites . push ( { verb, ... handler } )
188193 }
189194 return sites
190195}
@@ -347,8 +352,12 @@ function derivedPath(file: string): string {
347352 return [ '/api' , ...segments ] . join ( '/' )
348353}
349354
350- /** Where a contract identifier is bound in a route: a builder's `contract:` key or a raw `parseRequest`. */
351- type SiteKind = 'builder' | 'raw'
355+ /**
356+ * Where a contract identifier is bound in a route: a builder's `contract:` key or a raw
357+ * `parseRequest`. `scim` is `defineScimRoute`, the one builder that rejects a `HEAD` request
358+ * against a `GET` contract instead of serving it through `methodMatchesContract`.
359+ */
360+ type SiteKind = 'builder' | 'scim' | 'raw'
352361
353362/** One route file, as every site in it is checked against. */
354363interface RouteFile {
@@ -426,13 +435,13 @@ export async function checkSite(
426435 }
427436
428437 const subject =
429- kind === 'builder '
438+ kind !== 'raw '
430439 ? `export const ${ verb } is built from \`${ identifier } \``
431440 : `export const ${ verb } parses with \`${ identifier } \``
432441 const method = contract . method . toUpperCase ( )
433- if ( method !== verb && ! ( verb === 'HEAD' && method === 'GET' ) ) {
442+ if ( method !== verb && ! ( verb === 'HEAD' && method === 'GET' && kind !== 'scim' ) ) {
434443 failures . push (
435- kind === 'builder '
444+ kind !== 'raw '
436445 ? `${ relative } : ${ subject } , which declares ${ contract . method } ${ contract . path } . Next routes by the exported symbol, so ${ verb } requests 500 and ${ contract . method } requests 404.`
437446 : `${ relative } : ${ subject } , which declares ${ contract . method } ${ contract . path } . Clients calling through the contract send ${ contract . method } and get a 405.`
438447 )
@@ -493,13 +502,14 @@ async function main() {
493502
494503 const directSites = [ ...source . matchAll ( EXPORT_RE ) ] . map ( ( match ) => ( {
495504 verb : match [ 1 ] ,
505+ builder : match [ 2 ] ,
496506 optionsStart : ( match . index ?? 0 ) + match [ 0 ] . length ,
497507 } ) )
498508 const sites =
499509 directSites . length === builderCalls
500510 ? directSites
501511 : [ ...directSites , ...wrappedRouteSites ( source ) ]
502- for ( const { verb, optionsStart } of sites ) {
512+ for ( const { verb, builder , optionsStart } of sites ) {
503513 sitesInFile += 1
504514 const options = source . slice ( optionsStart , optionsStart + OPTIONS_SCAN_CHARS )
505515
@@ -509,7 +519,8 @@ async function main() {
509519 continue
510520 }
511521 const identifier = contractKey [ 1 ]
512- if ( await checkSite ( route , 'builder' , verb , identifier , failures , verbose ) ) checked += 1
522+ const kind = builder === 'defineScimRoute' ? 'scim' : 'builder'
523+ if ( await checkSite ( route , kind , verb , identifier , failures , verbose ) ) checked += 1
513524 }
514525
515526 if ( sitesInFile < builderCalls ) {
0 commit comments