diff --git a/npm/ng-packs/packages/schematics/src/tests/proxy-service-template-render.spec.ts b/npm/ng-packs/packages/schematics/src/tests/proxy-service-template-render.spec.ts index 87edc87b4f..f8eb80143c 100644 --- a/npm/ng-packs/packages/schematics/src/tests/proxy-service-template-render.spec.ts +++ b/npm/ng-packs/packages/schematics/src/tests/proxy-service-template-render.spec.ts @@ -190,4 +190,120 @@ describe('proxy service template — rendered output', () => { expect(output).toContain('this.restService.request 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); + // Mirror what the real schematic emits: every action gets a trailing + // `config?: Partial` 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` 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' } 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(req: any, config?: any): import('rxjs').Observable; + } + } + `; + 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(token: { new (...args: any[]): T }): T; + export function inject(token: any): T; + } + `; + const rxjsStub = ` + declare module 'rxjs' { + export class Observable { 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 = { + '/proxy/sample.service.ts': output, + '/proxy/ambient.d.ts': ambient, + }; + + const compilerOptions: any = { + target: ts.ScriptTarget.ES2020, + module: ts.ModuleKind.ES2020, + moduleResolution: 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); + }); });