Browse Source

Upgrade proxy service template test to ts.createProgram for real semantic checks

pull/25639/head
maliming 4 months ago
parent
commit
025e495d6e
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 116
      npm/ng-packs/packages/schematics/src/tests/proxy-service-template-render.spec.ts

116
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<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,
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);
});
}); });

Loading…
Cancel
Save