perf(compiler-cli): allow incremental compilation in the presence of redirected source files (#41448)
When multiple occurrences of the same package exist within a single TypeScript compilation unit, TypeScript deduplicates the source files by introducing redirected source file proxies. Such proxies are recreated during an incremental compilation even if the original declaration file did not change, which caused the compiler not to reuse any work from the prior compilation. This commit changes the incremental driver to recognize a redirected source file and treat them as their unredirected source file. PR Close #41448
This commit is contained in:
parent
0f54d6c4a5
commit
ffea31f433
|
@ -13,6 +13,7 @@ import {PerfEvent, PerfPhase, PerfRecorder} from '../../perf';
|
|||
import {ClassDeclaration} from '../../reflection';
|
||||
import {ClassRecord, TraitCompiler} from '../../transform';
|
||||
import {FileTypeCheckingData} from '../../typecheck/src/checker';
|
||||
import {toUnredirectedSourceFile} from '../../util/src/typescript';
|
||||
import {IncrementalBuild} from '../api';
|
||||
import {SemanticDepGraph, SemanticDepGraphUpdater} from '../semantic_graph';
|
||||
|
||||
|
@ -85,12 +86,14 @@ export class IncrementalDriver implements IncrementalBuild<ClassRecord, FileType
|
|||
// avoid leaking memory.
|
||||
|
||||
// All files in the old program, for easy detection of changes.
|
||||
const oldFiles = new Set<ts.SourceFile>(oldProgram.getSourceFiles());
|
||||
const oldFiles =
|
||||
new Set<ts.SourceFile>(oldProgram.getSourceFiles().map(toUnredirectedSourceFile));
|
||||
|
||||
// Assume all the old files were deleted to begin with. Only TS files are tracked.
|
||||
const deletedTsPaths = new Set<string>(tsOnlyFiles(oldProgram).map(sf => sf.fileName));
|
||||
|
||||
for (const newFile of newProgram.getSourceFiles()) {
|
||||
for (const possiblyRedirectedNewFile of newProgram.getSourceFiles()) {
|
||||
const newFile = toUnredirectedSourceFile(possiblyRedirectedNewFile);
|
||||
if (!newFile.isDeclarationFile) {
|
||||
// This file exists in the new program, so remove it from `deletedTsPaths`.
|
||||
deletedTsPaths.delete(newFile.fileName);
|
||||
|
|
|
@ -8,17 +8,9 @@
|
|||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AbsoluteFsPath} from '../../file_system';
|
||||
import {copyFileShimData, retagAllTsFiles, ShimReferenceTagger, untagAllTsFiles} from '../../shims';
|
||||
import {RequiredDelegations} from '../../util/src/typescript';
|
||||
import {RequiredDelegations, toUnredirectedSourceFile} from '../../util/src/typescript';
|
||||
|
||||
import {ProgramDriver, UpdateMode} from './api';
|
||||
|
||||
|
@ -114,12 +106,9 @@ class UpdatedProgramHost extends DelegatingCompilerHost {
|
|||
} else {
|
||||
sf = delegateSf;
|
||||
}
|
||||
// TypeScript doesn't allow returning redirect source files. To avoid unforseen errors we
|
||||
// TypeScript doesn't allow returning redirect source files. To avoid unforeseen errors we
|
||||
// return the original source file instead of the redirect target.
|
||||
const redirectInfo = (sf as any).redirectInfo;
|
||||
if (redirectInfo !== undefined) {
|
||||
sf = redirectInfo.unredirected;
|
||||
}
|
||||
sf = toUnredirectedSourceFile(sf);
|
||||
|
||||
this.shimTagger.tag(sf);
|
||||
return sf;
|
||||
|
|
|
@ -161,3 +161,23 @@ export type SubsetOfKeys<T, K extends keyof T> = K;
|
|||
export type RequiredDelegations<T> = {
|
||||
[M in keyof Required<T>]: T[M];
|
||||
};
|
||||
|
||||
/**
|
||||
* Source files may become redirects to other source files when their package name and version are
|
||||
* identical. TypeScript creates a proxy source file for such source files which has an internal
|
||||
* `redirectInfo` property that refers to the original source file.
|
||||
*/
|
||||
interface RedirectedSourceFile extends ts.SourceFile {
|
||||
redirectInfo?: {unredirected: ts.SourceFile;};
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains the non-redirected source file for `sf`.
|
||||
*/
|
||||
export function toUnredirectedSourceFile(sf: ts.SourceFile): ts.SourceFile {
|
||||
const redirectInfo = (sf as RedirectedSourceFile).redirectInfo;
|
||||
if (redirectInfo === undefined) {
|
||||
return sf;
|
||||
}
|
||||
return redirectInfo.unredirected;
|
||||
}
|
||||
|
|
|
@ -524,6 +524,58 @@ runInEachFileSystem(() => {
|
|||
env.driveMain();
|
||||
});
|
||||
|
||||
it('should allow incremental compilation with redirected source files', () => {
|
||||
env.tsconfig({fullTemplateTypeCheck: true});
|
||||
|
||||
// This file structure has an identical version of "a" under the root node_modules and inside
|
||||
// of "b". Because their package.json file indicates it is the exact same version of "a",
|
||||
// TypeScript will transform the source file of "node_modules/b/node_modules/a/index.d.ts"
|
||||
// into a redirect to "node_modules/a/index.d.ts". During incremental compilations, the
|
||||
// redirected "node_modules/b/node_modules/a/index.d.ts" source file should be considered as
|
||||
// its unredirected source file to avoid a change in declaration files.
|
||||
env.write('node_modules/a/index.js', `export class ServiceA {}`);
|
||||
env.write('node_modules/a/index.d.ts', `export declare class ServiceA {}`);
|
||||
env.write('node_modules/a/package.json', `{"name": "a", "version": "1.0"}`);
|
||||
env.write('node_modules/b/node_modules/a/index.js', `export class ServiceA {}`);
|
||||
env.write('node_modules/b/node_modules/a/index.d.ts', `export declare class ServiceA {}`);
|
||||
env.write('node_modules/b/node_modules/a/package.json', `{"name": "a", "version": "1.0"}`);
|
||||
env.write('node_modules/b/index.js', `export {ServiceA as ServiceB} from 'a';`);
|
||||
env.write('node_modules/b/index.d.ts', `export {ServiceA as ServiceB} from 'a';`);
|
||||
env.write('component1.ts', `
|
||||
import {Component} from '@angular/core';
|
||||
import {ServiceA} from 'a';
|
||||
import {ServiceB} from 'b';
|
||||
|
||||
@Component({selector: 'cmp', template: 'cmp'})
|
||||
export class Cmp1 {}
|
||||
`);
|
||||
env.write('component2.ts', `
|
||||
import {Component} from '@angular/core';
|
||||
import {ServiceA} from 'a';
|
||||
import {ServiceB} from 'b';
|
||||
|
||||
@Component({selector: 'cmp2', template: 'cmp'})
|
||||
export class Cmp2 {}
|
||||
`);
|
||||
env.driveMain();
|
||||
env.flushWrittenFileTracking();
|
||||
|
||||
// Now update `component1.ts` and change its imports to avoid complete structure reuse, which
|
||||
// forces recreation of source file redirects.
|
||||
env.write('component1.ts', `
|
||||
import {Component} from '@angular/core';
|
||||
import {ServiceA} from 'a';
|
||||
|
||||
@Component({selector: 'cmp', template: 'cmp'})
|
||||
export class Cmp1 {}
|
||||
`);
|
||||
env.driveMain();
|
||||
|
||||
const written = env.getFilesWrittenSinceLastFlush();
|
||||
expect(written).toContain('/component1.js');
|
||||
expect(written).not.toContain('/component2.js');
|
||||
});
|
||||
|
||||
describe('template type-checking', () => {
|
||||
beforeEach(() => {
|
||||
env.tsconfig({strictTemplates: true});
|
||||
|
|
Loading…
Reference in New Issue