2019-03-18 15:25:26 -04:00
|
|
|
/**
|
|
|
|
* @license
|
2020-05-19 15:08:49 -04:00
|
|
|
* Copyright Google LLC All Rights Reserved.
|
2019-03-18 15:25:26 -04:00
|
|
|
*
|
|
|
|
* 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
|
|
|
|
*/
|
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing';
|
2020-11-04 15:27:19 -05:00
|
|
|
import {loadStandardTestFiles} from '../../src/ngtsc/testing';
|
2019-06-10 11:22:56 -04:00
|
|
|
|
2019-03-18 15:25:26 -04:00
|
|
|
import {NgtscTestEnvironment} from './env';
|
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
const testFiles = loadStandardTestFiles();
|
|
|
|
|
|
|
|
runInEachFileSystem(() => {
|
|
|
|
describe('ngtsc incremental compilation', () => {
|
2020-04-07 15:43:43 -04:00
|
|
|
let env!: NgtscTestEnvironment;
|
2019-06-06 15:22:32 -04:00
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
env = NgtscTestEnvironment.setup(testFiles);
|
|
|
|
env.enableMultipleCompilations();
|
|
|
|
env.tsconfig();
|
|
|
|
});
|
2019-03-18 15:25:26 -04:00
|
|
|
|
2019-06-27 19:25:00 -04:00
|
|
|
it('should not crash if CLI does not provide getModifiedResourceFiles()', () => {
|
|
|
|
env.write('component1.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({selector: 'cmp', templateUrl: './component1.template.html'})
|
|
|
|
export class Cmp1 {}
|
|
|
|
`);
|
|
|
|
env.write('component1.template.html', 'cmp1');
|
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
// Simulate a change to `component1.html`
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
env.invalidateCachedFile('component1.html');
|
|
|
|
env.simulateLegacyCLICompilerHost();
|
|
|
|
env.driveMain();
|
|
|
|
});
|
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
it('should skip unchanged services', () => {
|
|
|
|
env.write('service.ts', `
|
2019-03-18 15:25:26 -04:00
|
|
|
import {Injectable} from '@angular/core';
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
export class Service {}
|
|
|
|
`);
|
2019-06-06 15:22:32 -04:00
|
|
|
env.write('test.ts', `
|
2019-03-18 15:25:26 -04:00
|
|
|
import {Component} from '@angular/core';
|
|
|
|
import {Service} from './service';
|
|
|
|
|
|
|
|
@Component({selector: 'cmp', template: 'cmp'})
|
|
|
|
export class Cmp {
|
|
|
|
constructor(service: Service) {}
|
|
|
|
}
|
|
|
|
`);
|
2019-06-06 15:22:32 -04:00
|
|
|
env.driveMain();
|
|
|
|
env.flushWrittenFileTracking();
|
2019-03-18 15:25:26 -04:00
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
// Pretend a change was made to test.ts.
|
|
|
|
env.invalidateCachedFile('test.ts');
|
|
|
|
env.driveMain();
|
|
|
|
const written = env.getFilesWrittenSinceLastFlush();
|
2019-03-18 15:25:26 -04:00
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
// The changed file should be recompiled, but not the service.
|
|
|
|
expect(written).toContain('/test.js');
|
|
|
|
expect(written).not.toContain('/service.js');
|
|
|
|
});
|
2019-04-24 13:26:47 -04:00
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
it('should rebuild components that have changed', () => {
|
perf(compiler-cli): perform template type-checking incrementally (#36211)
This optimization builds on a lot of prior work to finally make type-
checking of templates incremental.
Incrementality requires two main components:
- the ability to reuse work from a prior compilation.
- the ability to know when changes in the current program invalidate that
prior work.
Prior to this commit, on every type-checking pass the compiler would
generate new .ngtypecheck files for each original input file in the program.
1. (Build #1 main program): empty .ngtypecheck files generated for each
original input file.
2. (Build #1 type-check program): .ngtypecheck contents overridden for those
which have corresponding components that need type-checked.
3. (Build #2 main program): throw away old .ngtypecheck files and generate
new empty ones.
4. (Build #2 type-check program): same as step 2.
With this commit, the `IncrementalDriver` now tracks template type-checking
_metadata_ for each input file. The metadata contains information about
source mappings for generated type-checking code, as well as some
diagnostics which were discovered at type-check analysis time. The actual
type-checking code is stored in the TypeScript AST for type-checking files,
which is now re-used between programs as follows:
1. (Build #1 main program): empty .ngtypecheck files generated for each
original input file.
2. (Build #1 type-check program): .ngtypecheck contents overridden for those
which have corresponding components that need type-checked, and the
metadata registered in the `IncrementalDriver`.
3. (Build #2 main program): The `TypeCheckShimGenerator` now reuses _all_
.ngtypecheck `ts.SourceFile` shims from build #1's type-check program in
the construction of build #2's main program. Some of the contents of
these files might be stale (if a component's template changed, for
example), but wholesale reuse here prevents unnecessary changes in the
contents of the program at this point and makes TypeScript's job a lot
easier.
4. (Build #2 type-check program): For those input files which have not
"logically changed" (meaning components within are semantically the same
as they were before), the compiler will re-use the type-check file
metadata from build #1, and _not_ generate a new .ngtypecheck shim.
For components which have logically changed or where the previous
.ngtypecheck contents cannot otherwise be reused, code generation happens
as before.
PR Close #36211
2020-03-19 14:29:58 -04:00
|
|
|
env.tsconfig({strictTemplates: true});
|
2019-06-06 15:22:32 -04:00
|
|
|
env.write('component1.ts', `
|
2019-05-08 10:10:50 -04:00
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({selector: 'cmp', template: 'cmp'})
|
|
|
|
export class Cmp1 {}
|
|
|
|
`);
|
2019-06-06 15:22:32 -04:00
|
|
|
env.write('component2.ts', `
|
2019-05-08 10:10:50 -04:00
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({selector: 'cmp2', template: 'cmp'})
|
|
|
|
export class Cmp2 {}
|
|
|
|
`);
|
2019-06-06 15:22:32 -04:00
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
// Pretend a change was made to Cmp1
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
env.invalidateCachedFile('component1.ts');
|
|
|
|
env.driveMain();
|
|
|
|
const written = env.getFilesWrittenSinceLastFlush();
|
|
|
|
expect(written).toContain('/component1.js');
|
|
|
|
expect(written).not.toContain('/component2.js');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should rebuild components whose templates have changed', () => {
|
|
|
|
env.write('component1.ts', `
|
2019-06-10 11:22:56 -04:00
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({selector: 'cmp', templateUrl: './component1.template.html'})
|
|
|
|
export class Cmp1 {}
|
|
|
|
`);
|
2019-06-06 15:22:32 -04:00
|
|
|
env.write('component1.template.html', 'cmp1');
|
|
|
|
env.write('component2.ts', `
|
2019-06-10 11:22:56 -04:00
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({selector: 'cmp2', templateUrl: './component2.template.html'})
|
|
|
|
export class Cmp2 {}
|
|
|
|
`);
|
2019-06-06 15:22:32 -04:00
|
|
|
env.write('component2.template.html', 'cmp2');
|
2019-06-10 11:22:56 -04:00
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
env.driveMain();
|
2019-06-10 11:22:56 -04:00
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
// Make a change to Cmp1 template
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
env.write('component1.template.html', 'changed');
|
|
|
|
env.driveMain();
|
|
|
|
const written = env.getFilesWrittenSinceLastFlush();
|
|
|
|
expect(written).toContain('/component1.js');
|
|
|
|
expect(written).not.toContain('/component2.js');
|
|
|
|
});
|
2019-06-10 11:22:56 -04:00
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
it('should rebuild components whose partial-evaluation dependencies have changed', () => {
|
|
|
|
env.write('component1.ts', `
|
2019-05-08 10:10:50 -04:00
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({selector: 'cmp', template: 'cmp'})
|
|
|
|
export class Cmp1 {}
|
|
|
|
`);
|
2019-06-06 15:22:32 -04:00
|
|
|
env.write('component2.ts', `
|
2019-05-08 10:10:50 -04:00
|
|
|
import {Component} from '@angular/core';
|
|
|
|
import {SELECTOR} from './constants';
|
|
|
|
|
|
|
|
@Component({selector: SELECTOR, template: 'cmp'})
|
|
|
|
export class Cmp2 {}
|
|
|
|
`);
|
2019-06-06 15:22:32 -04:00
|
|
|
env.write('constants.ts', `
|
2019-05-08 10:10:50 -04:00
|
|
|
export const SELECTOR = 'cmp';
|
|
|
|
`);
|
2019-06-06 15:22:32 -04:00
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
// Pretend a change was made to SELECTOR
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
env.invalidateCachedFile('constants.ts');
|
|
|
|
env.driveMain();
|
|
|
|
const written = env.getFilesWrittenSinceLastFlush();
|
|
|
|
expect(written).toContain('/constants.js');
|
|
|
|
expect(written).not.toContain('/component1.js');
|
|
|
|
expect(written).toContain('/component2.js');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should rebuild components whose imported dependencies have changed', () => {
|
|
|
|
setupFooBarProgram(env);
|
|
|
|
|
|
|
|
// Pretend a change was made to BarDir.
|
perf(compiler-cli): detect semantic changes and their effect on an incremental rebuild (#40947)
In Angular programs, changing a file may require other files to be
emitted as well due to implicit NgModule dependencies. For example, if
the selector of a directive is changed then all components that have
that directive in their compilation scope need to be recompiled, as the
change of selector may affect the directive matching results.
Until now, the compiler solved this problem using a single dependency
graph. The implicit NgModule dependencies were represented in this
graph, such that a changed file would correctly also cause other files
to be re-emitted. This approach is limited in a few ways:
1. The file dependency graph is used to determine whether it is safe to
reuse the analysis data of an Angular decorated class. This analysis
data is invariant to unrelated changes to the NgModule scope, but
because the single dependency graph also tracked the implicit
NgModule dependencies the compiler had to consider analysis data as
stale far more often than necessary.
2. It is typical for a change to e.g. a directive to not affect its
public API—its selector, inputs, outputs, or exportAs clause—in which
case there is no need to re-emit all declarations in scope, as their
compilation output wouldn't have changed.
This commit implements a mechanism by which the compiler is able to
determine the impact of a change by comparing it to the prior
compilation. To achieve this, a new graph is maintained that tracks all
public API information of all Angular decorated symbols. During an
incremental compilation this information is compared to the information
that was captured in the most recently succeeded compilation. This
determines the exact impact of the changes to the public API, which
is then used to determine which files need to be re-emitted.
Note that the file dependency graph remains, as it is still used to
track the dependencies of analysis data. This graph does no longer track
the implicit NgModule dependencies, which allows for better reuse of
analysis data.
These changes also fix a bug where template type-checking would fail to
incorporate changes made to a transitive base class of a
directive/component. This used to be a problem because transitive base
classes were not recorded as a transitive dependency in the file
dependency graph, such that prior type-check blocks would erroneously
be reused.
This commit also fixes an incorrectness where a change to a declaration
in NgModule `A` would not cause the declarations in NgModules that
import from NgModule `A` to be re-emitted. This was intentionally
incorrect as otherwise the performance of incremental rebuilds would
have been far worse. This is no longer a concern, as the compiler is now
able to only re-emit when actually necessary.
Fixes #34867
Fixes #40635
Closes #40728
PR Close #40947
2020-11-20 15:18:46 -05:00
|
|
|
env.write('bar_directive.ts', `
|
|
|
|
import {Directive} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({selector: '[barr]'})
|
|
|
|
export class BarDir {}
|
|
|
|
`);
|
2019-06-06 15:22:32 -04:00
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
let written = env.getFilesWrittenSinceLastFlush();
|
|
|
|
expect(written).toContain('/bar_directive.js');
|
|
|
|
expect(written).toContain('/bar_component.js');
|
|
|
|
expect(written).toContain('/bar_module.js');
|
perf(compiler-cli): detect semantic changes and their effect on an incremental rebuild (#40947)
In Angular programs, changing a file may require other files to be
emitted as well due to implicit NgModule dependencies. For example, if
the selector of a directive is changed then all components that have
that directive in their compilation scope need to be recompiled, as the
change of selector may affect the directive matching results.
Until now, the compiler solved this problem using a single dependency
graph. The implicit NgModule dependencies were represented in this
graph, such that a changed file would correctly also cause other files
to be re-emitted. This approach is limited in a few ways:
1. The file dependency graph is used to determine whether it is safe to
reuse the analysis data of an Angular decorated class. This analysis
data is invariant to unrelated changes to the NgModule scope, but
because the single dependency graph also tracked the implicit
NgModule dependencies the compiler had to consider analysis data as
stale far more often than necessary.
2. It is typical for a change to e.g. a directive to not affect its
public API—its selector, inputs, outputs, or exportAs clause—in which
case there is no need to re-emit all declarations in scope, as their
compilation output wouldn't have changed.
This commit implements a mechanism by which the compiler is able to
determine the impact of a change by comparing it to the prior
compilation. To achieve this, a new graph is maintained that tracks all
public API information of all Angular decorated symbols. During an
incremental compilation this information is compared to the information
that was captured in the most recently succeeded compilation. This
determines the exact impact of the changes to the public API, which
is then used to determine which files need to be re-emitted.
Note that the file dependency graph remains, as it is still used to
track the dependencies of analysis data. This graph does no longer track
the implicit NgModule dependencies, which allows for better reuse of
analysis data.
These changes also fix a bug where template type-checking would fail to
incorporate changes made to a transitive base class of a
directive/component. This used to be a problem because transitive base
classes were not recorded as a transitive dependency in the file
dependency graph, such that prior type-check blocks would erroneously
be reused.
This commit also fixes an incorrectness where a change to a declaration
in NgModule `A` would not cause the declarations in NgModules that
import from NgModule `A` to be re-emitted. This was intentionally
incorrect as otherwise the performance of incremental rebuilds would
have been far worse. This is no longer a concern, as the compiler is now
able to only re-emit when actually necessary.
Fixes #34867
Fixes #40635
Closes #40728
PR Close #40947
2020-11-20 15:18:46 -05:00
|
|
|
expect(written).not.toContain('/foo_component.js'); // BarDir is not exported by BarModule,
|
|
|
|
// so upstream NgModule is not affected
|
2019-06-06 15:22:32 -04:00
|
|
|
expect(written).not.toContain('/foo_pipe.js');
|
|
|
|
expect(written).not.toContain('/foo_module.js');
|
|
|
|
});
|
|
|
|
|
fix(ivy): ensure module scope is rebuild on dependent change (#33522)
During incremental compilations, ngtsc needs to know which metadata
from a previous compilation can be reused, versus which metadata has to
be recomputed as some dependency was updated. Changes to
directives/components should cause the NgModule in which they are
declared to be recompiled, as the NgModule's compilation is dependent
on its directives/components.
When a dependent source file of a directive/component is updated,
however, a more subtle dependency should also cause to NgModule's source
file to be invalidated. During the reconciliation of state from a
previous compilation into the new program, the component's source file
is invalidated because one of its dependency has changed, ergo the
NgModule needs to be invalidated as well. Up until now, this implicit
dependency was not imposed on the NgModule. Additionally, any change to
a dependent file may influence the module scope to change, so all
components within the module must be invalidated as well.
This commit fixes the bug by introducing additional file dependencies,
as to ensure a proper rebuild of the module scope and its components.
Fixes #32416
PR Close #33522
2019-10-31 15:30:05 -04:00
|
|
|
// https://github.com/angular/angular/issues/32416
|
|
|
|
it('should rebuild full NgModule scope when a dependency of a declaration has changed', () => {
|
|
|
|
env.write('component1.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
import {SELECTOR} from './dep';
|
2020-05-19 15:08:49 -04:00
|
|
|
|
fix(ivy): ensure module scope is rebuild on dependent change (#33522)
During incremental compilations, ngtsc needs to know which metadata
from a previous compilation can be reused, versus which metadata has to
be recomputed as some dependency was updated. Changes to
directives/components should cause the NgModule in which they are
declared to be recompiled, as the NgModule's compilation is dependent
on its directives/components.
When a dependent source file of a directive/component is updated,
however, a more subtle dependency should also cause to NgModule's source
file to be invalidated. During the reconciliation of state from a
previous compilation into the new program, the component's source file
is invalidated because one of its dependency has changed, ergo the
NgModule needs to be invalidated as well. Up until now, this implicit
dependency was not imposed on the NgModule. Additionally, any change to
a dependent file may influence the module scope to change, so all
components within the module must be invalidated as well.
This commit fixes the bug by introducing additional file dependencies,
as to ensure a proper rebuild of the module scope and its components.
Fixes #32416
PR Close #33522
2019-10-31 15:30:05 -04:00
|
|
|
@Component({selector: SELECTOR, template: 'cmp'})
|
|
|
|
export class Cmp1 {}
|
|
|
|
`);
|
|
|
|
env.write('component2.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
2020-05-19 15:08:49 -04:00
|
|
|
|
perf(compiler-cli): detect semantic changes and their effect on an incremental rebuild (#40947)
In Angular programs, changing a file may require other files to be
emitted as well due to implicit NgModule dependencies. For example, if
the selector of a directive is changed then all components that have
that directive in their compilation scope need to be recompiled, as the
change of selector may affect the directive matching results.
Until now, the compiler solved this problem using a single dependency
graph. The implicit NgModule dependencies were represented in this
graph, such that a changed file would correctly also cause other files
to be re-emitted. This approach is limited in a few ways:
1. The file dependency graph is used to determine whether it is safe to
reuse the analysis data of an Angular decorated class. This analysis
data is invariant to unrelated changes to the NgModule scope, but
because the single dependency graph also tracked the implicit
NgModule dependencies the compiler had to consider analysis data as
stale far more often than necessary.
2. It is typical for a change to e.g. a directive to not affect its
public API—its selector, inputs, outputs, or exportAs clause—in which
case there is no need to re-emit all declarations in scope, as their
compilation output wouldn't have changed.
This commit implements a mechanism by which the compiler is able to
determine the impact of a change by comparing it to the prior
compilation. To achieve this, a new graph is maintained that tracks all
public API information of all Angular decorated symbols. During an
incremental compilation this information is compared to the information
that was captured in the most recently succeeded compilation. This
determines the exact impact of the changes to the public API, which
is then used to determine which files need to be re-emitted.
Note that the file dependency graph remains, as it is still used to
track the dependencies of analysis data. This graph does no longer track
the implicit NgModule dependencies, which allows for better reuse of
analysis data.
These changes also fix a bug where template type-checking would fail to
incorporate changes made to a transitive base class of a
directive/component. This used to be a problem because transitive base
classes were not recorded as a transitive dependency in the file
dependency graph, such that prior type-check blocks would erroneously
be reused.
This commit also fixes an incorrectness where a change to a declaration
in NgModule `A` would not cause the declarations in NgModules that
import from NgModule `A` to be re-emitted. This was intentionally
incorrect as otherwise the performance of incremental rebuilds would
have been far worse. This is no longer a concern, as the compiler is now
able to only re-emit when actually necessary.
Fixes #34867
Fixes #40635
Closes #40728
PR Close #40947
2020-11-20 15:18:46 -05:00
|
|
|
@Component({selector: 'cmp2', template: '<cmp></cmp>'})
|
fix(ivy): ensure module scope is rebuild on dependent change (#33522)
During incremental compilations, ngtsc needs to know which metadata
from a previous compilation can be reused, versus which metadata has to
be recomputed as some dependency was updated. Changes to
directives/components should cause the NgModule in which they are
declared to be recompiled, as the NgModule's compilation is dependent
on its directives/components.
When a dependent source file of a directive/component is updated,
however, a more subtle dependency should also cause to NgModule's source
file to be invalidated. During the reconciliation of state from a
previous compilation into the new program, the component's source file
is invalidated because one of its dependency has changed, ergo the
NgModule needs to be invalidated as well. Up until now, this implicit
dependency was not imposed on the NgModule. Additionally, any change to
a dependent file may influence the module scope to change, so all
components within the module must be invalidated as well.
This commit fixes the bug by introducing additional file dependencies,
as to ensure a proper rebuild of the module scope and its components.
Fixes #32416
PR Close #33522
2019-10-31 15:30:05 -04:00
|
|
|
export class Cmp2 {}
|
|
|
|
`);
|
|
|
|
env.write('dep.ts', `
|
|
|
|
export const SELECTOR = 'cmp';
|
|
|
|
`);
|
|
|
|
env.write('directive.ts', `
|
|
|
|
import {Directive} from '@angular/core';
|
2020-05-19 15:08:49 -04:00
|
|
|
|
fix(ivy): ensure module scope is rebuild on dependent change (#33522)
During incremental compilations, ngtsc needs to know which metadata
from a previous compilation can be reused, versus which metadata has to
be recomputed as some dependency was updated. Changes to
directives/components should cause the NgModule in which they are
declared to be recompiled, as the NgModule's compilation is dependent
on its directives/components.
When a dependent source file of a directive/component is updated,
however, a more subtle dependency should also cause to NgModule's source
file to be invalidated. During the reconciliation of state from a
previous compilation into the new program, the component's source file
is invalidated because one of its dependency has changed, ergo the
NgModule needs to be invalidated as well. Up until now, this implicit
dependency was not imposed on the NgModule. Additionally, any change to
a dependent file may influence the module scope to change, so all
components within the module must be invalidated as well.
This commit fixes the bug by introducing additional file dependencies,
as to ensure a proper rebuild of the module scope and its components.
Fixes #32416
PR Close #33522
2019-10-31 15:30:05 -04:00
|
|
|
@Directive({selector: 'dir'})
|
|
|
|
export class Dir {}
|
|
|
|
`);
|
|
|
|
env.write('pipe.ts', `
|
|
|
|
import {Pipe} from '@angular/core';
|
2020-05-19 15:08:49 -04:00
|
|
|
|
fix(ivy): ensure module scope is rebuild on dependent change (#33522)
During incremental compilations, ngtsc needs to know which metadata
from a previous compilation can be reused, versus which metadata has to
be recomputed as some dependency was updated. Changes to
directives/components should cause the NgModule in which they are
declared to be recompiled, as the NgModule's compilation is dependent
on its directives/components.
When a dependent source file of a directive/component is updated,
however, a more subtle dependency should also cause to NgModule's source
file to be invalidated. During the reconciliation of state from a
previous compilation into the new program, the component's source file
is invalidated because one of its dependency has changed, ergo the
NgModule needs to be invalidated as well. Up until now, this implicit
dependency was not imposed on the NgModule. Additionally, any change to
a dependent file may influence the module scope to change, so all
components within the module must be invalidated as well.
This commit fixes the bug by introducing additional file dependencies,
as to ensure a proper rebuild of the module scope and its components.
Fixes #32416
PR Close #33522
2019-10-31 15:30:05 -04:00
|
|
|
@Pipe({name: 'myPipe'})
|
2021-01-22 12:03:37 -05:00
|
|
|
export class MyPipe {
|
|
|
|
transform() {}
|
|
|
|
}
|
fix(ivy): ensure module scope is rebuild on dependent change (#33522)
During incremental compilations, ngtsc needs to know which metadata
from a previous compilation can be reused, versus which metadata has to
be recomputed as some dependency was updated. Changes to
directives/components should cause the NgModule in which they are
declared to be recompiled, as the NgModule's compilation is dependent
on its directives/components.
When a dependent source file of a directive/component is updated,
however, a more subtle dependency should also cause to NgModule's source
file to be invalidated. During the reconciliation of state from a
previous compilation into the new program, the component's source file
is invalidated because one of its dependency has changed, ergo the
NgModule needs to be invalidated as well. Up until now, this implicit
dependency was not imposed on the NgModule. Additionally, any change to
a dependent file may influence the module scope to change, so all
components within the module must be invalidated as well.
This commit fixes the bug by introducing additional file dependencies,
as to ensure a proper rebuild of the module scope and its components.
Fixes #32416
PR Close #33522
2019-10-31 15:30:05 -04:00
|
|
|
`);
|
|
|
|
env.write('module.ts', `
|
perf(compiler-cli): detect semantic changes and their effect on an incremental rebuild (#40947)
In Angular programs, changing a file may require other files to be
emitted as well due to implicit NgModule dependencies. For example, if
the selector of a directive is changed then all components that have
that directive in their compilation scope need to be recompiled, as the
change of selector may affect the directive matching results.
Until now, the compiler solved this problem using a single dependency
graph. The implicit NgModule dependencies were represented in this
graph, such that a changed file would correctly also cause other files
to be re-emitted. This approach is limited in a few ways:
1. The file dependency graph is used to determine whether it is safe to
reuse the analysis data of an Angular decorated class. This analysis
data is invariant to unrelated changes to the NgModule scope, but
because the single dependency graph also tracked the implicit
NgModule dependencies the compiler had to consider analysis data as
stale far more often than necessary.
2. It is typical for a change to e.g. a directive to not affect its
public API—its selector, inputs, outputs, or exportAs clause—in which
case there is no need to re-emit all declarations in scope, as their
compilation output wouldn't have changed.
This commit implements a mechanism by which the compiler is able to
determine the impact of a change by comparing it to the prior
compilation. To achieve this, a new graph is maintained that tracks all
public API information of all Angular decorated symbols. During an
incremental compilation this information is compared to the information
that was captured in the most recently succeeded compilation. This
determines the exact impact of the changes to the public API, which
is then used to determine which files need to be re-emitted.
Note that the file dependency graph remains, as it is still used to
track the dependencies of analysis data. This graph does no longer track
the implicit NgModule dependencies, which allows for better reuse of
analysis data.
These changes also fix a bug where template type-checking would fail to
incorporate changes made to a transitive base class of a
directive/component. This used to be a problem because transitive base
classes were not recorded as a transitive dependency in the file
dependency graph, such that prior type-check blocks would erroneously
be reused.
This commit also fixes an incorrectness where a change to a declaration
in NgModule `A` would not cause the declarations in NgModules that
import from NgModule `A` to be re-emitted. This was intentionally
incorrect as otherwise the performance of incremental rebuilds would
have been far worse. This is no longer a concern, as the compiler is now
able to only re-emit when actually necessary.
Fixes #34867
Fixes #40635
Closes #40728
PR Close #40947
2020-11-20 15:18:46 -05:00
|
|
|
import {NgModule, NO_ERRORS_SCHEMA} from '@angular/core';
|
fix(ivy): ensure module scope is rebuild on dependent change (#33522)
During incremental compilations, ngtsc needs to know which metadata
from a previous compilation can be reused, versus which metadata has to
be recomputed as some dependency was updated. Changes to
directives/components should cause the NgModule in which they are
declared to be recompiled, as the NgModule's compilation is dependent
on its directives/components.
When a dependent source file of a directive/component is updated,
however, a more subtle dependency should also cause to NgModule's source
file to be invalidated. During the reconciliation of state from a
previous compilation into the new program, the component's source file
is invalidated because one of its dependency has changed, ergo the
NgModule needs to be invalidated as well. Up until now, this implicit
dependency was not imposed on the NgModule. Additionally, any change to
a dependent file may influence the module scope to change, so all
components within the module must be invalidated as well.
This commit fixes the bug by introducing additional file dependencies,
as to ensure a proper rebuild of the module scope and its components.
Fixes #32416
PR Close #33522
2019-10-31 15:30:05 -04:00
|
|
|
import {Cmp1} from './component1';
|
|
|
|
import {Cmp2} from './component2';
|
|
|
|
import {Dir} from './directive';
|
|
|
|
import {MyPipe} from './pipe';
|
2020-05-19 15:08:49 -04:00
|
|
|
|
perf(compiler-cli): detect semantic changes and their effect on an incremental rebuild (#40947)
In Angular programs, changing a file may require other files to be
emitted as well due to implicit NgModule dependencies. For example, if
the selector of a directive is changed then all components that have
that directive in their compilation scope need to be recompiled, as the
change of selector may affect the directive matching results.
Until now, the compiler solved this problem using a single dependency
graph. The implicit NgModule dependencies were represented in this
graph, such that a changed file would correctly also cause other files
to be re-emitted. This approach is limited in a few ways:
1. The file dependency graph is used to determine whether it is safe to
reuse the analysis data of an Angular decorated class. This analysis
data is invariant to unrelated changes to the NgModule scope, but
because the single dependency graph also tracked the implicit
NgModule dependencies the compiler had to consider analysis data as
stale far more often than necessary.
2. It is typical for a change to e.g. a directive to not affect its
public API—its selector, inputs, outputs, or exportAs clause—in which
case there is no need to re-emit all declarations in scope, as their
compilation output wouldn't have changed.
This commit implements a mechanism by which the compiler is able to
determine the impact of a change by comparing it to the prior
compilation. To achieve this, a new graph is maintained that tracks all
public API information of all Angular decorated symbols. During an
incremental compilation this information is compared to the information
that was captured in the most recently succeeded compilation. This
determines the exact impact of the changes to the public API, which
is then used to determine which files need to be re-emitted.
Note that the file dependency graph remains, as it is still used to
track the dependencies of analysis data. This graph does no longer track
the implicit NgModule dependencies, which allows for better reuse of
analysis data.
These changes also fix a bug where template type-checking would fail to
incorporate changes made to a transitive base class of a
directive/component. This used to be a problem because transitive base
classes were not recorded as a transitive dependency in the file
dependency graph, such that prior type-check blocks would erroneously
be reused.
This commit also fixes an incorrectness where a change to a declaration
in NgModule `A` would not cause the declarations in NgModules that
import from NgModule `A` to be re-emitted. This was intentionally
incorrect as otherwise the performance of incremental rebuilds would
have been far worse. This is no longer a concern, as the compiler is now
able to only re-emit when actually necessary.
Fixes #34867
Fixes #40635
Closes #40728
PR Close #40947
2020-11-20 15:18:46 -05:00
|
|
|
@NgModule({declarations: [Cmp1, Cmp2, Dir, MyPipe], schemas: [NO_ERRORS_SCHEMA]})
|
fix(ivy): ensure module scope is rebuild on dependent change (#33522)
During incremental compilations, ngtsc needs to know which metadata
from a previous compilation can be reused, versus which metadata has to
be recomputed as some dependency was updated. Changes to
directives/components should cause the NgModule in which they are
declared to be recompiled, as the NgModule's compilation is dependent
on its directives/components.
When a dependent source file of a directive/component is updated,
however, a more subtle dependency should also cause to NgModule's source
file to be invalidated. During the reconciliation of state from a
previous compilation into the new program, the component's source file
is invalidated because one of its dependency has changed, ergo the
NgModule needs to be invalidated as well. Up until now, this implicit
dependency was not imposed on the NgModule. Additionally, any change to
a dependent file may influence the module scope to change, so all
components within the module must be invalidated as well.
This commit fixes the bug by introducing additional file dependencies,
as to ensure a proper rebuild of the module scope and its components.
Fixes #32416
PR Close #33522
2019-10-31 15:30:05 -04:00
|
|
|
export class Mod {}
|
|
|
|
`);
|
|
|
|
env.driveMain();
|
|
|
|
|
perf(compiler-cli): detect semantic changes and their effect on an incremental rebuild (#40947)
In Angular programs, changing a file may require other files to be
emitted as well due to implicit NgModule dependencies. For example, if
the selector of a directive is changed then all components that have
that directive in their compilation scope need to be recompiled, as the
change of selector may affect the directive matching results.
Until now, the compiler solved this problem using a single dependency
graph. The implicit NgModule dependencies were represented in this
graph, such that a changed file would correctly also cause other files
to be re-emitted. This approach is limited in a few ways:
1. The file dependency graph is used to determine whether it is safe to
reuse the analysis data of an Angular decorated class. This analysis
data is invariant to unrelated changes to the NgModule scope, but
because the single dependency graph also tracked the implicit
NgModule dependencies the compiler had to consider analysis data as
stale far more often than necessary.
2. It is typical for a change to e.g. a directive to not affect its
public API—its selector, inputs, outputs, or exportAs clause—in which
case there is no need to re-emit all declarations in scope, as their
compilation output wouldn't have changed.
This commit implements a mechanism by which the compiler is able to
determine the impact of a change by comparing it to the prior
compilation. To achieve this, a new graph is maintained that tracks all
public API information of all Angular decorated symbols. During an
incremental compilation this information is compared to the information
that was captured in the most recently succeeded compilation. This
determines the exact impact of the changes to the public API, which
is then used to determine which files need to be re-emitted.
Note that the file dependency graph remains, as it is still used to
track the dependencies of analysis data. This graph does no longer track
the implicit NgModule dependencies, which allows for better reuse of
analysis data.
These changes also fix a bug where template type-checking would fail to
incorporate changes made to a transitive base class of a
directive/component. This used to be a problem because transitive base
classes were not recorded as a transitive dependency in the file
dependency graph, such that prior type-check blocks would erroneously
be reused.
This commit also fixes an incorrectness where a change to a declaration
in NgModule `A` would not cause the declarations in NgModules that
import from NgModule `A` to be re-emitted. This was intentionally
incorrect as otherwise the performance of incremental rebuilds would
have been far worse. This is no longer a concern, as the compiler is now
able to only re-emit when actually necessary.
Fixes #34867
Fixes #40635
Closes #40728
PR Close #40947
2020-11-20 15:18:46 -05:00
|
|
|
// Pretend a change was made to 'dep'. Since the selector is updated this affects the NgModule
|
|
|
|
// scope, so all components in the module scope need to be recompiled.
|
fix(ivy): ensure module scope is rebuild on dependent change (#33522)
During incremental compilations, ngtsc needs to know which metadata
from a previous compilation can be reused, versus which metadata has to
be recomputed as some dependency was updated. Changes to
directives/components should cause the NgModule in which they are
declared to be recompiled, as the NgModule's compilation is dependent
on its directives/components.
When a dependent source file of a directive/component is updated,
however, a more subtle dependency should also cause to NgModule's source
file to be invalidated. During the reconciliation of state from a
previous compilation into the new program, the component's source file
is invalidated because one of its dependency has changed, ergo the
NgModule needs to be invalidated as well. Up until now, this implicit
dependency was not imposed on the NgModule. Additionally, any change to
a dependent file may influence the module scope to change, so all
components within the module must be invalidated as well.
This commit fixes the bug by introducing additional file dependencies,
as to ensure a proper rebuild of the module scope and its components.
Fixes #32416
PR Close #33522
2019-10-31 15:30:05 -04:00
|
|
|
env.flushWrittenFileTracking();
|
perf(compiler-cli): detect semantic changes and their effect on an incremental rebuild (#40947)
In Angular programs, changing a file may require other files to be
emitted as well due to implicit NgModule dependencies. For example, if
the selector of a directive is changed then all components that have
that directive in their compilation scope need to be recompiled, as the
change of selector may affect the directive matching results.
Until now, the compiler solved this problem using a single dependency
graph. The implicit NgModule dependencies were represented in this
graph, such that a changed file would correctly also cause other files
to be re-emitted. This approach is limited in a few ways:
1. The file dependency graph is used to determine whether it is safe to
reuse the analysis data of an Angular decorated class. This analysis
data is invariant to unrelated changes to the NgModule scope, but
because the single dependency graph also tracked the implicit
NgModule dependencies the compiler had to consider analysis data as
stale far more often than necessary.
2. It is typical for a change to e.g. a directive to not affect its
public API—its selector, inputs, outputs, or exportAs clause—in which
case there is no need to re-emit all declarations in scope, as their
compilation output wouldn't have changed.
This commit implements a mechanism by which the compiler is able to
determine the impact of a change by comparing it to the prior
compilation. To achieve this, a new graph is maintained that tracks all
public API information of all Angular decorated symbols. During an
incremental compilation this information is compared to the information
that was captured in the most recently succeeded compilation. This
determines the exact impact of the changes to the public API, which
is then used to determine which files need to be re-emitted.
Note that the file dependency graph remains, as it is still used to
track the dependencies of analysis data. This graph does no longer track
the implicit NgModule dependencies, which allows for better reuse of
analysis data.
These changes also fix a bug where template type-checking would fail to
incorporate changes made to a transitive base class of a
directive/component. This used to be a problem because transitive base
classes were not recorded as a transitive dependency in the file
dependency graph, such that prior type-check blocks would erroneously
be reused.
This commit also fixes an incorrectness where a change to a declaration
in NgModule `A` would not cause the declarations in NgModules that
import from NgModule `A` to be re-emitted. This was intentionally
incorrect as otherwise the performance of incremental rebuilds would
have been far worse. This is no longer a concern, as the compiler is now
able to only re-emit when actually necessary.
Fixes #34867
Fixes #40635
Closes #40728
PR Close #40947
2020-11-20 15:18:46 -05:00
|
|
|
env.write('dep.ts', `
|
|
|
|
export const SELECTOR = 'cmp_updated';
|
|
|
|
`);
|
fix(ivy): ensure module scope is rebuild on dependent change (#33522)
During incremental compilations, ngtsc needs to know which metadata
from a previous compilation can be reused, versus which metadata has to
be recomputed as some dependency was updated. Changes to
directives/components should cause the NgModule in which they are
declared to be recompiled, as the NgModule's compilation is dependent
on its directives/components.
When a dependent source file of a directive/component is updated,
however, a more subtle dependency should also cause to NgModule's source
file to be invalidated. During the reconciliation of state from a
previous compilation into the new program, the component's source file
is invalidated because one of its dependency has changed, ergo the
NgModule needs to be invalidated as well. Up until now, this implicit
dependency was not imposed on the NgModule. Additionally, any change to
a dependent file may influence the module scope to change, so all
components within the module must be invalidated as well.
This commit fixes the bug by introducing additional file dependencies,
as to ensure a proper rebuild of the module scope and its components.
Fixes #32416
PR Close #33522
2019-10-31 15:30:05 -04:00
|
|
|
env.driveMain();
|
|
|
|
const written = env.getFilesWrittenSinceLastFlush();
|
|
|
|
expect(written).not.toContain('/directive.js');
|
|
|
|
expect(written).not.toContain('/pipe.js');
|
perf(compiler-cli): detect semantic changes and their effect on an incremental rebuild (#40947)
In Angular programs, changing a file may require other files to be
emitted as well due to implicit NgModule dependencies. For example, if
the selector of a directive is changed then all components that have
that directive in their compilation scope need to be recompiled, as the
change of selector may affect the directive matching results.
Until now, the compiler solved this problem using a single dependency
graph. The implicit NgModule dependencies were represented in this
graph, such that a changed file would correctly also cause other files
to be re-emitted. This approach is limited in a few ways:
1. The file dependency graph is used to determine whether it is safe to
reuse the analysis data of an Angular decorated class. This analysis
data is invariant to unrelated changes to the NgModule scope, but
because the single dependency graph also tracked the implicit
NgModule dependencies the compiler had to consider analysis data as
stale far more often than necessary.
2. It is typical for a change to e.g. a directive to not affect its
public API—its selector, inputs, outputs, or exportAs clause—in which
case there is no need to re-emit all declarations in scope, as their
compilation output wouldn't have changed.
This commit implements a mechanism by which the compiler is able to
determine the impact of a change by comparing it to the prior
compilation. To achieve this, a new graph is maintained that tracks all
public API information of all Angular decorated symbols. During an
incremental compilation this information is compared to the information
that was captured in the most recently succeeded compilation. This
determines the exact impact of the changes to the public API, which
is then used to determine which files need to be re-emitted.
Note that the file dependency graph remains, as it is still used to
track the dependencies of analysis data. This graph does no longer track
the implicit NgModule dependencies, which allows for better reuse of
analysis data.
These changes also fix a bug where template type-checking would fail to
incorporate changes made to a transitive base class of a
directive/component. This used to be a problem because transitive base
classes were not recorded as a transitive dependency in the file
dependency graph, such that prior type-check blocks would erroneously
be reused.
This commit also fixes an incorrectness where a change to a declaration
in NgModule `A` would not cause the declarations in NgModules that
import from NgModule `A` to be re-emitted. This was intentionally
incorrect as otherwise the performance of incremental rebuilds would
have been far worse. This is no longer a concern, as the compiler is now
able to only re-emit when actually necessary.
Fixes #34867
Fixes #40635
Closes #40728
PR Close #40947
2020-11-20 15:18:46 -05:00
|
|
|
expect(written).not.toContain('/module.js');
|
fix(ivy): ensure module scope is rebuild on dependent change (#33522)
During incremental compilations, ngtsc needs to know which metadata
from a previous compilation can be reused, versus which metadata has to
be recomputed as some dependency was updated. Changes to
directives/components should cause the NgModule in which they are
declared to be recompiled, as the NgModule's compilation is dependent
on its directives/components.
When a dependent source file of a directive/component is updated,
however, a more subtle dependency should also cause to NgModule's source
file to be invalidated. During the reconciliation of state from a
previous compilation into the new program, the component's source file
is invalidated because one of its dependency has changed, ergo the
NgModule needs to be invalidated as well. Up until now, this implicit
dependency was not imposed on the NgModule. Additionally, any change to
a dependent file may influence the module scope to change, so all
components within the module must be invalidated as well.
This commit fixes the bug by introducing additional file dependencies,
as to ensure a proper rebuild of the module scope and its components.
Fixes #32416
PR Close #33522
2019-10-31 15:30:05 -04:00
|
|
|
expect(written).toContain('/component1.js');
|
|
|
|
expect(written).toContain('/component2.js');
|
|
|
|
expect(written).toContain('/dep.js');
|
|
|
|
});
|
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
it('should rebuild components where their NgModule declared dependencies have changed', () => {
|
|
|
|
setupFooBarProgram(env);
|
|
|
|
|
perf(compiler-cli): detect semantic changes and their effect on an incremental rebuild (#40947)
In Angular programs, changing a file may require other files to be
emitted as well due to implicit NgModule dependencies. For example, if
the selector of a directive is changed then all components that have
that directive in their compilation scope need to be recompiled, as the
change of selector may affect the directive matching results.
Until now, the compiler solved this problem using a single dependency
graph. The implicit NgModule dependencies were represented in this
graph, such that a changed file would correctly also cause other files
to be re-emitted. This approach is limited in a few ways:
1. The file dependency graph is used to determine whether it is safe to
reuse the analysis data of an Angular decorated class. This analysis
data is invariant to unrelated changes to the NgModule scope, but
because the single dependency graph also tracked the implicit
NgModule dependencies the compiler had to consider analysis data as
stale far more often than necessary.
2. It is typical for a change to e.g. a directive to not affect its
public API—its selector, inputs, outputs, or exportAs clause—in which
case there is no need to re-emit all declarations in scope, as their
compilation output wouldn't have changed.
This commit implements a mechanism by which the compiler is able to
determine the impact of a change by comparing it to the prior
compilation. To achieve this, a new graph is maintained that tracks all
public API information of all Angular decorated symbols. During an
incremental compilation this information is compared to the information
that was captured in the most recently succeeded compilation. This
determines the exact impact of the changes to the public API, which
is then used to determine which files need to be re-emitted.
Note that the file dependency graph remains, as it is still used to
track the dependencies of analysis data. This graph does no longer track
the implicit NgModule dependencies, which allows for better reuse of
analysis data.
These changes also fix a bug where template type-checking would fail to
incorporate changes made to a transitive base class of a
directive/component. This used to be a problem because transitive base
classes were not recorded as a transitive dependency in the file
dependency graph, such that prior type-check blocks would erroneously
be reused.
This commit also fixes an incorrectness where a change to a declaration
in NgModule `A` would not cause the declarations in NgModules that
import from NgModule `A` to be re-emitted. This was intentionally
incorrect as otherwise the performance of incremental rebuilds would
have been far worse. This is no longer a concern, as the compiler is now
able to only re-emit when actually necessary.
Fixes #34867
Fixes #40635
Closes #40728
PR Close #40947
2020-11-20 15:18:46 -05:00
|
|
|
// Rename the pipe so components that use it need to be recompiled.
|
|
|
|
env.write('foo_pipe.ts', `
|
|
|
|
import {Pipe} from '@angular/core';
|
|
|
|
|
|
|
|
@Pipe({name: 'foo_changed'})
|
2021-01-22 12:03:37 -05:00
|
|
|
export class FooPipe {
|
|
|
|
transform() {}
|
|
|
|
}
|
perf(compiler-cli): detect semantic changes and their effect on an incremental rebuild (#40947)
In Angular programs, changing a file may require other files to be
emitted as well due to implicit NgModule dependencies. For example, if
the selector of a directive is changed then all components that have
that directive in their compilation scope need to be recompiled, as the
change of selector may affect the directive matching results.
Until now, the compiler solved this problem using a single dependency
graph. The implicit NgModule dependencies were represented in this
graph, such that a changed file would correctly also cause other files
to be re-emitted. This approach is limited in a few ways:
1. The file dependency graph is used to determine whether it is safe to
reuse the analysis data of an Angular decorated class. This analysis
data is invariant to unrelated changes to the NgModule scope, but
because the single dependency graph also tracked the implicit
NgModule dependencies the compiler had to consider analysis data as
stale far more often than necessary.
2. It is typical for a change to e.g. a directive to not affect its
public API—its selector, inputs, outputs, or exportAs clause—in which
case there is no need to re-emit all declarations in scope, as their
compilation output wouldn't have changed.
This commit implements a mechanism by which the compiler is able to
determine the impact of a change by comparing it to the prior
compilation. To achieve this, a new graph is maintained that tracks all
public API information of all Angular decorated symbols. During an
incremental compilation this information is compared to the information
that was captured in the most recently succeeded compilation. This
determines the exact impact of the changes to the public API, which
is then used to determine which files need to be re-emitted.
Note that the file dependency graph remains, as it is still used to
track the dependencies of analysis data. This graph does no longer track
the implicit NgModule dependencies, which allows for better reuse of
analysis data.
These changes also fix a bug where template type-checking would fail to
incorporate changes made to a transitive base class of a
directive/component. This used to be a problem because transitive base
classes were not recorded as a transitive dependency in the file
dependency graph, such that prior type-check blocks would erroneously
be reused.
This commit also fixes an incorrectness where a change to a declaration
in NgModule `A` would not cause the declarations in NgModules that
import from NgModule `A` to be re-emitted. This was intentionally
incorrect as otherwise the performance of incremental rebuilds would
have been far worse. This is no longer a concern, as the compiler is now
able to only re-emit when actually necessary.
Fixes #34867
Fixes #40635
Closes #40728
PR Close #40947
2020-11-20 15:18:46 -05:00
|
|
|
`);
|
2019-06-06 15:22:32 -04:00
|
|
|
env.driveMain();
|
|
|
|
const written = env.getFilesWrittenSinceLastFlush();
|
|
|
|
expect(written).not.toContain('/bar_directive.js');
|
|
|
|
expect(written).not.toContain('/bar_component.js');
|
|
|
|
expect(written).not.toContain('/bar_module.js');
|
|
|
|
expect(written).toContain('/foo_component.js');
|
|
|
|
expect(written).toContain('/foo_pipe.js');
|
|
|
|
expect(written).toContain('/foo_module.js');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should rebuild components where their NgModule has changed', () => {
|
|
|
|
setupFooBarProgram(env);
|
|
|
|
|
perf(compiler-cli): detect semantic changes and their effect on an incremental rebuild (#40947)
In Angular programs, changing a file may require other files to be
emitted as well due to implicit NgModule dependencies. For example, if
the selector of a directive is changed then all components that have
that directive in their compilation scope need to be recompiled, as the
change of selector may affect the directive matching results.
Until now, the compiler solved this problem using a single dependency
graph. The implicit NgModule dependencies were represented in this
graph, such that a changed file would correctly also cause other files
to be re-emitted. This approach is limited in a few ways:
1. The file dependency graph is used to determine whether it is safe to
reuse the analysis data of an Angular decorated class. This analysis
data is invariant to unrelated changes to the NgModule scope, but
because the single dependency graph also tracked the implicit
NgModule dependencies the compiler had to consider analysis data as
stale far more often than necessary.
2. It is typical for a change to e.g. a directive to not affect its
public API—its selector, inputs, outputs, or exportAs clause—in which
case there is no need to re-emit all declarations in scope, as their
compilation output wouldn't have changed.
This commit implements a mechanism by which the compiler is able to
determine the impact of a change by comparing it to the prior
compilation. To achieve this, a new graph is maintained that tracks all
public API information of all Angular decorated symbols. During an
incremental compilation this information is compared to the information
that was captured in the most recently succeeded compilation. This
determines the exact impact of the changes to the public API, which
is then used to determine which files need to be re-emitted.
Note that the file dependency graph remains, as it is still used to
track the dependencies of analysis data. This graph does no longer track
the implicit NgModule dependencies, which allows for better reuse of
analysis data.
These changes also fix a bug where template type-checking would fail to
incorporate changes made to a transitive base class of a
directive/component. This used to be a problem because transitive base
classes were not recorded as a transitive dependency in the file
dependency graph, such that prior type-check blocks would erroneously
be reused.
This commit also fixes an incorrectness where a change to a declaration
in NgModule `A` would not cause the declarations in NgModules that
import from NgModule `A` to be re-emitted. This was intentionally
incorrect as otherwise the performance of incremental rebuilds would
have been far worse. This is no longer a concern, as the compiler is now
able to only re-emit when actually necessary.
Fixes #34867
Fixes #40635
Closes #40728
PR Close #40947
2020-11-20 15:18:46 -05:00
|
|
|
// Pretend a change was made to FooModule.
|
|
|
|
env.write('foo_module.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {FooCmp} from './foo_component';
|
|
|
|
import {FooPipe} from './foo_pipe';
|
|
|
|
import {BarModule} from './bar_module';
|
|
|
|
@NgModule({
|
|
|
|
declarations: [FooCmp], // removed FooPipe
|
|
|
|
imports: [BarModule],
|
|
|
|
})
|
|
|
|
export class FooModule {}
|
|
|
|
`);
|
2019-06-06 15:22:32 -04:00
|
|
|
env.driveMain();
|
|
|
|
const written = env.getFilesWrittenSinceLastFlush();
|
|
|
|
expect(written).not.toContain('/bar_directive.js');
|
|
|
|
expect(written).not.toContain('/bar_component.js');
|
|
|
|
expect(written).not.toContain('/bar_module.js');
|
perf(compiler-cli): detect semantic changes and their effect on an incremental rebuild (#40947)
In Angular programs, changing a file may require other files to be
emitted as well due to implicit NgModule dependencies. For example, if
the selector of a directive is changed then all components that have
that directive in their compilation scope need to be recompiled, as the
change of selector may affect the directive matching results.
Until now, the compiler solved this problem using a single dependency
graph. The implicit NgModule dependencies were represented in this
graph, such that a changed file would correctly also cause other files
to be re-emitted. This approach is limited in a few ways:
1. The file dependency graph is used to determine whether it is safe to
reuse the analysis data of an Angular decorated class. This analysis
data is invariant to unrelated changes to the NgModule scope, but
because the single dependency graph also tracked the implicit
NgModule dependencies the compiler had to consider analysis data as
stale far more often than necessary.
2. It is typical for a change to e.g. a directive to not affect its
public API—its selector, inputs, outputs, or exportAs clause—in which
case there is no need to re-emit all declarations in scope, as their
compilation output wouldn't have changed.
This commit implements a mechanism by which the compiler is able to
determine the impact of a change by comparing it to the prior
compilation. To achieve this, a new graph is maintained that tracks all
public API information of all Angular decorated symbols. During an
incremental compilation this information is compared to the information
that was captured in the most recently succeeded compilation. This
determines the exact impact of the changes to the public API, which
is then used to determine which files need to be re-emitted.
Note that the file dependency graph remains, as it is still used to
track the dependencies of analysis data. This graph does no longer track
the implicit NgModule dependencies, which allows for better reuse of
analysis data.
These changes also fix a bug where template type-checking would fail to
incorporate changes made to a transitive base class of a
directive/component. This used to be a problem because transitive base
classes were not recorded as a transitive dependency in the file
dependency graph, such that prior type-check blocks would erroneously
be reused.
This commit also fixes an incorrectness where a change to a declaration
in NgModule `A` would not cause the declarations in NgModules that
import from NgModule `A` to be re-emitted. This was intentionally
incorrect as otherwise the performance of incremental rebuilds would
have been far worse. This is no longer a concern, as the compiler is now
able to only re-emit when actually necessary.
Fixes #34867
Fixes #40635
Closes #40728
PR Close #40947
2020-11-20 15:18:46 -05:00
|
|
|
expect(written).not.toContain('/foo_pipe.js');
|
2019-06-06 15:22:32 -04:00
|
|
|
expect(written).toContain('/foo_component.js');
|
|
|
|
expect(written).toContain('/foo_module.js');
|
|
|
|
});
|
|
|
|
|
fix(compiler-cli): fix bug tracking indirect NgModule dependencies (#36211)
The compiler needs to track the dependencies of a component, including any
NgModules which happen to be present in a component's scope. If an upstream
NgModule changes, any downstream components need to have their templates
re-compiled and re-typechecked.
Previously, the compiler handled this well for the A -> B -> C case where
module A imports module B which re-exports module C. However, it fell apart
in the A -> B -> C -> D case, because previously tracking focused on changes
to components/directives in the scope, and not NgModules specifically.
This commit introduces logic to track which NgModules contributed to a given
scope, and treat them as dependencies of any components within.
This logic also contains a bug, which is intentional for now. It
purposefully does not track transitive dependencies of the NgModules which
contribute to a scope. If it did, using the current dependency system, this
would treat all components and directives (even those not exported into the
scope) as dependencies, causing a major performance bottleneck. Only those
dependencies which contributed to the module's export scope should be
considered, but the current system is incapable of making this distinction.
This will be fixed at a later date.
PR Close #36211
2020-03-19 14:25:15 -04:00
|
|
|
it('should rebuild a component if one of its deep NgModule dependencies changes', () => {
|
|
|
|
// This test constructs a chain of NgModules:
|
|
|
|
// - EntryModule imports MiddleAModule
|
|
|
|
// - MiddleAModule exports MiddleBModule
|
|
|
|
// - MiddleBModule exports DirModule
|
|
|
|
// The last link (MiddleBModule exports DirModule) is not present initially, but is added
|
|
|
|
// during a recompilation.
|
|
|
|
//
|
|
|
|
// Since the dependency from EntryModule on the contents of MiddleBModule is not "direct"
|
|
|
|
// (meaning MiddleBModule is not discovered during analysis of EntryModule), this test is
|
|
|
|
// verifying that NgModule dependency tracking correctly accounts for this situation.
|
|
|
|
env.write('entry.ts', `
|
|
|
|
import {Component, NgModule} from '@angular/core';
|
|
|
|
import {MiddleAModule} from './middle-a';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'test-cmp',
|
|
|
|
template: '<div dir>',
|
|
|
|
})
|
|
|
|
export class TestCmp {}
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [TestCmp],
|
|
|
|
imports: [MiddleAModule],
|
|
|
|
})
|
|
|
|
export class EntryModule {}
|
|
|
|
`);
|
|
|
|
env.write('middle-a.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {MiddleBModule} from './middle-b';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
exports: [MiddleBModule],
|
|
|
|
})
|
|
|
|
export class MiddleAModule {}
|
|
|
|
`);
|
|
|
|
env.write('middle-b.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
|
|
|
|
@NgModule({})
|
|
|
|
export class MiddleBModule {}
|
|
|
|
`);
|
|
|
|
env.write('dir_module.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {Dir} from './dir';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Dir],
|
|
|
|
exports: [Dir],
|
|
|
|
})
|
|
|
|
export class DirModule {}
|
|
|
|
`);
|
|
|
|
env.write('dir.ts', `
|
|
|
|
import {Directive} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({
|
|
|
|
selector: '[dir]',
|
|
|
|
})
|
|
|
|
export class Dir {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
expect(env.getContents('entry.js')).not.toContain('Dir');
|
|
|
|
|
|
|
|
env.write('middle-b.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {DirModule} from './dir_module';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
exports: [DirModule],
|
|
|
|
})
|
|
|
|
export class MiddleBModule {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
expect(env.getContents('entry.js')).toContain('Dir');
|
|
|
|
});
|
|
|
|
|
fix(ivy): correctly emit component when it's removed from its module (#34912)
This commit fixes a bug in the incremental rebuild engine of ngtsc, where if
a component was removed from its NgModule, it would not be properly
re-emitted.
The bug stemmed from the fact that whether to emit a file was a decision
based purely on the updated dependency graph, which captures the dependency
structure of the rebuild program. This graph has no edge from the component
to its former module (as it was removed, of course), so the compiler
erroneously decides not to emit the component.
The bug here is that the compiler does know, from the previous dependency
graph, that the component file has logically changed, since its previous
dependency (the module file) has changed. This information was not carried
forward into the set of files which need to be emitted, because it was
assumed that the updated dependency graph was a more accurate source of that
information.
With this commit, the set of files which need emit is pre-populated with the
set of logically changed files, to cover edge cases like this.
Fixes #34813
PR Close #34912
2020-01-22 16:34:39 -05:00
|
|
|
it('should rebuild a component if removed from an NgModule', () => {
|
|
|
|
// This test consists of a component with a dependency (the directive DepDir) provided via an
|
|
|
|
// NgModule. Initially this configuration is built, then the component is removed from its
|
|
|
|
// module (which removes DepDir from the component's scope) and a rebuild is performed.
|
|
|
|
// The compiler should re-emit the component without DepDir in its scope.
|
|
|
|
//
|
|
|
|
// This is a tricky scenario due to the backwards dependency arrow from a component to its
|
|
|
|
// module.
|
|
|
|
env.write('dep.ts', `
|
|
|
|
import {Directive, NgModule} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({selector: '[dep]'})
|
|
|
|
export class DepDir {}
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [DepDir],
|
|
|
|
exports: [DepDir],
|
|
|
|
})
|
|
|
|
export class DepModule {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.write('cmp.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'test-cmp',
|
|
|
|
template: '<div dep></div>',
|
|
|
|
})
|
|
|
|
export class Cmp {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.write('module.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {Cmp} from './cmp';
|
|
|
|
import {DepModule} from './dep';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Cmp],
|
|
|
|
imports: [DepModule],
|
|
|
|
})
|
|
|
|
export class Module {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
|
|
|
|
// Remove the component from the module and recompile.
|
|
|
|
env.write('module.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {DepModule} from './dep';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [],
|
|
|
|
imports: [DepModule],
|
|
|
|
})
|
|
|
|
export class Module {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
// After removing the component from the module, it should have been re-emitted without DepDir
|
|
|
|
// in its scope.
|
|
|
|
expect(env.getFilesWrittenSinceLastFlush()).toContain('/cmp.js');
|
|
|
|
expect(env.getContents('cmp.js')).not.toContain('DepDir');
|
|
|
|
});
|
|
|
|
|
perf(compiler-cli): detect semantic changes and their effect on an incremental rebuild (#40947)
In Angular programs, changing a file may require other files to be
emitted as well due to implicit NgModule dependencies. For example, if
the selector of a directive is changed then all components that have
that directive in their compilation scope need to be recompiled, as the
change of selector may affect the directive matching results.
Until now, the compiler solved this problem using a single dependency
graph. The implicit NgModule dependencies were represented in this
graph, such that a changed file would correctly also cause other files
to be re-emitted. This approach is limited in a few ways:
1. The file dependency graph is used to determine whether it is safe to
reuse the analysis data of an Angular decorated class. This analysis
data is invariant to unrelated changes to the NgModule scope, but
because the single dependency graph also tracked the implicit
NgModule dependencies the compiler had to consider analysis data as
stale far more often than necessary.
2. It is typical for a change to e.g. a directive to not affect its
public API—its selector, inputs, outputs, or exportAs clause—in which
case there is no need to re-emit all declarations in scope, as their
compilation output wouldn't have changed.
This commit implements a mechanism by which the compiler is able to
determine the impact of a change by comparing it to the prior
compilation. To achieve this, a new graph is maintained that tracks all
public API information of all Angular decorated symbols. During an
incremental compilation this information is compared to the information
that was captured in the most recently succeeded compilation. This
determines the exact impact of the changes to the public API, which
is then used to determine which files need to be re-emitted.
Note that the file dependency graph remains, as it is still used to
track the dependencies of analysis data. This graph does no longer track
the implicit NgModule dependencies, which allows for better reuse of
analysis data.
These changes also fix a bug where template type-checking would fail to
incorporate changes made to a transitive base class of a
directive/component. This used to be a problem because transitive base
classes were not recorded as a transitive dependency in the file
dependency graph, such that prior type-check blocks would erroneously
be reused.
This commit also fixes an incorrectness where a change to a declaration
in NgModule `A` would not cause the declarations in NgModules that
import from NgModule `A` to be re-emitted. This was intentionally
incorrect as otherwise the performance of incremental rebuilds would
have been far worse. This is no longer a concern, as the compiler is now
able to only re-emit when actually necessary.
Fixes #34867
Fixes #40635
Closes #40728
PR Close #40947
2020-11-20 15:18:46 -05:00
|
|
|
it('should rebuild only a Component (but with the correct CompilationScope) if its template has changed',
|
2019-07-31 12:11:33 -04:00
|
|
|
() => {
|
|
|
|
setupFooBarProgram(env);
|
|
|
|
|
|
|
|
// Make a change to the template of BarComponent.
|
|
|
|
env.write('bar_component.html', '<div bar>changed</div>');
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
const written = env.getFilesWrittenSinceLastFlush();
|
|
|
|
expect(written).not.toContain('/bar_directive.js');
|
|
|
|
expect(written).toContain('/bar_component.js');
|
perf(compiler-cli): detect semantic changes and their effect on an incremental rebuild (#40947)
In Angular programs, changing a file may require other files to be
emitted as well due to implicit NgModule dependencies. For example, if
the selector of a directive is changed then all components that have
that directive in their compilation scope need to be recompiled, as the
change of selector may affect the directive matching results.
Until now, the compiler solved this problem using a single dependency
graph. The implicit NgModule dependencies were represented in this
graph, such that a changed file would correctly also cause other files
to be re-emitted. This approach is limited in a few ways:
1. The file dependency graph is used to determine whether it is safe to
reuse the analysis data of an Angular decorated class. This analysis
data is invariant to unrelated changes to the NgModule scope, but
because the single dependency graph also tracked the implicit
NgModule dependencies the compiler had to consider analysis data as
stale far more often than necessary.
2. It is typical for a change to e.g. a directive to not affect its
public API—its selector, inputs, outputs, or exportAs clause—in which
case there is no need to re-emit all declarations in scope, as their
compilation output wouldn't have changed.
This commit implements a mechanism by which the compiler is able to
determine the impact of a change by comparing it to the prior
compilation. To achieve this, a new graph is maintained that tracks all
public API information of all Angular decorated symbols. During an
incremental compilation this information is compared to the information
that was captured in the most recently succeeded compilation. This
determines the exact impact of the changes to the public API, which
is then used to determine which files need to be re-emitted.
Note that the file dependency graph remains, as it is still used to
track the dependencies of analysis data. This graph does no longer track
the implicit NgModule dependencies, which allows for better reuse of
analysis data.
These changes also fix a bug where template type-checking would fail to
incorporate changes made to a transitive base class of a
directive/component. This used to be a problem because transitive base
classes were not recorded as a transitive dependency in the file
dependency graph, such that prior type-check blocks would erroneously
be reused.
This commit also fixes an incorrectness where a change to a declaration
in NgModule `A` would not cause the declarations in NgModules that
import from NgModule `A` to be re-emitted. This was intentionally
incorrect as otherwise the performance of incremental rebuilds would
have been far worse. This is no longer a concern, as the compiler is now
able to only re-emit when actually necessary.
Fixes #34867
Fixes #40635
Closes #40728
PR Close #40947
2020-11-20 15:18:46 -05:00
|
|
|
expect(written).not.toContain('/bar_module.js');
|
2019-07-31 12:11:33 -04:00
|
|
|
expect(written).not.toContain('/foo_component.js');
|
|
|
|
expect(written).not.toContain('/foo_pipe.js');
|
|
|
|
expect(written).not.toContain('/foo_module.js');
|
|
|
|
// Ensure that the used directives are included in the component's generated template.
|
|
|
|
expect(env.getContents('/built/bar_component.js')).toMatch(/directives:\s*\[.+\.BarDir\]/);
|
|
|
|
});
|
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
it('should rebuild everything if a typings file changes', () => {
|
|
|
|
setupFooBarProgram(env);
|
|
|
|
|
|
|
|
// Pretend a change was made to a typings file.
|
|
|
|
env.invalidateCachedFile('foo_selector.d.ts');
|
|
|
|
env.driveMain();
|
|
|
|
const written = env.getFilesWrittenSinceLastFlush();
|
|
|
|
expect(written).toContain('/bar_directive.js');
|
|
|
|
expect(written).toContain('/bar_component.js');
|
|
|
|
expect(written).toContain('/bar_module.js');
|
|
|
|
expect(written).toContain('/foo_component.js');
|
|
|
|
expect(written).toContain('/foo_pipe.js');
|
|
|
|
expect(written).toContain('/foo_module.js');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should compile incrementally with template type-checking turned on', () => {
|
2020-09-26 16:28:12 -04:00
|
|
|
env.tsconfig({fullTemplateTypeCheck: true});
|
|
|
|
env.write('main.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({template: ''})
|
|
|
|
export class MyComponent {}
|
|
|
|
`);
|
2019-06-06 15:22:32 -04:00
|
|
|
env.driveMain();
|
|
|
|
env.invalidateCachedFile('main.ts');
|
|
|
|
env.driveMain();
|
|
|
|
// If program reuse were configured incorrectly (as was responsible for
|
|
|
|
// https://github.com/angular/angular/issues/30079), this would have crashed.
|
|
|
|
});
|
2019-07-08 10:50:19 -04:00
|
|
|
|
2020-09-26 16:14:10 -04:00
|
|
|
// 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);
|
|
|
|
});
|
|
|
|
|
2019-07-08 10:50:19 -04:00
|
|
|
// https://github.com/angular/angular/pull/26036
|
|
|
|
it('should handle 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, we must
|
|
|
|
// assure not to reintroduce "node_modules/b/node_modules/a/index.d.ts" as its redirected
|
|
|
|
// source file, but instead use its original file.
|
|
|
|
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('test.ts', `
|
2020-09-26 16:28:12 -04:00
|
|
|
import {Component} from '@angular/core';
|
2019-07-08 10:50:19 -04:00
|
|
|
import {ServiceA} from 'a';
|
|
|
|
import {ServiceB} from 'b';
|
2020-09-26 16:28:12 -04:00
|
|
|
|
|
|
|
@Component({template: ''})
|
|
|
|
export class MyComponent {}
|
2019-07-08 10:50:19 -04:00
|
|
|
`);
|
|
|
|
env.driveMain();
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
|
|
|
|
// Pretend a change was made to test.ts. If redirect sources were introduced into the new
|
|
|
|
// program, this would fail due to an assertion failure in TS.
|
|
|
|
env.invalidateCachedFile('test.ts');
|
|
|
|
env.driveMain();
|
|
|
|
});
|
perf(compiler-cli): perform template type-checking incrementally (#36211)
This optimization builds on a lot of prior work to finally make type-
checking of templates incremental.
Incrementality requires two main components:
- the ability to reuse work from a prior compilation.
- the ability to know when changes in the current program invalidate that
prior work.
Prior to this commit, on every type-checking pass the compiler would
generate new .ngtypecheck files for each original input file in the program.
1. (Build #1 main program): empty .ngtypecheck files generated for each
original input file.
2. (Build #1 type-check program): .ngtypecheck contents overridden for those
which have corresponding components that need type-checked.
3. (Build #2 main program): throw away old .ngtypecheck files and generate
new empty ones.
4. (Build #2 type-check program): same as step 2.
With this commit, the `IncrementalDriver` now tracks template type-checking
_metadata_ for each input file. The metadata contains information about
source mappings for generated type-checking code, as well as some
diagnostics which were discovered at type-check analysis time. The actual
type-checking code is stored in the TypeScript AST for type-checking files,
which is now re-used between programs as follows:
1. (Build #1 main program): empty .ngtypecheck files generated for each
original input file.
2. (Build #1 type-check program): .ngtypecheck contents overridden for those
which have corresponding components that need type-checked, and the
metadata registered in the `IncrementalDriver`.
3. (Build #2 main program): The `TypeCheckShimGenerator` now reuses _all_
.ngtypecheck `ts.SourceFile` shims from build #1's type-check program in
the construction of build #2's main program. Some of the contents of
these files might be stale (if a component's template changed, for
example), but wholesale reuse here prevents unnecessary changes in the
contents of the program at this point and makes TypeScript's job a lot
easier.
4. (Build #2 type-check program): For those input files which have not
"logically changed" (meaning components within are semantically the same
as they were before), the compiler will re-use the type-check file
metadata from build #1, and _not_ generate a new .ngtypecheck shim.
For components which have logically changed or where the previous
.ngtypecheck contents cannot otherwise be reused, code generation happens
as before.
PR Close #36211
2020-03-19 14:29:58 -04:00
|
|
|
|
2021-04-04 16:09:17 -04:00
|
|
|
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');
|
|
|
|
});
|
|
|
|
|
perf(compiler-cli): perform template type-checking incrementally (#36211)
This optimization builds on a lot of prior work to finally make type-
checking of templates incremental.
Incrementality requires two main components:
- the ability to reuse work from a prior compilation.
- the ability to know when changes in the current program invalidate that
prior work.
Prior to this commit, on every type-checking pass the compiler would
generate new .ngtypecheck files for each original input file in the program.
1. (Build #1 main program): empty .ngtypecheck files generated for each
original input file.
2. (Build #1 type-check program): .ngtypecheck contents overridden for those
which have corresponding components that need type-checked.
3. (Build #2 main program): throw away old .ngtypecheck files and generate
new empty ones.
4. (Build #2 type-check program): same as step 2.
With this commit, the `IncrementalDriver` now tracks template type-checking
_metadata_ for each input file. The metadata contains information about
source mappings for generated type-checking code, as well as some
diagnostics which were discovered at type-check analysis time. The actual
type-checking code is stored in the TypeScript AST for type-checking files,
which is now re-used between programs as follows:
1. (Build #1 main program): empty .ngtypecheck files generated for each
original input file.
2. (Build #1 type-check program): .ngtypecheck contents overridden for those
which have corresponding components that need type-checked, and the
metadata registered in the `IncrementalDriver`.
3. (Build #2 main program): The `TypeCheckShimGenerator` now reuses _all_
.ngtypecheck `ts.SourceFile` shims from build #1's type-check program in
the construction of build #2's main program. Some of the contents of
these files might be stale (if a component's template changed, for
example), but wholesale reuse here prevents unnecessary changes in the
contents of the program at this point and makes TypeScript's job a lot
easier.
4. (Build #2 type-check program): For those input files which have not
"logically changed" (meaning components within are semantically the same
as they were before), the compiler will re-use the type-check file
metadata from build #1, and _not_ generate a new .ngtypecheck shim.
For components which have logically changed or where the previous
.ngtypecheck contents cannot otherwise be reused, code generation happens
as before.
PR Close #36211
2020-03-19 14:29:58 -04:00
|
|
|
describe('template type-checking', () => {
|
|
|
|
beforeEach(() => {
|
|
|
|
env.tsconfig({strictTemplates: true});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should repeat type errors across rebuilds, even if nothing has changed', () => {
|
|
|
|
// This test verifies that when a project is rebuilt multiple times with no changes, all
|
|
|
|
// template diagnostics are produced each time. Different types of errors are checked:
|
|
|
|
// - a general type error
|
|
|
|
// - an unmatched binding
|
|
|
|
// - a DOM schema error
|
|
|
|
env.write('component.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
@Component({
|
|
|
|
selector: 'test-cmp',
|
|
|
|
template: \`
|
|
|
|
{{notAProperty}}
|
|
|
|
<not-a-component></not-a-component>
|
|
|
|
<div [notMatched]="1"></div>
|
|
|
|
\`,
|
|
|
|
})
|
|
|
|
export class TestCmp {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
let diags = env.driveDiagnostics();
|
|
|
|
// Should get a diagnostic for each line in the template.
|
|
|
|
expect(diags.length).toBe(3);
|
|
|
|
|
|
|
|
// Now rebuild without any changes, and verify they're still produced.
|
|
|
|
diags = env.driveDiagnostics();
|
|
|
|
expect(diags.length).toBe(3);
|
|
|
|
|
|
|
|
// If it's worth testing, it's worth overtesting.
|
|
|
|
//
|
|
|
|
// Actually, the above only tests the transition from "initial" to "incremental"
|
|
|
|
// compilation. The next build verifies that an "incremental to incremental" transition
|
|
|
|
// preserves the diagnostics as well.
|
|
|
|
diags = env.driveDiagnostics();
|
|
|
|
expect(diags.length).toBe(3);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should pick up errors caused by changing an unrelated interface', () => {
|
|
|
|
// The premise of this test is that `iface.ts` declares an interface which is used to type
|
|
|
|
// a property of a component. The interface is then changed in a subsequent compilation in
|
|
|
|
// a way that introduces a type error in the template. The test verifies the resulting
|
|
|
|
// diagnostic is produced.
|
|
|
|
env.write('iface.ts', `
|
|
|
|
export interface SomeType {
|
|
|
|
field: string;
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
env.write('component.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
import {SomeType} from './iface';
|
2020-05-19 15:08:49 -04:00
|
|
|
|
perf(compiler-cli): perform template type-checking incrementally (#36211)
This optimization builds on a lot of prior work to finally make type-
checking of templates incremental.
Incrementality requires two main components:
- the ability to reuse work from a prior compilation.
- the ability to know when changes in the current program invalidate that
prior work.
Prior to this commit, on every type-checking pass the compiler would
generate new .ngtypecheck files for each original input file in the program.
1. (Build #1 main program): empty .ngtypecheck files generated for each
original input file.
2. (Build #1 type-check program): .ngtypecheck contents overridden for those
which have corresponding components that need type-checked.
3. (Build #2 main program): throw away old .ngtypecheck files and generate
new empty ones.
4. (Build #2 type-check program): same as step 2.
With this commit, the `IncrementalDriver` now tracks template type-checking
_metadata_ for each input file. The metadata contains information about
source mappings for generated type-checking code, as well as some
diagnostics which were discovered at type-check analysis time. The actual
type-checking code is stored in the TypeScript AST for type-checking files,
which is now re-used between programs as follows:
1. (Build #1 main program): empty .ngtypecheck files generated for each
original input file.
2. (Build #1 type-check program): .ngtypecheck contents overridden for those
which have corresponding components that need type-checked, and the
metadata registered in the `IncrementalDriver`.
3. (Build #2 main program): The `TypeCheckShimGenerator` now reuses _all_
.ngtypecheck `ts.SourceFile` shims from build #1's type-check program in
the construction of build #2's main program. Some of the contents of
these files might be stale (if a component's template changed, for
example), but wholesale reuse here prevents unnecessary changes in the
contents of the program at this point and makes TypeScript's job a lot
easier.
4. (Build #2 type-check program): For those input files which have not
"logically changed" (meaning components within are semantically the same
as they were before), the compiler will re-use the type-check file
metadata from build #1, and _not_ generate a new .ngtypecheck shim.
For components which have logically changed or where the previous
.ngtypecheck contents cannot otherwise be reused, code generation happens
as before.
PR Close #36211
2020-03-19 14:29:58 -04:00
|
|
|
@Component({
|
|
|
|
selector: 'test-cmp',
|
|
|
|
template: '{{ doSomething(value.field) }}',
|
|
|
|
})
|
|
|
|
export class TestCmp {
|
|
|
|
value!: SomeType;
|
|
|
|
// Takes a string value only.
|
|
|
|
doSomething(param: string): string {
|
|
|
|
return param;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
|
|
|
|
expect(env.driveDiagnostics().length).toBe(0);
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
|
|
|
|
// Change the interface.
|
|
|
|
env.write('iface.ts', `
|
|
|
|
export interface SomeType {
|
|
|
|
field: number;
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
|
|
|
|
expect(env.driveDiagnostics().length).toBe(1);
|
|
|
|
});
|
|
|
|
|
2021-04-10 15:45:59 -04:00
|
|
|
it('should retain default imports that have been converted into a value expression', () => {
|
|
|
|
// This test defines the component `TestCmp` that has a default-imported class as
|
|
|
|
// constructor parameter, and uses `TestDir` in its template. An incremental compilation
|
|
|
|
// updates `TestDir` and changes its inputs, thereby triggering re-emit of `TestCmp` without
|
|
|
|
// performing re-analysis of `TestCmp`. The output of the re-emitted file for `TestCmp`
|
|
|
|
// should continue to have retained the default import.
|
|
|
|
env.write('service.ts', `
|
|
|
|
import {Injectable} from '@angular/core';
|
|
|
|
|
|
|
|
@Injectable({ providedIn: 'root' })
|
|
|
|
export default class DefaultService {}
|
|
|
|
`);
|
|
|
|
env.write('cmp.ts', `
|
|
|
|
import {Component, Directive} from '@angular/core';
|
|
|
|
import DefaultService from './service';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
template: '<div dir></div>',
|
|
|
|
})
|
|
|
|
export class TestCmp {
|
|
|
|
constructor(service: DefaultService) {}
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
env.write('dir.ts', `
|
|
|
|
import {Directive} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({ selector: '[dir]' })
|
|
|
|
export class TestDir {}
|
|
|
|
`);
|
|
|
|
env.write('mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {TestDir} from './dir';
|
|
|
|
import {TestCmp} from './cmp';
|
|
|
|
|
|
|
|
@NgModule({ declarations: [TestDir, TestCmp] })
|
|
|
|
export class TestMod {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
|
|
|
|
// Update `TestDir` to change its inputs, triggering a re-emit of `TestCmp` that uses
|
|
|
|
// `TestDir`.
|
|
|
|
env.write('dir.ts', `
|
|
|
|
import {Directive} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({ selector: '[dir]', inputs: ['added'] })
|
|
|
|
export class TestDir {}
|
|
|
|
`);
|
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
// Verify that `TestCmp` was indeed re-emitted.
|
|
|
|
const written = env.getFilesWrittenSinceLastFlush();
|
|
|
|
expect(written).toContain('/dir.js');
|
|
|
|
expect(written).toContain('/cmp.js');
|
|
|
|
|
|
|
|
// Verify that the default import is still present.
|
|
|
|
const content = env.getContents('cmp.js');
|
|
|
|
expect(content).toContain(`import DefaultService from './service';`);
|
|
|
|
});
|
|
|
|
|
perf(compiler-cli): perform template type-checking incrementally (#36211)
This optimization builds on a lot of prior work to finally make type-
checking of templates incremental.
Incrementality requires two main components:
- the ability to reuse work from a prior compilation.
- the ability to know when changes in the current program invalidate that
prior work.
Prior to this commit, on every type-checking pass the compiler would
generate new .ngtypecheck files for each original input file in the program.
1. (Build #1 main program): empty .ngtypecheck files generated for each
original input file.
2. (Build #1 type-check program): .ngtypecheck contents overridden for those
which have corresponding components that need type-checked.
3. (Build #2 main program): throw away old .ngtypecheck files and generate
new empty ones.
4. (Build #2 type-check program): same as step 2.
With this commit, the `IncrementalDriver` now tracks template type-checking
_metadata_ for each input file. The metadata contains information about
source mappings for generated type-checking code, as well as some
diagnostics which were discovered at type-check analysis time. The actual
type-checking code is stored in the TypeScript AST for type-checking files,
which is now re-used between programs as follows:
1. (Build #1 main program): empty .ngtypecheck files generated for each
original input file.
2. (Build #1 type-check program): .ngtypecheck contents overridden for those
which have corresponding components that need type-checked, and the
metadata registered in the `IncrementalDriver`.
3. (Build #2 main program): The `TypeCheckShimGenerator` now reuses _all_
.ngtypecheck `ts.SourceFile` shims from build #1's type-check program in
the construction of build #2's main program. Some of the contents of
these files might be stale (if a component's template changed, for
example), but wholesale reuse here prevents unnecessary changes in the
contents of the program at this point and makes TypeScript's job a lot
easier.
4. (Build #2 type-check program): For those input files which have not
"logically changed" (meaning components within are semantically the same
as they were before), the compiler will re-use the type-check file
metadata from build #1, and _not_ generate a new .ngtypecheck shim.
For components which have logically changed or where the previous
.ngtypecheck contents cannot otherwise be reused, code generation happens
as before.
PR Close #36211
2020-03-19 14:29:58 -04:00
|
|
|
it('should recompile when a remote change happens to a scope', () => {
|
|
|
|
// The premise of this test is that the component Cmp has a template error (a binding to an
|
|
|
|
// unknown property). Cmp is in ModuleA, which imports ModuleB, which declares Dir that has
|
|
|
|
// the property. Because ModuleB doesn't export Dir, it's not visible to Cmp - hence the
|
|
|
|
// error.
|
|
|
|
// In the test, during the incremental rebuild Dir is exported from ModuleB. This is a
|
|
|
|
// change to the scope of ModuleA made by changing ModuleB (hence, a "remote change"). The
|
|
|
|
// test verifies that incremental template type-checking.
|
|
|
|
env.write('cmp.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
2020-05-19 15:08:49 -04:00
|
|
|
|
perf(compiler-cli): perform template type-checking incrementally (#36211)
This optimization builds on a lot of prior work to finally make type-
checking of templates incremental.
Incrementality requires two main components:
- the ability to reuse work from a prior compilation.
- the ability to know when changes in the current program invalidate that
prior work.
Prior to this commit, on every type-checking pass the compiler would
generate new .ngtypecheck files for each original input file in the program.
1. (Build #1 main program): empty .ngtypecheck files generated for each
original input file.
2. (Build #1 type-check program): .ngtypecheck contents overridden for those
which have corresponding components that need type-checked.
3. (Build #2 main program): throw away old .ngtypecheck files and generate
new empty ones.
4. (Build #2 type-check program): same as step 2.
With this commit, the `IncrementalDriver` now tracks template type-checking
_metadata_ for each input file. The metadata contains information about
source mappings for generated type-checking code, as well as some
diagnostics which were discovered at type-check analysis time. The actual
type-checking code is stored in the TypeScript AST for type-checking files,
which is now re-used between programs as follows:
1. (Build #1 main program): empty .ngtypecheck files generated for each
original input file.
2. (Build #1 type-check program): .ngtypecheck contents overridden for those
which have corresponding components that need type-checked, and the
metadata registered in the `IncrementalDriver`.
3. (Build #2 main program): The `TypeCheckShimGenerator` now reuses _all_
.ngtypecheck `ts.SourceFile` shims from build #1's type-check program in
the construction of build #2's main program. Some of the contents of
these files might be stale (if a component's template changed, for
example), but wholesale reuse here prevents unnecessary changes in the
contents of the program at this point and makes TypeScript's job a lot
easier.
4. (Build #2 type-check program): For those input files which have not
"logically changed" (meaning components within are semantically the same
as they were before), the compiler will re-use the type-check file
metadata from build #1, and _not_ generate a new .ngtypecheck shim.
For components which have logically changed or where the previous
.ngtypecheck contents cannot otherwise be reused, code generation happens
as before.
PR Close #36211
2020-03-19 14:29:58 -04:00
|
|
|
@Component({
|
|
|
|
selector: 'test-cmp',
|
|
|
|
template: '<div dir [someInput]="1"></div>',
|
|
|
|
})
|
|
|
|
export class Cmp {}
|
|
|
|
`);
|
|
|
|
env.write('module-a.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {Cmp} from './cmp';
|
|
|
|
import {ModuleB} from './module-b';
|
2020-05-19 15:08:49 -04:00
|
|
|
|
perf(compiler-cli): perform template type-checking incrementally (#36211)
This optimization builds on a lot of prior work to finally make type-
checking of templates incremental.
Incrementality requires two main components:
- the ability to reuse work from a prior compilation.
- the ability to know when changes in the current program invalidate that
prior work.
Prior to this commit, on every type-checking pass the compiler would
generate new .ngtypecheck files for each original input file in the program.
1. (Build #1 main program): empty .ngtypecheck files generated for each
original input file.
2. (Build #1 type-check program): .ngtypecheck contents overridden for those
which have corresponding components that need type-checked.
3. (Build #2 main program): throw away old .ngtypecheck files and generate
new empty ones.
4. (Build #2 type-check program): same as step 2.
With this commit, the `IncrementalDriver` now tracks template type-checking
_metadata_ for each input file. The metadata contains information about
source mappings for generated type-checking code, as well as some
diagnostics which were discovered at type-check analysis time. The actual
type-checking code is stored in the TypeScript AST for type-checking files,
which is now re-used between programs as follows:
1. (Build #1 main program): empty .ngtypecheck files generated for each
original input file.
2. (Build #1 type-check program): .ngtypecheck contents overridden for those
which have corresponding components that need type-checked, and the
metadata registered in the `IncrementalDriver`.
3. (Build #2 main program): The `TypeCheckShimGenerator` now reuses _all_
.ngtypecheck `ts.SourceFile` shims from build #1's type-check program in
the construction of build #2's main program. Some of the contents of
these files might be stale (if a component's template changed, for
example), but wholesale reuse here prevents unnecessary changes in the
contents of the program at this point and makes TypeScript's job a lot
easier.
4. (Build #2 type-check program): For those input files which have not
"logically changed" (meaning components within are semantically the same
as they were before), the compiler will re-use the type-check file
metadata from build #1, and _not_ generate a new .ngtypecheck shim.
For components which have logically changed or where the previous
.ngtypecheck contents cannot otherwise be reused, code generation happens
as before.
PR Close #36211
2020-03-19 14:29:58 -04:00
|
|
|
@NgModule({
|
|
|
|
declarations: [Cmp],
|
|
|
|
imports: [ModuleB],
|
|
|
|
})
|
|
|
|
export class ModuleA {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
// Declare ModuleB and a directive Dir, but ModuleB does not yet export Dir.
|
|
|
|
env.write('module-b.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {Dir} from './dir';
|
2020-05-19 15:08:49 -04:00
|
|
|
|
perf(compiler-cli): perform template type-checking incrementally (#36211)
This optimization builds on a lot of prior work to finally make type-
checking of templates incremental.
Incrementality requires two main components:
- the ability to reuse work from a prior compilation.
- the ability to know when changes in the current program invalidate that
prior work.
Prior to this commit, on every type-checking pass the compiler would
generate new .ngtypecheck files for each original input file in the program.
1. (Build #1 main program): empty .ngtypecheck files generated for each
original input file.
2. (Build #1 type-check program): .ngtypecheck contents overridden for those
which have corresponding components that need type-checked.
3. (Build #2 main program): throw away old .ngtypecheck files and generate
new empty ones.
4. (Build #2 type-check program): same as step 2.
With this commit, the `IncrementalDriver` now tracks template type-checking
_metadata_ for each input file. The metadata contains information about
source mappings for generated type-checking code, as well as some
diagnostics which were discovered at type-check analysis time. The actual
type-checking code is stored in the TypeScript AST for type-checking files,
which is now re-used between programs as follows:
1. (Build #1 main program): empty .ngtypecheck files generated for each
original input file.
2. (Build #1 type-check program): .ngtypecheck contents overridden for those
which have corresponding components that need type-checked, and the
metadata registered in the `IncrementalDriver`.
3. (Build #2 main program): The `TypeCheckShimGenerator` now reuses _all_
.ngtypecheck `ts.SourceFile` shims from build #1's type-check program in
the construction of build #2's main program. Some of the contents of
these files might be stale (if a component's template changed, for
example), but wholesale reuse here prevents unnecessary changes in the
contents of the program at this point and makes TypeScript's job a lot
easier.
4. (Build #2 type-check program): For those input files which have not
"logically changed" (meaning components within are semantically the same
as they were before), the compiler will re-use the type-check file
metadata from build #1, and _not_ generate a new .ngtypecheck shim.
For components which have logically changed or where the previous
.ngtypecheck contents cannot otherwise be reused, code generation happens
as before.
PR Close #36211
2020-03-19 14:29:58 -04:00
|
|
|
@NgModule({
|
|
|
|
declarations: [Dir],
|
|
|
|
})
|
|
|
|
export class ModuleB {}
|
|
|
|
`);
|
|
|
|
env.write('dir.ts', `
|
|
|
|
import {Directive, Input} from '@angular/core';
|
2020-05-19 15:08:49 -04:00
|
|
|
|
perf(compiler-cli): perform template type-checking incrementally (#36211)
This optimization builds on a lot of prior work to finally make type-
checking of templates incremental.
Incrementality requires two main components:
- the ability to reuse work from a prior compilation.
- the ability to know when changes in the current program invalidate that
prior work.
Prior to this commit, on every type-checking pass the compiler would
generate new .ngtypecheck files for each original input file in the program.
1. (Build #1 main program): empty .ngtypecheck files generated for each
original input file.
2. (Build #1 type-check program): .ngtypecheck contents overridden for those
which have corresponding components that need type-checked.
3. (Build #2 main program): throw away old .ngtypecheck files and generate
new empty ones.
4. (Build #2 type-check program): same as step 2.
With this commit, the `IncrementalDriver` now tracks template type-checking
_metadata_ for each input file. The metadata contains information about
source mappings for generated type-checking code, as well as some
diagnostics which were discovered at type-check analysis time. The actual
type-checking code is stored in the TypeScript AST for type-checking files,
which is now re-used between programs as follows:
1. (Build #1 main program): empty .ngtypecheck files generated for each
original input file.
2. (Build #1 type-check program): .ngtypecheck contents overridden for those
which have corresponding components that need type-checked, and the
metadata registered in the `IncrementalDriver`.
3. (Build #2 main program): The `TypeCheckShimGenerator` now reuses _all_
.ngtypecheck `ts.SourceFile` shims from build #1's type-check program in
the construction of build #2's main program. Some of the contents of
these files might be stale (if a component's template changed, for
example), but wholesale reuse here prevents unnecessary changes in the
contents of the program at this point and makes TypeScript's job a lot
easier.
4. (Build #2 type-check program): For those input files which have not
"logically changed" (meaning components within are semantically the same
as they were before), the compiler will re-use the type-check file
metadata from build #1, and _not_ generate a new .ngtypecheck shim.
For components which have logically changed or where the previous
.ngtypecheck contents cannot otherwise be reused, code generation happens
as before.
PR Close #36211
2020-03-19 14:29:58 -04:00
|
|
|
@Directive({selector: '[dir]'})
|
|
|
|
export class Dir {
|
|
|
|
@Input() someInput!: any;
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
|
|
|
|
let diags = env.driveDiagnostics();
|
|
|
|
// Should get a diagnostic about [dir] not being a valid binding.
|
|
|
|
expect(diags.length).toBe(1);
|
|
|
|
|
|
|
|
|
|
|
|
// As a precautionary check, run the build a second time with no changes, to ensure the
|
|
|
|
// diagnostic is repeated.
|
|
|
|
diags = env.driveDiagnostics();
|
|
|
|
// Should get a diagnostic about [dir] not being a valid binding.
|
|
|
|
expect(diags.length).toBe(1);
|
|
|
|
|
|
|
|
// Modify ModuleB to now export Dir.
|
|
|
|
env.write('module-b.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {Dir} from './dir';
|
2020-05-19 15:08:49 -04:00
|
|
|
|
perf(compiler-cli): perform template type-checking incrementally (#36211)
This optimization builds on a lot of prior work to finally make type-
checking of templates incremental.
Incrementality requires two main components:
- the ability to reuse work from a prior compilation.
- the ability to know when changes in the current program invalidate that
prior work.
Prior to this commit, on every type-checking pass the compiler would
generate new .ngtypecheck files for each original input file in the program.
1. (Build #1 main program): empty .ngtypecheck files generated for each
original input file.
2. (Build #1 type-check program): .ngtypecheck contents overridden for those
which have corresponding components that need type-checked.
3. (Build #2 main program): throw away old .ngtypecheck files and generate
new empty ones.
4. (Build #2 type-check program): same as step 2.
With this commit, the `IncrementalDriver` now tracks template type-checking
_metadata_ for each input file. The metadata contains information about
source mappings for generated type-checking code, as well as some
diagnostics which were discovered at type-check analysis time. The actual
type-checking code is stored in the TypeScript AST for type-checking files,
which is now re-used between programs as follows:
1. (Build #1 main program): empty .ngtypecheck files generated for each
original input file.
2. (Build #1 type-check program): .ngtypecheck contents overridden for those
which have corresponding components that need type-checked, and the
metadata registered in the `IncrementalDriver`.
3. (Build #2 main program): The `TypeCheckShimGenerator` now reuses _all_
.ngtypecheck `ts.SourceFile` shims from build #1's type-check program in
the construction of build #2's main program. Some of the contents of
these files might be stale (if a component's template changed, for
example), but wholesale reuse here prevents unnecessary changes in the
contents of the program at this point and makes TypeScript's job a lot
easier.
4. (Build #2 type-check program): For those input files which have not
"logically changed" (meaning components within are semantically the same
as they were before), the compiler will re-use the type-check file
metadata from build #1, and _not_ generate a new .ngtypecheck shim.
For components which have logically changed or where the previous
.ngtypecheck contents cannot otherwise be reused, code generation happens
as before.
PR Close #36211
2020-03-19 14:29:58 -04:00
|
|
|
@NgModule({
|
|
|
|
declarations: [Dir],
|
|
|
|
exports: [Dir],
|
|
|
|
})
|
|
|
|
export class ModuleB {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
diags = env.driveDiagnostics();
|
|
|
|
// Diagnostic should be gone, as the error has been fixed.
|
|
|
|
expect(diags.length).toBe(0);
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('inline operations', () => {
|
|
|
|
it('should still pick up on errors from inlined type check blocks', () => {
|
|
|
|
// In certain cases the template type-checker has to inline type checking blocks into user
|
|
|
|
// code, instead of placing it in a parallel template type-checking file. In these cases
|
|
|
|
// incremental checking cannot be used, and the type-checking code must be regenerated on
|
|
|
|
// each build. This test verifies that the above mechanism works properly, by performing
|
|
|
|
// type-checking on an unexported class (not exporting the class forces the inline
|
|
|
|
// checking de-optimization).
|
|
|
|
env.write('cmp.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
2020-05-19 15:08:49 -04:00
|
|
|
|
perf(compiler-cli): perform template type-checking incrementally (#36211)
This optimization builds on a lot of prior work to finally make type-
checking of templates incremental.
Incrementality requires two main components:
- the ability to reuse work from a prior compilation.
- the ability to know when changes in the current program invalidate that
prior work.
Prior to this commit, on every type-checking pass the compiler would
generate new .ngtypecheck files for each original input file in the program.
1. (Build #1 main program): empty .ngtypecheck files generated for each
original input file.
2. (Build #1 type-check program): .ngtypecheck contents overridden for those
which have corresponding components that need type-checked.
3. (Build #2 main program): throw away old .ngtypecheck files and generate
new empty ones.
4. (Build #2 type-check program): same as step 2.
With this commit, the `IncrementalDriver` now tracks template type-checking
_metadata_ for each input file. The metadata contains information about
source mappings for generated type-checking code, as well as some
diagnostics which were discovered at type-check analysis time. The actual
type-checking code is stored in the TypeScript AST for type-checking files,
which is now re-used between programs as follows:
1. (Build #1 main program): empty .ngtypecheck files generated for each
original input file.
2. (Build #1 type-check program): .ngtypecheck contents overridden for those
which have corresponding components that need type-checked, and the
metadata registered in the `IncrementalDriver`.
3. (Build #2 main program): The `TypeCheckShimGenerator` now reuses _all_
.ngtypecheck `ts.SourceFile` shims from build #1's type-check program in
the construction of build #2's main program. Some of the contents of
these files might be stale (if a component's template changed, for
example), but wholesale reuse here prevents unnecessary changes in the
contents of the program at this point and makes TypeScript's job a lot
easier.
4. (Build #2 type-check program): For those input files which have not
"logically changed" (meaning components within are semantically the same
as they were before), the compiler will re-use the type-check file
metadata from build #1, and _not_ generate a new .ngtypecheck shim.
For components which have logically changed or where the previous
.ngtypecheck contents cannot otherwise be reused, code generation happens
as before.
PR Close #36211
2020-03-19 14:29:58 -04:00
|
|
|
@Component({
|
|
|
|
selector: 'test-cmp',
|
|
|
|
template: '{{test}}',
|
|
|
|
})
|
|
|
|
class Cmp {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
// On the first compilation, an error should be produced.
|
|
|
|
let diags = env.driveDiagnostics();
|
|
|
|
expect(diags.length).toBe(1);
|
|
|
|
|
|
|
|
// Next, two more builds are run, one with no changes made to the file, and the other with
|
|
|
|
// changes made that should remove the error.
|
|
|
|
|
|
|
|
// The error should still be present when rebuilding.
|
|
|
|
diags = env.driveDiagnostics();
|
|
|
|
expect(diags.length).toBe(1);
|
|
|
|
|
|
|
|
// Now, correct the error by adding the 'test' property to the component.
|
|
|
|
env.write('cmp.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
2020-05-19 15:08:49 -04:00
|
|
|
|
perf(compiler-cli): perform template type-checking incrementally (#36211)
This optimization builds on a lot of prior work to finally make type-
checking of templates incremental.
Incrementality requires two main components:
- the ability to reuse work from a prior compilation.
- the ability to know when changes in the current program invalidate that
prior work.
Prior to this commit, on every type-checking pass the compiler would
generate new .ngtypecheck files for each original input file in the program.
1. (Build #1 main program): empty .ngtypecheck files generated for each
original input file.
2. (Build #1 type-check program): .ngtypecheck contents overridden for those
which have corresponding components that need type-checked.
3. (Build #2 main program): throw away old .ngtypecheck files and generate
new empty ones.
4. (Build #2 type-check program): same as step 2.
With this commit, the `IncrementalDriver` now tracks template type-checking
_metadata_ for each input file. The metadata contains information about
source mappings for generated type-checking code, as well as some
diagnostics which were discovered at type-check analysis time. The actual
type-checking code is stored in the TypeScript AST for type-checking files,
which is now re-used between programs as follows:
1. (Build #1 main program): empty .ngtypecheck files generated for each
original input file.
2. (Build #1 type-check program): .ngtypecheck contents overridden for those
which have corresponding components that need type-checked, and the
metadata registered in the `IncrementalDriver`.
3. (Build #2 main program): The `TypeCheckShimGenerator` now reuses _all_
.ngtypecheck `ts.SourceFile` shims from build #1's type-check program in
the construction of build #2's main program. Some of the contents of
these files might be stale (if a component's template changed, for
example), but wholesale reuse here prevents unnecessary changes in the
contents of the program at this point and makes TypeScript's job a lot
easier.
4. (Build #2 type-check program): For those input files which have not
"logically changed" (meaning components within are semantically the same
as they were before), the compiler will re-use the type-check file
metadata from build #1, and _not_ generate a new .ngtypecheck shim.
For components which have logically changed or where the previous
.ngtypecheck contents cannot otherwise be reused, code generation happens
as before.
PR Close #36211
2020-03-19 14:29:58 -04:00
|
|
|
@Component({
|
|
|
|
selector: 'test-cmp',
|
|
|
|
template: '{{test}}',
|
|
|
|
})
|
|
|
|
class Cmp {
|
|
|
|
test!: string;
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
// The error should be gone.
|
|
|
|
diags = env.driveDiagnostics();
|
|
|
|
expect(diags.length).toBe(0);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should still pick up on errors caused by inlined type constructors', () => {
|
|
|
|
// In certain cases the template type-checker cannot generate a type constructor for a
|
|
|
|
// directive within the template type-checking file which requires it, but must inline the
|
|
|
|
// type constructor within its original source file. In these cases, template type
|
|
|
|
// checking cannot be performed incrementally. This test verifies that such cases still
|
|
|
|
// work correctly, by repeatedly performing diagnostics on a component which depends on a
|
|
|
|
// directive with an inlined type constructor.
|
|
|
|
env.write('dir.ts', `
|
|
|
|
import {Directive, Input} from '@angular/core';
|
|
|
|
export interface Keys {
|
|
|
|
alpha: string;
|
|
|
|
beta: string;
|
|
|
|
}
|
|
|
|
@Directive({
|
|
|
|
selector: '[dir]'
|
|
|
|
})
|
|
|
|
export class Dir<T extends keyof Keys> {
|
|
|
|
// The use of 'keyof' in the generic bound causes a deopt to an inline type
|
|
|
|
// constructor.
|
|
|
|
@Input() dir: T;
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.write('cmp.ts', `
|
|
|
|
import {Component, NgModule} from '@angular/core';
|
|
|
|
import {Dir} from './dir';
|
|
|
|
@Component({
|
|
|
|
selector: 'test-cmp',
|
|
|
|
template: '<div dir="gamma"></div>',
|
|
|
|
})
|
|
|
|
export class Cmp {}
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Cmp, Dir],
|
|
|
|
})
|
|
|
|
export class Module {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
let diags = env.driveDiagnostics();
|
|
|
|
expect(diags.length).toBe(1);
|
2021-03-16 15:10:32 -04:00
|
|
|
expect(diags[0].messageText)
|
|
|
|
.toContain(`Type '"gamma"' is not assignable to type 'keyof Keys'.`);
|
perf(compiler-cli): perform template type-checking incrementally (#36211)
This optimization builds on a lot of prior work to finally make type-
checking of templates incremental.
Incrementality requires two main components:
- the ability to reuse work from a prior compilation.
- the ability to know when changes in the current program invalidate that
prior work.
Prior to this commit, on every type-checking pass the compiler would
generate new .ngtypecheck files for each original input file in the program.
1. (Build #1 main program): empty .ngtypecheck files generated for each
original input file.
2. (Build #1 type-check program): .ngtypecheck contents overridden for those
which have corresponding components that need type-checked.
3. (Build #2 main program): throw away old .ngtypecheck files and generate
new empty ones.
4. (Build #2 type-check program): same as step 2.
With this commit, the `IncrementalDriver` now tracks template type-checking
_metadata_ for each input file. The metadata contains information about
source mappings for generated type-checking code, as well as some
diagnostics which were discovered at type-check analysis time. The actual
type-checking code is stored in the TypeScript AST for type-checking files,
which is now re-used between programs as follows:
1. (Build #1 main program): empty .ngtypecheck files generated for each
original input file.
2. (Build #1 type-check program): .ngtypecheck contents overridden for those
which have corresponding components that need type-checked, and the
metadata registered in the `IncrementalDriver`.
3. (Build #2 main program): The `TypeCheckShimGenerator` now reuses _all_
.ngtypecheck `ts.SourceFile` shims from build #1's type-check program in
the construction of build #2's main program. Some of the contents of
these files might be stale (if a component's template changed, for
example), but wholesale reuse here prevents unnecessary changes in the
contents of the program at this point and makes TypeScript's job a lot
easier.
4. (Build #2 type-check program): For those input files which have not
"logically changed" (meaning components within are semantically the same
as they were before), the compiler will re-use the type-check file
metadata from build #1, and _not_ generate a new .ngtypecheck shim.
For components which have logically changed or where the previous
.ngtypecheck contents cannot otherwise be reused, code generation happens
as before.
PR Close #36211
2020-03-19 14:29:58 -04:00
|
|
|
|
|
|
|
// On a rebuild, the same errors should be present.
|
|
|
|
diags = env.driveDiagnostics();
|
|
|
|
expect(diags.length).toBe(1);
|
2021-03-16 15:10:32 -04:00
|
|
|
expect(diags[0].messageText)
|
|
|
|
.toContain(`Type '"gamma"' is not assignable to type 'keyof Keys'.`);
|
perf(compiler-cli): perform template type-checking incrementally (#36211)
This optimization builds on a lot of prior work to finally make type-
checking of templates incremental.
Incrementality requires two main components:
- the ability to reuse work from a prior compilation.
- the ability to know when changes in the current program invalidate that
prior work.
Prior to this commit, on every type-checking pass the compiler would
generate new .ngtypecheck files for each original input file in the program.
1. (Build #1 main program): empty .ngtypecheck files generated for each
original input file.
2. (Build #1 type-check program): .ngtypecheck contents overridden for those
which have corresponding components that need type-checked.
3. (Build #2 main program): throw away old .ngtypecheck files and generate
new empty ones.
4. (Build #2 type-check program): same as step 2.
With this commit, the `IncrementalDriver` now tracks template type-checking
_metadata_ for each input file. The metadata contains information about
source mappings for generated type-checking code, as well as some
diagnostics which were discovered at type-check analysis time. The actual
type-checking code is stored in the TypeScript AST for type-checking files,
which is now re-used between programs as follows:
1. (Build #1 main program): empty .ngtypecheck files generated for each
original input file.
2. (Build #1 type-check program): .ngtypecheck contents overridden for those
which have corresponding components that need type-checked, and the
metadata registered in the `IncrementalDriver`.
3. (Build #2 main program): The `TypeCheckShimGenerator` now reuses _all_
.ngtypecheck `ts.SourceFile` shims from build #1's type-check program in
the construction of build #2's main program. Some of the contents of
these files might be stale (if a component's template changed, for
example), but wholesale reuse here prevents unnecessary changes in the
contents of the program at this point and makes TypeScript's job a lot
easier.
4. (Build #2 type-check program): For those input files which have not
"logically changed" (meaning components within are semantically the same
as they were before), the compiler will re-use the type-check file
metadata from build #1, and _not_ generate a new .ngtypecheck shim.
For components which have logically changed or where the previous
.ngtypecheck contents cannot otherwise be reused, code generation happens
as before.
PR Close #36211
2020-03-19 14:29:58 -04:00
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
2019-05-08 10:10:50 -04:00
|
|
|
});
|
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
function setupFooBarProgram(env: NgtscTestEnvironment) {
|
|
|
|
env.write('foo_component.ts', `
|
2019-05-08 10:10:50 -04:00
|
|
|
import {Component} from '@angular/core';
|
|
|
|
import {fooSelector} from './foo_selector';
|
|
|
|
|
perf(compiler-cli): detect semantic changes and their effect on an incremental rebuild (#40947)
In Angular programs, changing a file may require other files to be
emitted as well due to implicit NgModule dependencies. For example, if
the selector of a directive is changed then all components that have
that directive in their compilation scope need to be recompiled, as the
change of selector may affect the directive matching results.
Until now, the compiler solved this problem using a single dependency
graph. The implicit NgModule dependencies were represented in this
graph, such that a changed file would correctly also cause other files
to be re-emitted. This approach is limited in a few ways:
1. The file dependency graph is used to determine whether it is safe to
reuse the analysis data of an Angular decorated class. This analysis
data is invariant to unrelated changes to the NgModule scope, but
because the single dependency graph also tracked the implicit
NgModule dependencies the compiler had to consider analysis data as
stale far more often than necessary.
2. It is typical for a change to e.g. a directive to not affect its
public API—its selector, inputs, outputs, or exportAs clause—in which
case there is no need to re-emit all declarations in scope, as their
compilation output wouldn't have changed.
This commit implements a mechanism by which the compiler is able to
determine the impact of a change by comparing it to the prior
compilation. To achieve this, a new graph is maintained that tracks all
public API information of all Angular decorated symbols. During an
incremental compilation this information is compared to the information
that was captured in the most recently succeeded compilation. This
determines the exact impact of the changes to the public API, which
is then used to determine which files need to be re-emitted.
Note that the file dependency graph remains, as it is still used to
track the dependencies of analysis data. This graph does no longer track
the implicit NgModule dependencies, which allows for better reuse of
analysis data.
These changes also fix a bug where template type-checking would fail to
incorporate changes made to a transitive base class of a
directive/component. This used to be a problem because transitive base
classes were not recorded as a transitive dependency in the file
dependency graph, such that prior type-check blocks would erroneously
be reused.
This commit also fixes an incorrectness where a change to a declaration
in NgModule `A` would not cause the declarations in NgModules that
import from NgModule `A` to be re-emitted. This was intentionally
incorrect as otherwise the performance of incremental rebuilds would
have been far worse. This is no longer a concern, as the compiler is now
able to only re-emit when actually necessary.
Fixes #34867
Fixes #40635
Closes #40728
PR Close #40947
2020-11-20 15:18:46 -05:00
|
|
|
@Component({
|
|
|
|
selector: fooSelector,
|
|
|
|
template: '{{ 1 | foo }}'
|
|
|
|
})
|
2019-05-08 10:10:50 -04:00
|
|
|
export class FooCmp {}
|
|
|
|
`);
|
2019-06-06 15:22:32 -04:00
|
|
|
env.write('foo_pipe.ts', `
|
2019-05-08 10:10:50 -04:00
|
|
|
import {Pipe} from '@angular/core';
|
|
|
|
|
|
|
|
@Pipe({name: 'foo'})
|
2021-01-22 12:03:37 -05:00
|
|
|
export class FooPipe {
|
|
|
|
transform() {}
|
|
|
|
}
|
2019-05-08 10:10:50 -04:00
|
|
|
`);
|
2019-06-06 15:22:32 -04:00
|
|
|
env.write('foo_module.ts', `
|
2019-05-08 10:10:50 -04:00
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {FooCmp} from './foo_component';
|
|
|
|
import {FooPipe} from './foo_pipe';
|
|
|
|
import {BarModule} from './bar_module';
|
|
|
|
@NgModule({
|
|
|
|
declarations: [FooCmp, FooPipe],
|
|
|
|
imports: [BarModule],
|
|
|
|
})
|
|
|
|
export class FooModule {}
|
|
|
|
`);
|
2019-06-06 15:22:32 -04:00
|
|
|
env.write('bar_component.ts', `
|
2019-05-08 10:10:50 -04:00
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
2019-07-31 12:11:33 -04:00
|
|
|
@Component({selector: 'bar', templateUrl: './bar_component.html'})
|
2019-05-08 10:10:50 -04:00
|
|
|
export class BarCmp {}
|
|
|
|
`);
|
2019-07-31 12:11:33 -04:00
|
|
|
env.write('bar_component.html', '<div bar></div>');
|
2019-06-06 15:22:32 -04:00
|
|
|
env.write('bar_directive.ts', `
|
2019-05-08 10:10:50 -04:00
|
|
|
import {Directive} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({selector: '[bar]'})
|
|
|
|
export class BarDir {}
|
perf(compiler-cli): detect semantic changes and their effect on an incremental rebuild (#40947)
In Angular programs, changing a file may require other files to be
emitted as well due to implicit NgModule dependencies. For example, if
the selector of a directive is changed then all components that have
that directive in their compilation scope need to be recompiled, as the
change of selector may affect the directive matching results.
Until now, the compiler solved this problem using a single dependency
graph. The implicit NgModule dependencies were represented in this
graph, such that a changed file would correctly also cause other files
to be re-emitted. This approach is limited in a few ways:
1. The file dependency graph is used to determine whether it is safe to
reuse the analysis data of an Angular decorated class. This analysis
data is invariant to unrelated changes to the NgModule scope, but
because the single dependency graph also tracked the implicit
NgModule dependencies the compiler had to consider analysis data as
stale far more often than necessary.
2. It is typical for a change to e.g. a directive to not affect its
public API—its selector, inputs, outputs, or exportAs clause—in which
case there is no need to re-emit all declarations in scope, as their
compilation output wouldn't have changed.
This commit implements a mechanism by which the compiler is able to
determine the impact of a change by comparing it to the prior
compilation. To achieve this, a new graph is maintained that tracks all
public API information of all Angular decorated symbols. During an
incremental compilation this information is compared to the information
that was captured in the most recently succeeded compilation. This
determines the exact impact of the changes to the public API, which
is then used to determine which files need to be re-emitted.
Note that the file dependency graph remains, as it is still used to
track the dependencies of analysis data. This graph does no longer track
the implicit NgModule dependencies, which allows for better reuse of
analysis data.
These changes also fix a bug where template type-checking would fail to
incorporate changes made to a transitive base class of a
directive/component. This used to be a problem because transitive base
classes were not recorded as a transitive dependency in the file
dependency graph, such that prior type-check blocks would erroneously
be reused.
This commit also fixes an incorrectness where a change to a declaration
in NgModule `A` would not cause the declarations in NgModules that
import from NgModule `A` to be re-emitted. This was intentionally
incorrect as otherwise the performance of incremental rebuilds would
have been far worse. This is no longer a concern, as the compiler is now
able to only re-emit when actually necessary.
Fixes #34867
Fixes #40635
Closes #40728
PR Close #40947
2020-11-20 15:18:46 -05:00
|
|
|
`);
|
|
|
|
env.write('bar_pipe.ts', `
|
|
|
|
import {Pipe} from '@angular/core';
|
|
|
|
|
|
|
|
@Pipe({name: 'foo'})
|
2021-01-22 12:03:37 -05:00
|
|
|
export class BarPipe {
|
|
|
|
transform() {}
|
|
|
|
}
|
2019-05-08 10:10:50 -04:00
|
|
|
`);
|
2019-06-06 15:22:32 -04:00
|
|
|
env.write('bar_module.ts', `
|
2019-05-08 10:10:50 -04:00
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {BarCmp} from './bar_component';
|
|
|
|
import {BarDir} from './bar_directive';
|
perf(compiler-cli): detect semantic changes and their effect on an incremental rebuild (#40947)
In Angular programs, changing a file may require other files to be
emitted as well due to implicit NgModule dependencies. For example, if
the selector of a directive is changed then all components that have
that directive in their compilation scope need to be recompiled, as the
change of selector may affect the directive matching results.
Until now, the compiler solved this problem using a single dependency
graph. The implicit NgModule dependencies were represented in this
graph, such that a changed file would correctly also cause other files
to be re-emitted. This approach is limited in a few ways:
1. The file dependency graph is used to determine whether it is safe to
reuse the analysis data of an Angular decorated class. This analysis
data is invariant to unrelated changes to the NgModule scope, but
because the single dependency graph also tracked the implicit
NgModule dependencies the compiler had to consider analysis data as
stale far more often than necessary.
2. It is typical for a change to e.g. a directive to not affect its
public API—its selector, inputs, outputs, or exportAs clause—in which
case there is no need to re-emit all declarations in scope, as their
compilation output wouldn't have changed.
This commit implements a mechanism by which the compiler is able to
determine the impact of a change by comparing it to the prior
compilation. To achieve this, a new graph is maintained that tracks all
public API information of all Angular decorated symbols. During an
incremental compilation this information is compared to the information
that was captured in the most recently succeeded compilation. This
determines the exact impact of the changes to the public API, which
is then used to determine which files need to be re-emitted.
Note that the file dependency graph remains, as it is still used to
track the dependencies of analysis data. This graph does no longer track
the implicit NgModule dependencies, which allows for better reuse of
analysis data.
These changes also fix a bug where template type-checking would fail to
incorporate changes made to a transitive base class of a
directive/component. This used to be a problem because transitive base
classes were not recorded as a transitive dependency in the file
dependency graph, such that prior type-check blocks would erroneously
be reused.
This commit also fixes an incorrectness where a change to a declaration
in NgModule `A` would not cause the declarations in NgModules that
import from NgModule `A` to be re-emitted. This was intentionally
incorrect as otherwise the performance of incremental rebuilds would
have been far worse. This is no longer a concern, as the compiler is now
able to only re-emit when actually necessary.
Fixes #34867
Fixes #40635
Closes #40728
PR Close #40947
2020-11-20 15:18:46 -05:00
|
|
|
import {BarPipe} from './bar_pipe';
|
2019-05-08 10:10:50 -04:00
|
|
|
@NgModule({
|
perf(compiler-cli): detect semantic changes and their effect on an incremental rebuild (#40947)
In Angular programs, changing a file may require other files to be
emitted as well due to implicit NgModule dependencies. For example, if
the selector of a directive is changed then all components that have
that directive in their compilation scope need to be recompiled, as the
change of selector may affect the directive matching results.
Until now, the compiler solved this problem using a single dependency
graph. The implicit NgModule dependencies were represented in this
graph, such that a changed file would correctly also cause other files
to be re-emitted. This approach is limited in a few ways:
1. The file dependency graph is used to determine whether it is safe to
reuse the analysis data of an Angular decorated class. This analysis
data is invariant to unrelated changes to the NgModule scope, but
because the single dependency graph also tracked the implicit
NgModule dependencies the compiler had to consider analysis data as
stale far more often than necessary.
2. It is typical for a change to e.g. a directive to not affect its
public API—its selector, inputs, outputs, or exportAs clause—in which
case there is no need to re-emit all declarations in scope, as their
compilation output wouldn't have changed.
This commit implements a mechanism by which the compiler is able to
determine the impact of a change by comparing it to the prior
compilation. To achieve this, a new graph is maintained that tracks all
public API information of all Angular decorated symbols. During an
incremental compilation this information is compared to the information
that was captured in the most recently succeeded compilation. This
determines the exact impact of the changes to the public API, which
is then used to determine which files need to be re-emitted.
Note that the file dependency graph remains, as it is still used to
track the dependencies of analysis data. This graph does no longer track
the implicit NgModule dependencies, which allows for better reuse of
analysis data.
These changes also fix a bug where template type-checking would fail to
incorporate changes made to a transitive base class of a
directive/component. This used to be a problem because transitive base
classes were not recorded as a transitive dependency in the file
dependency graph, such that prior type-check blocks would erroneously
be reused.
This commit also fixes an incorrectness where a change to a declaration
in NgModule `A` would not cause the declarations in NgModules that
import from NgModule `A` to be re-emitted. This was intentionally
incorrect as otherwise the performance of incremental rebuilds would
have been far worse. This is no longer a concern, as the compiler is now
able to only re-emit when actually necessary.
Fixes #34867
Fixes #40635
Closes #40728
PR Close #40947
2020-11-20 15:18:46 -05:00
|
|
|
declarations: [BarCmp, BarDir, BarPipe],
|
|
|
|
exports: [BarCmp, BarPipe],
|
2019-05-08 10:10:50 -04:00
|
|
|
})
|
|
|
|
export class BarModule {}
|
|
|
|
`);
|
2019-06-06 15:22:32 -04:00
|
|
|
env.write('foo_selector.d.ts', `
|
2019-05-08 10:10:50 -04:00
|
|
|
export const fooSelector = 'foo';
|
|
|
|
`);
|
2019-06-06 15:22:32 -04:00
|
|
|
env.driveMain();
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
}
|
|
|
|
});
|