@ -190,4 +190,120 @@ describe('proxy service template — rendered output', () => {
expect ( output ) . toContain ( 'this.restService.request<any,' ) ;
expect ( output ) . toContain ( 'this.restService.request<any,' ) ;
expect ( output . match ( /}/g ) ! . length ) . toBeGreaterThanOrEqual ( 3 ) ;
expect ( output . match ( /}/g ) ! . length ) . toBeGreaterThanOrEqual ( 3 ) ;
} ) ;
} ) ;
test . each ( [
{ name : 'string + json Accept' , body : { responseType : 'string' , responseTypeWithNamespace : 'string' , httpResponseType : 'json' , acceptHeader : 'application/problem+json' } } ,
{ name : 'string + text Accept' , body : { responseType : 'string' , responseTypeWithNamespace : 'string' , httpResponseType : 'text' , acceptHeader : 'text/csv' } } ,
{ name : 'blob + pdf Accept' , body : { responseType : 'Blob' , responseTypeWithNamespace : 'Blob' , httpResponseType : 'blob' , acceptHeader : 'application/pdf' , isBlobMethod : ( ) = > true } } ,
{ name : 'any[] degradation' , body : { responseType : 'any[]' , responseTypeWithNamespace : '[Volo.Abp.Content.IRemoteStreamContent]' } } ,
{ name : 'xml Accept only' , body : { responseType : 'any' , responseTypeWithNamespace : 'any' , acceptHeader : 'application/xml' } } ,
] ) ( 'rendered service compiles cleanly under real ts.Program ($name)' , ( { body } ) = > {
// ts.transpileModule is a single-file transform; it can't resolve @abp/ng.core
// imports. Real ts.createProgram with a host that supplies a minimal .d.ts stub
// for @abp/ng.core exercises the actual semantic checker — closer to ng build
// without the cost of a full Angular workspace.
const ts = require ( 'typescript' ) ;
const ctx = buildContext ( body as Partial < MockBody > ) ;
// Mirror what the real schematic emits: every action gets a trailing
// `config?: Partial<Rest.Config>` parameter — without it the rendered call
// site references a `config` identifier that's never declared.
// Use a permissive type so strict-mode spread doesn't trip on Partial<{...}>
// semantics; the real schematic uses `Partial<Rest.Config>` but the spread
// pattern is identical and we want to assert template *shape*, not the
// exact inference Angular's strict mode does for the upstream type.
ctx . methods [ 0 ] . signature . parameters = [ { name : 'config' , type : 'Record<string, any>' } as any ] ;
const output = render ( ctx ) ;
const abpStub = `
declare module '@abp/ng.core' {
export namespace Rest {
export interface Config {
apiName? : string ;
observe? : any ;
skipHandleError? : boolean ;
responseType? : string ;
[ key : string ] : any ;
}
export type Observe = any ;
}
export class RestService {
request < TBody , TResponse > ( req : any , config? : any ) : import ( 'rxjs' ) . Observable < TResponse > ;
}
}
` ;
const domStub = 'declare class Blob { constructor(parts?: any[], options?: any); }\n' ;
const angularCoreStub = `
declare module '@angular/core' {
export function Injectable ( opts? : any ) : ClassDecorator ;
// Mirror Angular's overload that accepts a class token and returns its instance.
export function inject < T > ( token : { new ( . . . args : any [ ] ) : T } ) : T ;
export function inject < T > ( token : any ) : T ;
}
` ;
const rxjsStub = `
declare module 'rxjs' {
export class Observable < T > { subscribe ( . . . args : any [ ] ) : unknown ; }
}
` ;
// Bundle the @abp/ng.core / @angular/core / rxjs ambient declarations into
// a single .d.ts root file so TS sees them as ambient module declarations.
const ambient = abpStub + angularCoreStub + rxjsStub + domStub ;
const sources : Record < string , string > = {
'/proxy/sample.service.ts' : output ,
'/proxy/ambient.d.ts' : ambient ,
} ;
const compilerOptions : any = {
target : ts.ScriptTarget.ES2020 ,
module : ts.ModuleKind.ES2020 ,
module Resolution : ts . ModuleResolutionKind . NodeJs ,
experimentalDecorators : true ,
emitDecoratorMetadata : true ,
strict : true ,
noEmit : true ,
skipLibCheck : true ,
} ;
const baseHost = ts . createCompilerHost ( compilerOptions , true ) ;
const host : any = {
. . . baseHost ,
getSourceFile : ( fileName : string , languageVersion : any , onError : any ) = > {
if ( sources [ fileName ] ) {
return ts . createSourceFile ( fileName , sources [ fileName ] , languageVersion , true ) ;
}
return baseHost . getSourceFile ( fileName , languageVersion , onError ) ;
} ,
fileExists : ( fileName : string ) = >
sources [ fileName ] != null || baseHost . fileExists ( fileName ) ,
readFile : ( fileName : string ) = >
sources [ fileName ] ? ? baseHost . readFile ( fileName ) ,
} ;
const program = ts . createProgram ( Object . keys ( sources ) , compilerOptions , host ) ;
const errors = ts
. getPreEmitDiagnostics ( program )
. filter ( ( d : any ) = >
d . category === ts . DiagnosticCategory . Error &&
// Ignore TS6053 "File 'lib.x' not found" which is unrelated to our generated code.
d . code !== 6053 ,
) ;
if ( errors . length ) {
const messages = errors
. map ( ( d : any ) = > {
const where = d . file
? ( ( ) = > {
const p = d . file . getLineAndCharacterOfPosition ( d . start ? ? 0 ) ;
const lineText = d . file . text . split ( '\n' ) [ p . line ] ;
return ` ${ d . file . fileName } : ${ p . line + 1 } : ${ p . character + 1 } \ n>>> ${ lineText } \ n>>> ${ ' ' . repeat ( p . character ) } ^ ` ;
} ) ( )
: '(no file)' ;
return ` [ ${ where } ] TS ${ d . code } : ${ ts . flattenDiagnosticMessageText ( d . messageText , '\n' ) } ` ;
} )
. join ( '\n---\n' ) ;
throw new Error ( ` Generated proxy did not compile: \ n ${ output } \ n=== diagnostics === \ n ${ messages } ` ) ;
}
expect ( errors ) . toHaveLength ( 0 ) ;
} ) ;
} ) ;
} ) ;