fix(compiler-cli): enable @types discovery in incremental rebuilds (#39011)
Prior to this fix, incremental rebuilds could fail to type check due to missing ambient types from auto-discovered declaration files in @types directories, or type roots in general. This was caused by the intermediary `ts.Program` that is created for template type checking, for which a `ts.CompilerHost` was used which did not implement the optional `directoryExists` methods. As a result, auto-discovery of types would not be working correctly, and this would retain into the `ts.Program` that would be created for an incremental rebuild. This commit fixes the issue by forcing the custom `ts.CompilerHost` used for type checking to properly delegate into the original `ts.CompilerHost`, even for optional methods. This is accomplished using a base class `DelegatingCompilerHost` which is typed in such a way that newly introduced `ts.CompilerHost` methods must be accounted for. Fixes #38979 PR Close #39011
This commit is contained in:
parent
3f9be429fc
commit
e9a8f9f705
|
@ -10,11 +10,59 @@ import * as ts from 'typescript';
|
||||||
|
|
||||||
import {copyFileShimData, ShimReferenceTagger} from '../../shims';
|
import {copyFileShimData, ShimReferenceTagger} from '../../shims';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the `ts.CompilerHost` interface, with a transformation applied that turns all
|
||||||
|
* methods (even optional ones) into required fields (which may be `undefined`, if the method was
|
||||||
|
* optional).
|
||||||
|
*/
|
||||||
|
export type RequiredCompilerHostDelegations = {
|
||||||
|
[M in keyof Required<ts.CompilerHost>]: ts.CompilerHost[M];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delegates all methods of `ts.CompilerHost` to a delegate, with the exception of
|
||||||
|
* `getSourceFile`, `fileExists` and `writeFile` which are implemented in `TypeCheckProgramHost`.
|
||||||
|
*
|
||||||
|
* If a new method is added to `ts.CompilerHost` which is not delegated, a type error will be
|
||||||
|
* generated for this class.
|
||||||
|
*/
|
||||||
|
export class DelegatingCompilerHost implements
|
||||||
|
Omit<RequiredCompilerHostDelegations, 'getSourceFile'|'fileExists'|'writeFile'> {
|
||||||
|
constructor(protected delegate: ts.CompilerHost) {}
|
||||||
|
|
||||||
|
private delegateMethod<M extends keyof ts.CompilerHost>(name: M): ts.CompilerHost[M] {
|
||||||
|
return this.delegate[name] !== undefined ? (this.delegate[name] as any).bind(this.delegate) :
|
||||||
|
undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Excluded are 'getSourceFile', 'fileExists' and 'writeFile', which are actually implemented by
|
||||||
|
// `TypeCheckProgramHost` below.
|
||||||
|
createHash = this.delegateMethod('createHash');
|
||||||
|
directoryExists = this.delegateMethod('directoryExists');
|
||||||
|
getCancellationToken = this.delegateMethod('getCancellationToken');
|
||||||
|
getCanonicalFileName = this.delegateMethod('getCanonicalFileName');
|
||||||
|
getCurrentDirectory = this.delegateMethod('getCurrentDirectory');
|
||||||
|
getDefaultLibFileName = this.delegateMethod('getDefaultLibFileName');
|
||||||
|
getDefaultLibLocation = this.delegateMethod('getDefaultLibLocation');
|
||||||
|
getDirectories = this.delegateMethod('getDirectories');
|
||||||
|
getEnvironmentVariable = this.delegateMethod('getEnvironmentVariable');
|
||||||
|
getNewLine = this.delegateMethod('getNewLine');
|
||||||
|
getParsedCommandLine = this.delegateMethod('getParsedCommandLine');
|
||||||
|
getSourceFileByPath = this.delegateMethod('getSourceFileByPath');
|
||||||
|
readDirectory = this.delegateMethod('readDirectory');
|
||||||
|
readFile = this.delegateMethod('readFile');
|
||||||
|
realpath = this.delegateMethod('realpath');
|
||||||
|
resolveModuleNames = this.delegateMethod('resolveModuleNames');
|
||||||
|
resolveTypeReferenceDirectives = this.delegateMethod('resolveTypeReferenceDirectives');
|
||||||
|
trace = this.delegateMethod('trace');
|
||||||
|
useCaseSensitiveFileNames = this.delegateMethod('useCaseSensitiveFileNames');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A `ts.CompilerHost` which augments source files with type checking code from a
|
* A `ts.CompilerHost` which augments source files with type checking code from a
|
||||||
* `TypeCheckContext`.
|
* `TypeCheckContext`.
|
||||||
*/
|
*/
|
||||||
export class TypeCheckProgramHost implements ts.CompilerHost {
|
export class TypeCheckProgramHost extends DelegatingCompilerHost {
|
||||||
/**
|
/**
|
||||||
* Map of source file names to `ts.SourceFile` instances.
|
* Map of source file names to `ts.SourceFile` instances.
|
||||||
*/
|
*/
|
||||||
|
@ -32,20 +80,11 @@ export class TypeCheckProgramHost implements ts.CompilerHost {
|
||||||
*/
|
*/
|
||||||
private shimTagger = new ShimReferenceTagger(this.shimExtensionPrefixes);
|
private shimTagger = new ShimReferenceTagger(this.shimExtensionPrefixes);
|
||||||
|
|
||||||
readonly resolveModuleNames?: ts.CompilerHost['resolveModuleNames'];
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
sfMap: Map<string, ts.SourceFile>, private originalProgram: ts.Program,
|
sfMap: Map<string, ts.SourceFile>, private originalProgram: ts.Program,
|
||||||
private delegate: ts.CompilerHost, private shimExtensionPrefixes: string[]) {
|
delegate: ts.CompilerHost, private shimExtensionPrefixes: string[]) {
|
||||||
|
super(delegate);
|
||||||
this.sfMap = sfMap;
|
this.sfMap = sfMap;
|
||||||
|
|
||||||
if (delegate.getDirectories !== undefined) {
|
|
||||||
this.getDirectories = (path: string) => delegate.getDirectories!(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (delegate.resolveModuleNames !== undefined) {
|
|
||||||
this.resolveModuleNames = delegate.resolveModuleNames;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getSourceFile(
|
getSourceFile(
|
||||||
|
@ -88,42 +127,11 @@ export class TypeCheckProgramHost implements ts.CompilerHost {
|
||||||
this.shimTagger.finalize();
|
this.shimTagger.finalize();
|
||||||
}
|
}
|
||||||
|
|
||||||
// The rest of the methods simply delegate to the underlying `ts.CompilerHost`.
|
writeFile(): never {
|
||||||
|
|
||||||
getDefaultLibFileName(options: ts.CompilerOptions): string {
|
|
||||||
return this.delegate.getDefaultLibFileName(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
writeFile(
|
|
||||||
fileName: string, data: string, writeByteOrderMark: boolean,
|
|
||||||
onError: ((message: string) => void)|undefined,
|
|
||||||
sourceFiles: ReadonlyArray<ts.SourceFile>|undefined): void {
|
|
||||||
throw new Error(`TypeCheckProgramHost should never write files`);
|
throw new Error(`TypeCheckProgramHost should never write files`);
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentDirectory(): string {
|
|
||||||
return this.delegate.getCurrentDirectory();
|
|
||||||
}
|
|
||||||
|
|
||||||
getDirectories?: (path: string) => string[];
|
|
||||||
|
|
||||||
getCanonicalFileName(fileName: string): string {
|
|
||||||
return this.delegate.getCanonicalFileName(fileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
useCaseSensitiveFileNames(): boolean {
|
|
||||||
return this.delegate.useCaseSensitiveFileNames();
|
|
||||||
}
|
|
||||||
|
|
||||||
getNewLine(): string {
|
|
||||||
return this.delegate.getNewLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
fileExists(fileName: string): boolean {
|
fileExists(fileName: string): boolean {
|
||||||
return this.sfMap.has(fileName) || this.delegate.fileExists(fileName);
|
return this.sfMap.has(fileName) || this.delegate.fileExists(fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
readFile(fileName: string): string|undefined {
|
|
||||||
return this.delegate.readFile(fileName);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -442,6 +442,27 @@ runInEachFileSystem(() => {
|
||||||
// https://github.com/angular/angular/issues/30079), this would have crashed.
|
// https://github.com/angular/angular/issues/30079), this would have crashed.
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// https://github.com/angular/angular/issues/38979
|
||||||
|
it('should retain ambient types provided by auto-discovered @types', () => {
|
||||||
|
// This test verifies that ambient types declared in node_modules/@types are still available
|
||||||
|
// in incremental compilations. In the below code, the usage of `require` should be valid
|
||||||
|
// in the original program and the incremental program.
|
||||||
|
env.tsconfig({fullTemplateTypeCheck: true});
|
||||||
|
env.write('node_modules/@types/node/index.d.ts', 'declare var require: any;');
|
||||||
|
env.write('main.ts', `
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
|
||||||
|
require('path');
|
||||||
|
|
||||||
|
@Component({template: ''})
|
||||||
|
export class MyComponent {}
|
||||||
|
`);
|
||||||
|
env.driveMain();
|
||||||
|
env.invalidateCachedFile('main.ts');
|
||||||
|
const diags = env.driveDiagnostics();
|
||||||
|
expect(diags.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
// https://github.com/angular/angular/pull/26036
|
// https://github.com/angular/angular/pull/26036
|
||||||
it('should handle redirected source files', () => {
|
it('should handle redirected source files', () => {
|
||||||
env.tsconfig({fullTemplateTypeCheck: true});
|
env.tsconfig({fullTemplateTypeCheck: true});
|
||||||
|
|
Loading…
Reference in New Issue