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
|
|
|
/**
|
|
|
|
* @license
|
|
|
|
* Copyright Google LLC All Rights Reserved.
|
|
|
|
*
|
|
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
|
|
* found in the LICENSE file at https://angular.io/license
|
|
|
|
*/
|
|
|
|
|
|
|
|
import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing';
|
|
|
|
import {loadStandardTestFiles} from '../../src/ngtsc/testing';
|
|
|
|
|
|
|
|
import {NgtscTestEnvironment} from './env';
|
|
|
|
|
|
|
|
const testFiles = loadStandardTestFiles();
|
|
|
|
|
|
|
|
runInEachFileSystem(() => {
|
|
|
|
describe('ngtsc incremental compilation (semantic changes)', () => {
|
|
|
|
let env!: NgtscTestEnvironment;
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
env = NgtscTestEnvironment.setup(testFiles);
|
|
|
|
env.enableMultipleCompilations();
|
|
|
|
env.tsconfig();
|
|
|
|
});
|
|
|
|
|
|
|
|
function expectToHaveWritten(files: string[]): void {
|
|
|
|
const set = env.getFilesWrittenSinceLastFlush();
|
|
|
|
|
|
|
|
const expectedSet = new Set<string>();
|
|
|
|
for (const file of files) {
|
|
|
|
expectedSet.add(file);
|
|
|
|
expectedSet.add(file.replace(/\.js$/, '.d.ts'));
|
|
|
|
}
|
|
|
|
|
|
|
|
expect(set).toEqual(expectedSet);
|
|
|
|
|
|
|
|
// Reset for the next compilation.
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
}
|
|
|
|
|
|
|
|
describe('changes to public api', () => {
|
|
|
|
it('should not recompile dependent components when public api is unchanged', () => {
|
|
|
|
// Testing setup: ADep is a component with an input and an output, and is consumed by two
|
|
|
|
// other components - ACmp within its same NgModule, and BCmp which depends on ADep via an
|
|
|
|
// NgModule import.
|
|
|
|
//
|
|
|
|
// ADep is changed during the test without affecting its public API, and the test asserts
|
|
|
|
// that both ACmp and BCmp which consume ADep are not re-emitted.
|
|
|
|
env.write('a/dep.ts', `
|
|
|
|
import {Component, Input, Output, EventEmitter} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'a-dep',
|
|
|
|
template: 'a-dep',
|
|
|
|
})
|
|
|
|
export class ADep {
|
|
|
|
@Input()
|
|
|
|
input!: string;
|
|
|
|
|
|
|
|
@Output()
|
|
|
|
output = new EventEmitter<string>();
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
env.write('a/cmp.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'a-cmp',
|
|
|
|
template: '<a-dep></a-dep>',
|
|
|
|
})
|
|
|
|
export class ACmp {}
|
|
|
|
`);
|
|
|
|
env.write('a/mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {ADep} from './dep';
|
|
|
|
import {ACmp} from './cmp';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [ADep, ACmp],
|
|
|
|
exports: [ADep],
|
|
|
|
})
|
|
|
|
export class AMod {}
|
|
|
|
`);
|
|
|
|
env.write('b/cmp.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'b-cmp',
|
|
|
|
template: '<a-dep></a-dep>',
|
|
|
|
})
|
|
|
|
export class BCmp {}
|
|
|
|
`);
|
|
|
|
env.write('b/mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {BCmp} from './cmp';
|
|
|
|
import {AMod} from '../a/mod';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [BCmp],
|
|
|
|
imports: [AMod],
|
|
|
|
})
|
|
|
|
export class BMod {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
|
|
|
|
// Change ADep without affecting its public API.
|
|
|
|
env.write('a/dep.ts', `
|
|
|
|
import {Component, Input, Output, EventEmitter} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'a-dep',
|
|
|
|
template: 'a-dep',
|
|
|
|
})
|
|
|
|
export class ADep {
|
|
|
|
@Input()
|
|
|
|
input!: string;
|
|
|
|
|
|
|
|
@Output()
|
|
|
|
output = new EventEmitter<number>(); // changed from string to number
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
expectToHaveWritten([
|
|
|
|
// ADep is written because it was updated.
|
|
|
|
'/a/dep.js',
|
|
|
|
|
|
|
|
// AMod is written because it has a direct dependency on ADep.
|
|
|
|
'/a/mod.js',
|
|
|
|
|
|
|
|
// Nothing else is written because the public API of AppCmpB was not affected
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should not recompile components that do not use a changed directive', () => {
|
|
|
|
// Testing setup: ADep is a directive with an input and output, which is visible to two
|
|
|
|
// components which do not use ADep in their templates - ACmp within the same NgModule, and
|
|
|
|
// BCmp which has visibility of ADep via an NgModule import.
|
|
|
|
//
|
|
|
|
// During the test, ADep's public API is changed, and the test verifies that neither ACmp
|
|
|
|
// nor BCmp are re-emitted.
|
|
|
|
|
|
|
|
env.write('a/dep.ts', `
|
|
|
|
import {Directive, Input, Output, EventEmitter} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({
|
|
|
|
selector: '[a-dep]',
|
|
|
|
})
|
|
|
|
export class ADep {
|
|
|
|
@Input()
|
|
|
|
input!: string;
|
|
|
|
|
|
|
|
@Output()
|
|
|
|
output = new EventEmitter<string>();
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
env.write('a/cmp.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'a-cmp',
|
|
|
|
template: 'Does not use a-dep.',
|
|
|
|
})
|
|
|
|
export class ACmp {}
|
|
|
|
`);
|
|
|
|
env.write('a/mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {ADep} from './dep';
|
|
|
|
import {ACmp} from './cmp';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [ADep, ACmp],
|
|
|
|
exports: [ADep],
|
|
|
|
})
|
|
|
|
export class AMod {}
|
|
|
|
`);
|
|
|
|
env.write('b/cmp.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'b-cmp',
|
|
|
|
template: 'Does not use a-dep.',
|
|
|
|
})
|
|
|
|
export class BCmp {}
|
|
|
|
`);
|
|
|
|
env.write('b/mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {BCmp} from './cmp';
|
|
|
|
import {AMod} from '../a/mod';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [BCmp],
|
|
|
|
imports: [AMod],
|
|
|
|
})
|
|
|
|
export class BMod {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
|
|
|
|
// Update ADep and change its public API.
|
|
|
|
env.write('a/dep.ts', `
|
|
|
|
import {Directive, Input, Output, EventEmitter} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({
|
|
|
|
selector: '[a-dep]',
|
|
|
|
template: 'a-dep',
|
|
|
|
})
|
|
|
|
export class ADep {
|
|
|
|
@Input()
|
|
|
|
input!: string;
|
|
|
|
|
|
|
|
@Output('output-renamed') // public binding name of the @Output is changed.
|
|
|
|
output = new EventEmitter<string>();
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
expectToHaveWritten([
|
|
|
|
// ADep is written because it was updated.
|
|
|
|
'/a/dep.js',
|
|
|
|
|
|
|
|
// AMod is written because it has a direct dependency on ADep.
|
|
|
|
'/a/mod.js',
|
|
|
|
|
|
|
|
// Nothing else is written because neither ACmp nor BCmp depend on ADep.
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should recompile components for which a directive usage is introduced', () => {
|
|
|
|
// Testing setup: Cmp is a component with a template that would match a directive with the
|
|
|
|
// selector '[dep]' if one existed. Dep is a directive with a different selector initially.
|
|
|
|
//
|
|
|
|
// During the test, Dep's selector is updated to '[dep]', causing it to begin matching the
|
|
|
|
// template of Cmp. The test verifies that Cmp is re-emitted after this change.
|
|
|
|
env.write('dep.ts', `
|
|
|
|
import {Directive} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({
|
|
|
|
selector: '[does-not-match]',
|
|
|
|
})
|
|
|
|
export class Dep {}
|
|
|
|
`);
|
|
|
|
env.write('cmp.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'cmp',
|
|
|
|
template: '<div dep></div>',
|
|
|
|
})
|
|
|
|
export class Cmp {}
|
|
|
|
`);
|
|
|
|
env.write('mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {Cmp} from './cmp';
|
|
|
|
import {Dep} from './dep';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Cmp, Dep],
|
|
|
|
})
|
|
|
|
export class Mod {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
|
|
|
|
env.write('dep.ts', `
|
|
|
|
import {Directive} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({
|
|
|
|
selector: '[dep]', // selector changed to now match inside Cmp's template
|
|
|
|
})
|
|
|
|
export class Dep {}
|
|
|
|
`);
|
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
expectToHaveWritten([
|
|
|
|
// Dep is written because it was directly updated.
|
|
|
|
'/dep.js',
|
|
|
|
|
|
|
|
// Mod is written because it has a direct dependency on Dep.
|
|
|
|
'/mod.js',
|
|
|
|
|
|
|
|
// Cmp is written because the directives matched in its template have changed.
|
|
|
|
'/cmp.js',
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should recompile components for which a directive usage is removed', () => {
|
|
|
|
// Testing setup: Cmp is a component with a template that matches a directive Dep with the
|
|
|
|
// initial selector '[dep]'.
|
|
|
|
//
|
|
|
|
// During the test, Dep's selector is changed, causing it to no longer match the template of
|
|
|
|
// Cmp. The test verifies that Cmp is re-emitted after this change.
|
|
|
|
|
|
|
|
env.write('dep.ts', `
|
|
|
|
import {Directive} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({
|
|
|
|
selector: '[dep]',
|
|
|
|
})
|
|
|
|
export class Dep {}
|
|
|
|
`);
|
|
|
|
env.write('cmp.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'cmp',
|
|
|
|
template: '<div dep></div>',
|
|
|
|
})
|
|
|
|
export class Cmp {}
|
|
|
|
`);
|
|
|
|
env.write('mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {Cmp} from './cmp';
|
|
|
|
import {Dep} from './dep';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Cmp, Dep],
|
|
|
|
})
|
|
|
|
export class Mod {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
|
|
|
|
env.write('dep.ts', `
|
|
|
|
import {Directive} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({
|
|
|
|
selector: '[does-not-match]', // selector changed to no longer match Cmp's template
|
|
|
|
})
|
|
|
|
export class Dep {}
|
|
|
|
`);
|
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
expectToHaveWritten([
|
|
|
|
// Dep is written because it was directly updated.
|
|
|
|
'/dep.js',
|
|
|
|
|
|
|
|
// Mod is written because it has a direct dependency on Dep.
|
|
|
|
'/mod.js',
|
|
|
|
|
|
|
|
// Cmp is written because the directives matched in its template have changed.
|
|
|
|
'/cmp.js',
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should recompile dependent components when an input is added', () => {
|
|
|
|
// Testing setup: Cmp is a component with a template that matches a directive Dep with the
|
|
|
|
// initial selector '[dep]'.
|
|
|
|
//
|
|
|
|
// During the test, an input is added to Dep, and the test verifies that Cmp is re-emitted.
|
|
|
|
|
|
|
|
env.write('dep.ts', `
|
|
|
|
import {Directive} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({
|
|
|
|
selector: '[dep]',
|
|
|
|
})
|
|
|
|
export class Dep {}
|
|
|
|
`);
|
|
|
|
env.write('cmp.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'cmp',
|
|
|
|
template: '<div dep></div>',
|
|
|
|
})
|
|
|
|
export class Cmp {}
|
|
|
|
`);
|
|
|
|
env.write('mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {Cmp} from './cmp';
|
|
|
|
import {Dep} from './dep';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Cmp, Dep],
|
|
|
|
})
|
|
|
|
export class Mod {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
|
|
|
|
env.write('dep.ts', `
|
|
|
|
import {Directive, Input} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({
|
|
|
|
selector: '[dep]',
|
|
|
|
})
|
|
|
|
export class Dep {
|
|
|
|
@Input() input!: string; // adding this changes Dep's public API
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
expectToHaveWritten([
|
|
|
|
// Dep is written because it was directly updated.
|
|
|
|
'/dep.js',
|
|
|
|
|
|
|
|
// Mod is written because it has a direct dependency on Dep.
|
|
|
|
'/mod.js',
|
|
|
|
|
|
|
|
// Cmp is written because it depends on Dep, which has changed in its public API.
|
|
|
|
'/cmp.js',
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should recompile dependent components when an input is renamed', () => {
|
|
|
|
// Testing setup: Cmp is a component with a template that matches a directive Dep with the
|
|
|
|
// initial selector '[dep]'.
|
|
|
|
//
|
|
|
|
// During the test, an input of Dep is renamed, and the test verifies that Cmp is
|
|
|
|
// re-emitted.
|
|
|
|
|
|
|
|
env.write('dep.ts', `
|
|
|
|
import {Directive, Input} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({
|
|
|
|
selector: '[dep]',
|
|
|
|
})
|
|
|
|
export class Dep {
|
|
|
|
@Input() input!: string;
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
env.write('cmp.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'cmp',
|
|
|
|
template: '<div dep></div>',
|
|
|
|
})
|
|
|
|
export class Cmp {}
|
|
|
|
`);
|
|
|
|
env.write('mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {Cmp} from './cmp';
|
|
|
|
import {Dep} from './dep';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Cmp, Dep],
|
|
|
|
})
|
|
|
|
export class Mod {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
|
|
|
|
env.write('dep.ts', `
|
|
|
|
import {Directive, Input} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({
|
|
|
|
selector: '[dep]',
|
|
|
|
})
|
|
|
|
export class Dep {
|
|
|
|
@Input('renamed') input!: string; // renaming this changes Dep's public API
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
expectToHaveWritten([
|
|
|
|
// Dep is written because it was directly updated.
|
|
|
|
'/dep.js',
|
|
|
|
|
|
|
|
// Mod is written because it has a direct dependency on Dep.
|
|
|
|
'/mod.js',
|
|
|
|
|
|
|
|
// Cmp is written because it depends on Dep, which has changed in its public API.
|
|
|
|
'/cmp.js',
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should recompile dependent components when an input is removed', () => {
|
|
|
|
// Testing setup: Cmp is a component with a template that matches a directive Dep with the
|
|
|
|
// initial selector '[dep]'.
|
|
|
|
//
|
|
|
|
// During the test, an input of Dep is removed, and the test verifies that Cmp is
|
|
|
|
// re-emitted.
|
|
|
|
|
|
|
|
env.write('dep.ts', `
|
|
|
|
import {Directive, Input} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({
|
|
|
|
selector: '[dep]',
|
|
|
|
})
|
|
|
|
export class Dep {
|
|
|
|
@Input() input!: string;
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
env.write('cmp.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'cmp',
|
|
|
|
template: '<div dep></div>',
|
|
|
|
})
|
|
|
|
export class Cmp {}
|
|
|
|
`);
|
|
|
|
env.write('mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {Cmp} from './cmp';
|
|
|
|
import {Dep} from './dep';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Cmp, Dep],
|
|
|
|
})
|
|
|
|
export class Mod {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
|
|
|
|
env.write('dep.ts', `
|
|
|
|
import {Directive} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({
|
|
|
|
selector: '[dep]',
|
|
|
|
})
|
|
|
|
export class Dep {
|
|
|
|
// Dep's input has been removed, which changes its public API
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
expectToHaveWritten([
|
|
|
|
// Dep is written because it was directly updated.
|
|
|
|
'/dep.js',
|
|
|
|
|
|
|
|
// Mod is written because it has a direct dependency on Dep.
|
|
|
|
'/mod.js',
|
|
|
|
|
|
|
|
// Cmp is written because it depends on Dep, which has changed in its public API.
|
|
|
|
'/cmp.js',
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should recompile dependent components when an output is added', () => {
|
|
|
|
// Testing setup: Cmp is a component with a template that matches a directive Dep with the
|
|
|
|
// initial selector '[dep]'.
|
|
|
|
//
|
|
|
|
// During the test, an output of Dep is added, and the test verifies that Cmp is re-emitted.
|
|
|
|
|
|
|
|
env.write('dep.ts', `
|
|
|
|
import {Directive} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({
|
|
|
|
selector: '[dep]',
|
|
|
|
})
|
|
|
|
export class Dep {}
|
|
|
|
`);
|
|
|
|
env.write('cmp.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'cmp',
|
|
|
|
template: '<div dep></div>',
|
|
|
|
})
|
|
|
|
export class Cmp {}
|
|
|
|
`);
|
|
|
|
env.write('mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {Cmp} from './cmp';
|
|
|
|
import {Dep} from './dep';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Cmp, Dep],
|
|
|
|
})
|
|
|
|
export class Mod {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
|
|
|
|
env.write('dep.ts', `
|
|
|
|
import {Directive, Output, EventEmitter} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({
|
|
|
|
selector: '[dep]',
|
|
|
|
})
|
|
|
|
export class Dep {
|
|
|
|
@Output()
|
|
|
|
output = new EventEmitter<string>(); // added, which changes Dep's public API
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
expectToHaveWritten([
|
|
|
|
// Dep is written because it was directly updated.
|
|
|
|
'/dep.js',
|
|
|
|
|
|
|
|
// Mod is written because it has a direct dependency on Dep.
|
|
|
|
'/mod.js',
|
|
|
|
|
|
|
|
// Cmp is written because it depends on Dep, which has changed in its public API.
|
|
|
|
'/cmp.js',
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should recompile dependent components when an output is renamed', () => {
|
|
|
|
// Testing setup: Cmp is a component with a template that matches a directive Dep with the
|
|
|
|
// initial selector '[dep]'.
|
|
|
|
//
|
|
|
|
// During the test, an output of Dep is renamed, and the test verifies that Cmp is
|
|
|
|
// re-emitted.
|
|
|
|
|
|
|
|
env.write('dep.ts', `
|
|
|
|
import {Directive, Output, EventEmitter} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({
|
|
|
|
selector: '[dep]',
|
|
|
|
})
|
|
|
|
export class Dep {
|
|
|
|
@Output() output = new EventEmitter<string>();
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
env.write('cmp.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'cmp',
|
|
|
|
template: '<div dep></div>',
|
|
|
|
})
|
|
|
|
export class Cmp {}
|
|
|
|
`);
|
|
|
|
env.write('mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {Cmp} from './cmp';
|
|
|
|
import {Dep} from './dep';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Cmp, Dep],
|
|
|
|
})
|
|
|
|
export class Mod {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
|
|
|
|
env.write('dep.ts', `
|
|
|
|
import {Directive, Output, EventEmitter} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({
|
|
|
|
selector: '[dep]',
|
|
|
|
})
|
|
|
|
export class Dep {
|
|
|
|
@Output('renamed') output = new EventEmitter<string>(); // public API changed
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
expectToHaveWritten([
|
|
|
|
// Dep is written because it was directly updated.
|
|
|
|
'/dep.js',
|
|
|
|
|
|
|
|
// Mod is written because it has a direct dependency on Dep.
|
|
|
|
'/mod.js',
|
|
|
|
|
|
|
|
// Cmp is written because it depends on Dep, which has changed in its public API.
|
|
|
|
'/cmp.js',
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should recompile dependent components when an output is removed', () => {
|
|
|
|
// Testing setup: Cmp is a component with a template that matches a directive Dep with the
|
|
|
|
// initial selector '[dep]'.
|
|
|
|
//
|
|
|
|
// During the test, an output of Dep is removed, and the test verifies that Cmp is
|
|
|
|
// re-emitted.
|
|
|
|
|
|
|
|
env.write('dep.ts', `
|
|
|
|
import {Directive, Output, EventEmitter} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({
|
|
|
|
selector: '[dep]',
|
|
|
|
})
|
|
|
|
export class Dep {
|
|
|
|
@Output() output = new EventEmitter<string>();
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
env.write('cmp.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'cmp',
|
|
|
|
template: '<div dep></div>',
|
|
|
|
})
|
|
|
|
export class Cmp {}
|
|
|
|
`);
|
|
|
|
env.write('mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {Cmp} from './cmp';
|
|
|
|
import {Dep} from './dep';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Cmp, Dep],
|
|
|
|
})
|
|
|
|
export class Mod {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
|
|
|
|
env.write('dep.ts', `
|
|
|
|
import {Directive} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({
|
|
|
|
selector: '[dep]',
|
|
|
|
})
|
|
|
|
export class Dep {
|
|
|
|
// Dep's output has been removed, which changes its public API
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
expectToHaveWritten([
|
|
|
|
// Dep is written because it was directly updated.
|
|
|
|
'/dep.js',
|
|
|
|
|
|
|
|
// Mod is written because it has a direct dependency on Dep.
|
|
|
|
'/mod.js',
|
|
|
|
|
|
|
|
// Cmp is written because it depends on Dep, which has changed in its public API.
|
|
|
|
'/cmp.js',
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should recompile dependent components when exportAs clause changes', () => {
|
|
|
|
// Testing setup: Cmp is a component with a template that matches a directive Dep with the
|
|
|
|
// initial selector '[dep]' and an exportAs clause.
|
|
|
|
//
|
|
|
|
// During the test, the exportAs clause of Dep is changed, and the test verifies that Cmp is
|
|
|
|
// re-emitted.
|
|
|
|
|
|
|
|
env.write('dep.ts', `
|
|
|
|
import {Directive} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({
|
|
|
|
selector: '[dep]',
|
|
|
|
exportAs: 'depExport1',
|
|
|
|
})
|
|
|
|
export class Dep {}
|
|
|
|
`);
|
|
|
|
env.write('cmp.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'cmp',
|
|
|
|
template: '<div dep></div>',
|
|
|
|
})
|
|
|
|
export class Cmp {}
|
|
|
|
`);
|
|
|
|
env.write('mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {Cmp} from './cmp';
|
|
|
|
import {Dep} from './dep';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Cmp, Dep],
|
|
|
|
})
|
|
|
|
export class Mod {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
|
|
|
|
env.write('dep.ts', `
|
|
|
|
import {Directive} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({
|
|
|
|
selector: '[dep]',
|
|
|
|
exportAs: 'depExport2', // changing this changes Dep's public API
|
|
|
|
})
|
|
|
|
export class Dep {}
|
|
|
|
`);
|
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
expectToHaveWritten([
|
|
|
|
// Dep is written because it was directly updated.
|
|
|
|
'/dep.js',
|
|
|
|
|
|
|
|
// Mod is written because it has a direct dependency on Dep.
|
|
|
|
'/mod.js',
|
|
|
|
|
|
|
|
// Cmp is written because it depends on Dep, which has changed in its public API.
|
|
|
|
'/cmp.js',
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should recompile components when a pipe is newly matched because it was renamed', () => {
|
|
|
|
// Testing setup: Cmp uses two pipes (PipeA and PipeB) in its template.
|
|
|
|
//
|
|
|
|
// During the test, the selectors of these pipes are swapped. This ensures that Cmp's
|
|
|
|
// template is still valid, since both pipe names continue to be valid within it. However,
|
|
|
|
// as the identity of each pipe is now different, the effective public API of those pipe
|
|
|
|
// usages has changed. The test then verifies that Cmp is re-emitted.
|
|
|
|
|
|
|
|
env.write('pipes.ts', `
|
|
|
|
import {Pipe} from '@angular/core';
|
|
|
|
|
|
|
|
@Pipe({
|
|
|
|
name: 'pipeA',
|
|
|
|
})
|
|
|
|
export class PipeA {
|
|
|
|
transform(value: any): any { return value; }
|
|
|
|
}
|
|
|
|
|
|
|
|
@Pipe({
|
|
|
|
name: 'pipeB',
|
|
|
|
})
|
|
|
|
export class PipeB {
|
|
|
|
transform(value: any): any { return value; }
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
env.write('cmp.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'cmp',
|
|
|
|
template: '{{ value | pipeA }} {{ value | pipeB }}',
|
|
|
|
})
|
|
|
|
export class Cmp {
|
|
|
|
value!: string;
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
env.write('mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {PipeA, PipeB} from './pipes';
|
|
|
|
import {Cmp} from './cmp';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Cmp, PipeA, PipeB],
|
|
|
|
})
|
|
|
|
export class Mod {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
|
|
|
|
env.write('pipes.ts', `
|
|
|
|
import {Pipe} from '@angular/core';
|
|
|
|
|
|
|
|
@Pipe({
|
|
|
|
name: 'pipeB', // swapped with PipeB's selector
|
|
|
|
})
|
|
|
|
export class PipeA {
|
|
|
|
transform(value: any): any { return value; }
|
|
|
|
}
|
|
|
|
|
|
|
|
@Pipe({
|
|
|
|
name: 'pipeA', // swapped with PipeA's selector
|
|
|
|
})
|
|
|
|
export class PipeB {
|
|
|
|
transform(value: any): any { return value; }
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
expectToHaveWritten([
|
|
|
|
// PipeA and PipeB have directly changed.
|
|
|
|
'/pipes.js',
|
|
|
|
|
|
|
|
// Mod depends directly on PipeA and PipeB.
|
|
|
|
'/mod.js',
|
|
|
|
|
|
|
|
// Cmp depends on the public APIs of PipeA and PipeB, which have changed (as they've
|
|
|
|
// swapped).
|
|
|
|
'/cmp.js',
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('external declarations', () => {
|
|
|
|
it('should not recompile components that use external declarations that are not changed',
|
|
|
|
() => {
|
|
|
|
// Testing setup: Two components (MyCmpA and MyCmpB) both depend on an external directive
|
|
|
|
// which matches their templates, via an NgModule import.
|
|
|
|
//
|
|
|
|
// During the test, MyCmpA is invalidated, and the test verifies that only MyCmpA and not
|
|
|
|
// MyCmpB is re-emitted.
|
|
|
|
env.write('node_modules/external/index.d.ts', `
|
|
|
|
import * as ng from '@angular/core';
|
|
|
|
|
|
|
|
export declare class ExternalDir {
|
|
|
|
static ɵdir: ng.ɵɵDirectiveDefWithMeta<ExternalDir, "[external]", never, {}, {}, never>;
|
|
|
|
}
|
|
|
|
|
|
|
|
export declare class ExternalMod {
|
|
|
|
static ɵmod: ng.ɵɵNgModuleDefWithMeta<ExternalMod, [typeof ExternalDir], never, [typeof ExternalDir]>;
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
env.write('cmp-a.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
template: '<div external></div>',
|
|
|
|
})
|
|
|
|
export class MyCmpA {}
|
|
|
|
`);
|
|
|
|
env.write('cmp-b.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
template: '<div external></div>',
|
|
|
|
})
|
|
|
|
export class MyCmpB {}
|
|
|
|
`);
|
|
|
|
env.write('mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {ExternalMod} from 'external';
|
|
|
|
import {MyCmpA} from './cmp-a';
|
|
|
|
import {MyCmpB} from './cmp-b';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [MyCmpA, MyCmpB],
|
|
|
|
imports: [ExternalMod],
|
|
|
|
})
|
|
|
|
export class MyMod {}
|
|
|
|
`);
|
|
|
|
env.driveMain();
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
|
|
|
|
// Invalidate MyCmpA, causing it to be re-emitted.
|
|
|
|
env.invalidateCachedFile('cmp-a.ts');
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
expectToHaveWritten([
|
|
|
|
// MyMod is written because it has a direct reference to MyCmpA, which was invalidated.
|
|
|
|
'/mod.js',
|
|
|
|
|
|
|
|
// MyCmpA is written because it was invalidated.
|
|
|
|
'/cmp-a.js',
|
|
|
|
|
|
|
|
// MyCmpB should not be written because it is unaffected.
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should recompile components once an external declaration is changed', () => {
|
|
|
|
// Testing setup: Two components (MyCmpA and MyCmpB) both depend on an external directive
|
|
|
|
// which matches their templates, via an NgModule import.
|
|
|
|
//
|
|
|
|
// During the test, the external directive is invalidated, and the test verifies that both
|
|
|
|
// components are re-emitted as a result.
|
|
|
|
env.write('node_modules/external/index.d.ts', `
|
|
|
|
import * as ng from '@angular/core';
|
|
|
|
|
|
|
|
export declare class ExternalDir {
|
|
|
|
static ɵdir: ng.ɵɵDirectiveDefWithMeta<ExternalDir, "[external]", never, {}, {}, never>;
|
|
|
|
}
|
|
|
|
|
|
|
|
export declare class ExternalMod {
|
|
|
|
static ɵmod: ng.ɵɵNgModuleDefWithMeta<ExternalMod, [typeof ExternalDir], never, [typeof ExternalDir]>;
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
env.write('cmp-a.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
template: '<div external></div>',
|
|
|
|
})
|
|
|
|
export class MyCmpA {}
|
|
|
|
`);
|
|
|
|
env.write('cmp-b.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
template: '<div external></div>',
|
|
|
|
})
|
|
|
|
export class MyCmpB {}
|
|
|
|
`);
|
|
|
|
env.write('mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {ExternalMod} from 'external';
|
|
|
|
import {MyCmpA} from './cmp-a';
|
|
|
|
import {MyCmpB} from './cmp-b';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [MyCmpA, MyCmpB],
|
|
|
|
imports: [ExternalMod],
|
|
|
|
})
|
|
|
|
export class MyMod {}
|
|
|
|
`);
|
|
|
|
env.driveMain();
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
|
|
|
|
// Invalidate the external file. Only the referential identity of external symbols matters
|
|
|
|
// for emit reuse, so invalidating this should cause all dependent components to be
|
|
|
|
// re-emitted.
|
|
|
|
env.invalidateCachedFile('node_modules/external/index.d.ts');
|
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
expectToHaveWritten([
|
|
|
|
// MyMod is written because it has a direct reference to ExternalMod, which was
|
|
|
|
// invalidated.
|
|
|
|
'/mod.js',
|
|
|
|
|
|
|
|
// MyCmpA is written because it uses ExternalDir, which has not changed public API but has
|
|
|
|
// changed identity.
|
|
|
|
'/cmp-a.js',
|
|
|
|
|
|
|
|
// MyCmpB is written because it uses ExternalDir, which has not changed public API but has
|
|
|
|
// changed identity.
|
|
|
|
'/cmp-b.js',
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('symbol identity', () => {
|
|
|
|
it('should recompile components when their declaration name changes', () => {
|
|
|
|
// Testing setup: component Cmp depends on component Dep, which is directly exported.
|
|
|
|
//
|
|
|
|
// During the test, Dep's name is changed while keeping its public API the same. The test
|
|
|
|
// verifies that Cmp is re-emitted.
|
|
|
|
env.write('cmp.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'cmp',
|
|
|
|
template: '<dep></dep>',
|
|
|
|
})
|
|
|
|
export class Cmp {}
|
|
|
|
`);
|
|
|
|
env.write('dep.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'dep',
|
|
|
|
template: 'Dep',
|
|
|
|
})
|
|
|
|
export class Dep {}
|
|
|
|
`);
|
|
|
|
env.write('mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {Cmp} from './cmp';
|
|
|
|
import {Dep} from './dep';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Cmp, Dep]
|
|
|
|
})
|
|
|
|
export class Mod {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
env.write('dep.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'dep',
|
|
|
|
template: 'Dep',
|
|
|
|
})
|
|
|
|
export class ChangedDep {} // Dep renamed to ChangedDep.
|
|
|
|
`);
|
|
|
|
env.write('mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
|
|
|
|
import {Cmp} from './cmp';
|
|
|
|
import {ChangedDep} from './dep';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Cmp, ChangedDep]
|
|
|
|
})
|
|
|
|
export class Mod {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
expectToHaveWritten([
|
|
|
|
// Dep and Mod were directly updated.
|
|
|
|
'/dep.js',
|
|
|
|
'/mod.js',
|
|
|
|
|
|
|
|
// Cmp required a re-emit because the name of Dep changed.
|
|
|
|
'/cmp.js',
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should not recompile components that use a local directive', () => {
|
|
|
|
// Testing setup: a single source file 'cmp.ts' declares components `Cmp` and `Dir`, where
|
|
|
|
// `Cmp` uses `Dir` in its template. This test verifies that the local reference of `Cmp`
|
|
|
|
// that is emitted into `Dir` does not inadvertently cause `cmp.ts` to be emitted even when
|
|
|
|
// nothing changed.
|
|
|
|
env.write('cmp.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'dep',
|
|
|
|
template: 'Dep',
|
|
|
|
})
|
|
|
|
export class Dep {}
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'cmp',
|
|
|
|
template: '<dep></dep>',
|
|
|
|
})
|
|
|
|
export class Cmp {}
|
|
|
|
`);
|
|
|
|
env.write('mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {Cmp, Dep} from './cmp';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Cmp, Dep]
|
|
|
|
})
|
|
|
|
export class Mod {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
env.invalidateCachedFile('mod.ts');
|
|
|
|
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
expectToHaveWritten([
|
|
|
|
// Only `mod.js` should be written because it was invalidated.
|
|
|
|
'/mod.js',
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should recompile components when the name by which they are exported changes', () => {
|
|
|
|
// Testing setup: component Cmp depends on component Dep, which is directly exported.
|
|
|
|
//
|
|
|
|
// During the test, Dep's exported name is changed while keeping its declaration name the
|
|
|
|
// same. The test verifies that Cmp is re-emitted.
|
|
|
|
env.write('cmp.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'cmp',
|
|
|
|
template: '<dep></dep>',
|
|
|
|
})
|
|
|
|
export class Cmp {}
|
|
|
|
`);
|
|
|
|
env.write('dep.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'dep',
|
|
|
|
template: 'Dep',
|
|
|
|
})
|
|
|
|
export class Dep {}
|
|
|
|
`);
|
|
|
|
env.write('mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {Cmp} from './cmp';
|
|
|
|
import {Dep} from './dep';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Cmp, Dep]
|
|
|
|
})
|
|
|
|
export class Mod {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
env.write('dep.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'dep',
|
|
|
|
template: 'Dep',
|
|
|
|
})
|
|
|
|
class Dep {}
|
|
|
|
export {Dep as ChangedDep}; // the export name of Dep is changed.
|
|
|
|
`);
|
|
|
|
env.write('mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
|
|
|
|
import {Cmp} from './cmp';
|
|
|
|
import {ChangedDep} from './dep';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Cmp, ChangedDep]
|
|
|
|
})
|
|
|
|
export class Mod {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
expectToHaveWritten([
|
|
|
|
// Dep and Mod were directly updated.
|
|
|
|
'/dep.js',
|
|
|
|
'/mod.js',
|
|
|
|
|
|
|
|
// Cmp required a re-emit because the exported name of Dep changed.
|
|
|
|
'/cmp.js',
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should recompile components when a re-export is renamed', () => {
|
|
|
|
// Testing setup: CmpUser uses CmpDep in its template. CmpDep is both directly and
|
|
|
|
// indirectly exported, and the compiler is guided into using the indirect export.
|
|
|
|
//
|
|
|
|
// During the test, the indirect export name is changed, and the test verifies that CmpUser
|
|
|
|
// is re-emitted.
|
|
|
|
|
|
|
|
env.write('cmp-user.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'cmp-user',
|
|
|
|
template: '<cmp-dep></cmp-dep>',
|
|
|
|
})
|
|
|
|
export class CmpUser {}
|
|
|
|
`);
|
|
|
|
env.write('cmp-dep.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
export {CmpDep as CmpDepExport};
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'cmp-dep',
|
|
|
|
template: 'Dep',
|
|
|
|
})
|
2021-04-28 16:14:38 -04:00
|
|
|
class CmpDep {}
|
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('module.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {CmpUser} from './cmp-user';
|
|
|
|
import {CmpDepExport} from './cmp-dep';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [CmpUser, CmpDepExport]
|
|
|
|
})
|
|
|
|
export class Module {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
// Verify that the reference emitter used the export of `CmpDep` that appeared first in
|
|
|
|
// the source, i.e. `CmpDepExport`.
|
|
|
|
const userCmpJs = env.getContents('cmp-user.js');
|
|
|
|
expect(userCmpJs).toContain('CmpDepExport');
|
|
|
|
|
|
|
|
env.write('cmp-dep.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
export {CmpDep as CmpDepExport2};
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'cmp-dep',
|
|
|
|
template: 'Dep',
|
|
|
|
})
|
2021-04-28 16:14:38 -04:00
|
|
|
class CmpDep {}
|
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('module.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {CmpUser} from './cmp-user';
|
|
|
|
import {CmpDepExport2} from './cmp-dep';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [CmpUser, CmpDepExport2]
|
|
|
|
})
|
|
|
|
export class Module {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
expectToHaveWritten([
|
|
|
|
// CmpDep and its module were directly updated.
|
|
|
|
'/cmp-dep.js',
|
|
|
|
'/module.js',
|
|
|
|
|
|
|
|
// CmpUser required a re-emit because it was previous emitted as `CmpDepExport`, but
|
|
|
|
// that export has since been renamed.
|
|
|
|
'/cmp-user.js',
|
|
|
|
]);
|
|
|
|
|
|
|
|
// Verify that `CmpUser` now correctly imports `CmpDep` using its renamed
|
|
|
|
// re-export `CmpDepExport2`.
|
|
|
|
const userCmp2Js = env.getContents('cmp-user.js');
|
|
|
|
expect(userCmp2Js).toContain('CmpDepExport2');
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should not recompile components when a directive is changed into a component', () => {
|
|
|
|
env.write('cmp-user.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'cmp-user',
|
|
|
|
template: '<div dep></div>',
|
|
|
|
})
|
|
|
|
export class CmpUser {}
|
|
|
|
`);
|
|
|
|
env.write('dep.ts', `
|
|
|
|
import {Directive} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({
|
|
|
|
selector: '[dep]',
|
|
|
|
})
|
|
|
|
export class Dep {}
|
|
|
|
`);
|
|
|
|
env.write('module.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {CmpUser} from './cmp-user';
|
|
|
|
import {Dep} from './dep';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [CmpUser, Dep]
|
|
|
|
})
|
|
|
|
export class Module {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
env.write('dep.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: '[dep]',
|
|
|
|
template: 'Dep',
|
|
|
|
})
|
|
|
|
export class Dep {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
expectToHaveWritten([
|
|
|
|
// Dep was directly changed.
|
|
|
|
'/dep.js',
|
|
|
|
|
|
|
|
// Module required a re-emit because its direct dependency (Dep) was changed.
|
|
|
|
'/module.js',
|
|
|
|
|
|
|
|
// CmpUser did not require a re-emit because its semantic dependencies were not affected.
|
|
|
|
// Dep is still matched and still has the same public API.
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should recompile components when a directive and pipe are swapped', () => {
|
|
|
|
// CmpUser uses a directive DepA and a pipe DepB, with the same selector/name 'dep'.
|
|
|
|
//
|
|
|
|
// During the test, the decorators of DepA and DepB are swapped, effectively changing the
|
|
|
|
// SemanticSymbol types for them into different species while ensuring that CmpUser's
|
|
|
|
// template is still valid. The test then verifies that CmpUser is re-emitted.
|
|
|
|
|
|
|
|
env.write('cmp-user.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'cmp-user',
|
|
|
|
template: '<dep>{{1 | dep}}</dep>',
|
|
|
|
})
|
|
|
|
export class CmpUser {}
|
|
|
|
`);
|
|
|
|
env.write('dep.ts', `
|
|
|
|
import {Directive, Pipe} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({
|
|
|
|
selector: 'dep',
|
|
|
|
})
|
|
|
|
export class DepA {}
|
|
|
|
|
|
|
|
@Pipe({
|
|
|
|
name: 'dep',
|
|
|
|
})
|
2021-01-22 12:03:37 -05:00
|
|
|
export class DepB {
|
|
|
|
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
|
|
|
`);
|
|
|
|
env.write('module.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {CmpUser} from './cmp-user';
|
|
|
|
import {DepA, DepB} from './dep';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [CmpUser, DepA, DepB],
|
|
|
|
})
|
|
|
|
export class Module {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
// The annotations on DepA and DepB are swapped. This ensures that when we're comparing the
|
|
|
|
// public API of these symbols to the prior program, the prior symbols are of a different
|
|
|
|
// type (pipe vs directive) than the new symbols, which should lead to a re-emit.
|
|
|
|
env.write('dep.ts', `
|
|
|
|
import {Directive, Pipe} from '@angular/core';
|
|
|
|
|
|
|
|
@Pipe({
|
|
|
|
name: 'dep',
|
|
|
|
})
|
2021-01-22 12:03:37 -05:00
|
|
|
export class DepA {
|
|
|
|
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
|
|
|
|
|
|
|
@Directive({
|
|
|
|
selector: 'dep',
|
|
|
|
})
|
|
|
|
export class DepB {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
expectToHaveWritten([
|
|
|
|
// Dep was directly changed.
|
|
|
|
'/dep.js',
|
|
|
|
|
|
|
|
// Module required a re-emit because its direct dependency (Dep) was changed.
|
|
|
|
'/module.js',
|
|
|
|
|
|
|
|
// CmpUser required a re-emit because the shape of its matched symbols changed.
|
|
|
|
'/cmp-user.js',
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should not recompile components when a component is changed into a directive', () => {
|
|
|
|
// Testing setup: CmpUser depends on a component Dep with an attribute selector.
|
|
|
|
//
|
|
|
|
// During the test, Dep is changed into a directive, and the test verifies that CmpUser is
|
|
|
|
// not re-emitted (as the public API of a directive and a component are the same).
|
|
|
|
|
|
|
|
env.write('cmp-user.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'cmp-user',
|
|
|
|
template: '<div dep></div>',
|
|
|
|
})
|
|
|
|
export class CmpUser {}
|
|
|
|
`);
|
|
|
|
env.write('dep.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: '[dep]',
|
|
|
|
template: 'Dep',
|
|
|
|
})
|
|
|
|
export class Dep {}
|
|
|
|
`);
|
|
|
|
env.write('module.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {CmpUser} from './cmp-user';
|
|
|
|
import {Dep} from './dep';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [CmpUser, Dep]
|
|
|
|
})
|
|
|
|
export class Module {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
env.write('dep.ts', `
|
|
|
|
import {Directive} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({
|
|
|
|
selector: '[dep]',
|
|
|
|
})
|
|
|
|
export class Dep {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
expectToHaveWritten([
|
|
|
|
// Dep was directly changed.
|
|
|
|
'/dep.js',
|
|
|
|
|
|
|
|
// Module required a re-emit because its direct dependency (Dep) was changed.
|
|
|
|
'/module.js',
|
|
|
|
|
|
|
|
// CmpUser did not require a re-emit because its semantic dependencies were not affected.
|
|
|
|
// Dep is still matched and still has the same public API.
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('remote scoping', () => {
|
|
|
|
it('should not recompile an NgModule nor component when remote scoping is unaffected', () => {
|
|
|
|
// Testing setup: MyCmpA and MyCmpB are two components with an indirect import cycle. That
|
|
|
|
// is, each component consumes the other in its template. This forces the compiler to use
|
|
|
|
// remote scoping to set the directiveDefs of at least one of the components in their
|
|
|
|
// NgModule.
|
|
|
|
//
|
|
|
|
// During the test, an unrelated change is made to the template of MyCmpB, and the test
|
|
|
|
// verifies that the NgModule for the components is not re-emitted.
|
|
|
|
|
|
|
|
env.write('cmp-a-template.html', `<cmp-b><cmp-b>`);
|
|
|
|
env.write('cmp-a.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'cmp-a',
|
|
|
|
templateUrl: './cmp-a-template.html',
|
|
|
|
})
|
|
|
|
export class MyCmpA {}
|
|
|
|
`);
|
|
|
|
env.write('cmp-b-template.html', `<cmp-a><cmp-a>`);
|
|
|
|
env.write('cmp-b.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'cmp-b',
|
|
|
|
templateUrl: './cmp-b-template.html',
|
|
|
|
})
|
|
|
|
export class MyCmpB {}
|
|
|
|
`);
|
|
|
|
env.write('mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {MyCmpA} from './cmp-a';
|
|
|
|
import {MyCmpB} from './cmp-b';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [MyCmpA, MyCmpB],
|
|
|
|
})
|
|
|
|
export class MyMod {}
|
|
|
|
`);
|
|
|
|
env.driveMain();
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
|
|
|
|
env.write('cmp-b-template.html', `<cmp-a>Update</cmp-a>`);
|
|
|
|
env.driveMain();
|
|
|
|
expectToHaveWritten([
|
|
|
|
// MyCmpB is written because its template was updated.
|
|
|
|
'/cmp-b.js',
|
|
|
|
|
|
|
|
// MyCmpA should not be written because MyCmpB's public API didn't change.
|
|
|
|
// MyMod should not be written because remote scoping didn't change.
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should recompile an NgModule and component when an import cycle is introduced', () => {
|
|
|
|
// Testing setup: MyCmpA and MyCmpB are two components where MyCmpB consumes MyCmpA in its
|
|
|
|
// template.
|
|
|
|
//
|
|
|
|
// During the test, MyCmpA's template is updated to consume MyCmpB, creating an effective
|
|
|
|
// import cycle and forcing the compiler to use remote scoping for at least one of the
|
|
|
|
// components. The test verifies that the components' NgModule is emitted as a result.
|
|
|
|
|
|
|
|
env.write('cmp-a-template.html', ``);
|
|
|
|
env.write('cmp-a.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'cmp-a',
|
|
|
|
templateUrl: './cmp-a-template.html',
|
|
|
|
})
|
|
|
|
export class MyCmpA {}
|
|
|
|
`);
|
|
|
|
env.write('cmp-b-template.html', `<cmp-a><cmp-a>`);
|
|
|
|
env.write('cmp-b.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'cmp-b',
|
|
|
|
templateUrl: './cmp-b-template.html',
|
|
|
|
})
|
|
|
|
export class MyCmpB {}
|
|
|
|
`);
|
|
|
|
env.write('mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {MyCmpA} from './cmp-a';
|
|
|
|
import {MyCmpB} from './cmp-b';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [MyCmpA, MyCmpB],
|
|
|
|
})
|
|
|
|
export class MyMod {}
|
|
|
|
`);
|
|
|
|
env.driveMain();
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
|
|
|
|
env.write('cmp-a-template.html', `<cmp-b><cmp-b>`);
|
|
|
|
env.driveMain();
|
|
|
|
expectToHaveWritten([
|
|
|
|
// MyMod is written because its remote scopes have changed.
|
|
|
|
'/mod.js',
|
|
|
|
|
|
|
|
// MyCmpA is written because its template was updated.
|
|
|
|
'/cmp-a.js',
|
|
|
|
|
|
|
|
// MyCmpB is written because it now requires remote scoping, where previously it did not.
|
|
|
|
'/cmp-b.js',
|
|
|
|
]);
|
|
|
|
|
|
|
|
// Validate the correctness of the assumptions made above:
|
|
|
|
// * CmpA should not be using remote scoping.
|
|
|
|
// * CmpB should be using remote scoping.
|
|
|
|
const moduleJs = env.getContents('mod.js');
|
|
|
|
expect(moduleJs).not.toContain('setComponentScope(MyCmpA,');
|
|
|
|
expect(moduleJs).toContain('setComponentScope(MyCmpB,');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should recompile an NgModule and component when an import cycle is removed', () => {
|
|
|
|
// Testing setup: MyCmpA and MyCmpB are two components that each consume the other in their
|
|
|
|
// template, forcing the compiler to utilize remote scoping for at least one of them.
|
|
|
|
//
|
|
|
|
// During the test, MyCmpA's template is updated to no longer consume MyCmpB, breaking the
|
|
|
|
// effective import cycle and causing remote scoping to no longer be required. The test
|
|
|
|
// verifies that the components' NgModule is emitted as a result.
|
|
|
|
|
|
|
|
env.write('cmp-a-template.html', `<cmp-b><cmp-b>`);
|
|
|
|
env.write('cmp-a.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'cmp-a',
|
|
|
|
templateUrl: './cmp-a-template.html',
|
|
|
|
})
|
|
|
|
export class MyCmpA {}
|
|
|
|
`);
|
|
|
|
env.write('cmp-b-template.html', `<cmp-a><cmp-a>`);
|
|
|
|
env.write('cmp-b.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'cmp-b',
|
|
|
|
templateUrl: './cmp-b-template.html',
|
|
|
|
})
|
|
|
|
export class MyCmpB {}
|
|
|
|
`);
|
|
|
|
env.write('mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {MyCmpA} from './cmp-a';
|
|
|
|
import {MyCmpB} from './cmp-b';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [MyCmpA, MyCmpB],
|
|
|
|
})
|
|
|
|
export class MyMod {}
|
|
|
|
`);
|
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
// Validate the correctness of the assumption that CmpB will be the remotely scoped
|
|
|
|
// component due to the above cycle:
|
|
|
|
const moduleJs = env.getContents('mod.js');
|
|
|
|
expect(moduleJs).not.toContain('setComponentScope(MyCmpA,');
|
|
|
|
expect(moduleJs).toContain('setComponentScope(MyCmpB,');
|
|
|
|
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
env.write('cmp-a-template.html', ``);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
expectToHaveWritten([
|
|
|
|
// MyMod is written because its remote scopes have changed.
|
|
|
|
'/mod.js',
|
|
|
|
|
|
|
|
// MyCmpA is written because its template was updated.
|
|
|
|
'/cmp-a.js',
|
|
|
|
|
|
|
|
// MyCmpB is written because it no longer needs remote scoping.
|
|
|
|
'/cmp-b.js',
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should recompile an NgModule when a remotely scoped component\'s scope is changed',
|
|
|
|
() => {
|
|
|
|
// Testing setup: MyCmpA and MyCmpB are two components that each consume the other in
|
|
|
|
// their template, forcing the compiler to utilize remote scoping for MyCmpB (which is
|
|
|
|
// verified). Dir is a directive which is initially unused by either component.
|
|
|
|
//
|
|
|
|
// During the test, MyCmpB is updated to additionally consume Dir in its template. This
|
|
|
|
// changes the remote scope of MyCmpB, requiring a re-emit of its NgModule which the test
|
|
|
|
// verifies.
|
|
|
|
|
|
|
|
env.write('dir.ts', `
|
|
|
|
import {Directive} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({
|
|
|
|
selector: '[dir]',
|
|
|
|
})
|
|
|
|
export class Dir {}
|
|
|
|
`);
|
|
|
|
env.write('cmp-a-template.html', `<cmp-b><cmp-b>`);
|
|
|
|
env.write('cmp-a.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'cmp-a',
|
|
|
|
templateUrl: './cmp-a-template.html',
|
|
|
|
})
|
|
|
|
export class MyCmpA {}
|
|
|
|
`);
|
|
|
|
env.write('cmp-b-template.html', `<cmp-a><cmp-a>`);
|
|
|
|
env.write('cmp-b.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'cmp-b',
|
|
|
|
templateUrl: './cmp-b-template.html',
|
|
|
|
})
|
|
|
|
export class MyCmpB {}
|
|
|
|
`);
|
|
|
|
env.write('mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {MyCmpA} from './cmp-a';
|
|
|
|
import {MyCmpB} from './cmp-b';
|
|
|
|
import {Dir} from './dir';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [MyCmpA, MyCmpB, Dir],
|
|
|
|
})
|
|
|
|
export class MyMod {}
|
|
|
|
`);
|
|
|
|
env.driveMain();
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
|
|
|
|
// Validate the correctness of the assumption that MyCmpB will be remotely scoped:
|
|
|
|
const moduleJs = env.getContents('mod.js');
|
|
|
|
expect(moduleJs).not.toContain('setComponentScope(MyCmpA,');
|
|
|
|
expect(moduleJs).toContain('setComponentScope(MyCmpB,');
|
|
|
|
|
|
|
|
env.write('cmp-b-template.html', `<cmp-a dir>Update</cmp-a>`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
expectToHaveWritten([
|
|
|
|
// MyCmpB is written because its template was updated.
|
|
|
|
'/cmp-b.js',
|
|
|
|
|
|
|
|
// MyMod should be written because one of its remotely scoped components has a changed
|
|
|
|
// scope.
|
|
|
|
'/mod.js'
|
|
|
|
|
|
|
|
// MyCmpA should not be written because none of its dependencies have changed in their
|
|
|
|
// public API.
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should recompile an NgModule when its set of remotely scoped components changes', () => {
|
|
|
|
// Testing setup: three components (MyCmpA, MyCmpB, and MyCmpC) are declared. MyCmpA
|
|
|
|
// consumes the other two in its template, and MyCmpB consumes MyCmpA creating an effective
|
|
|
|
// import cycle that forces the compiler to use remote scoping for MyCmpB (which is
|
|
|
|
// verified).
|
|
|
|
//
|
|
|
|
// During the test, MyCmpC's template is changed to depend on MyCmpA, forcing remote
|
|
|
|
// scoping for it as well. The test verifies that the NgModule is re-emitted as a new
|
|
|
|
// component within it now requires remote scoping.
|
|
|
|
|
|
|
|
env.write('cmp-a-template.html', `<cmp-b><cmp-b> <cmp-c></cmp-c>`);
|
|
|
|
env.write('cmp-a.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'cmp-a',
|
|
|
|
templateUrl: './cmp-a-template.html',
|
|
|
|
})
|
|
|
|
export class MyCmpA {}
|
|
|
|
`);
|
|
|
|
env.write('cmp-b-template.html', `<cmp-a><cmp-a>`);
|
|
|
|
env.write('cmp-b.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'cmp-b',
|
|
|
|
templateUrl: './cmp-b-template.html',
|
|
|
|
})
|
|
|
|
export class MyCmpB {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.write('cmp-c-template.html', ``);
|
|
|
|
env.write('cmp-c.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'cmp-c',
|
|
|
|
templateUrl: './cmp-c-template.html',
|
|
|
|
})
|
|
|
|
export class MyCmpC {}
|
|
|
|
`);
|
|
|
|
env.write('mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {MyCmpA} from './cmp-a';
|
|
|
|
import {MyCmpB} from './cmp-b';
|
|
|
|
import {MyCmpC} from './cmp-c';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [MyCmpA, MyCmpB, MyCmpC],
|
|
|
|
})
|
|
|
|
export class MyMod {}
|
|
|
|
`);
|
|
|
|
env.driveMain();
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
|
|
|
|
// Validate the correctness of the assumption that MyCmpB will be the only remotely
|
|
|
|
// scoped component due to the MyCmpA <-> MyCmpB cycle:
|
|
|
|
const moduleJsBefore = env.getContents('mod.js');
|
|
|
|
expect(moduleJsBefore).not.toContain('setComponentScope(MyCmpA,');
|
|
|
|
expect(moduleJsBefore).toContain('setComponentScope(MyCmpB,');
|
|
|
|
expect(moduleJsBefore).not.toContain('setComponentScope(MyCmpC,');
|
|
|
|
|
|
|
|
env.write('cmp-c-template.html', `<cmp-a>Update</cmp-a>`);
|
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
// Validate the correctness of the assumption that MyCmpB and MyCmpC are now both
|
|
|
|
// remotely scoped due to the MyCmpA <-> MyCmpB and MyCmpA <-> MyCmpC cycles:
|
|
|
|
const moduleJsAfter = env.getContents('mod.js');
|
|
|
|
expect(moduleJsAfter).not.toContain('setComponentScope(MyCmpA,');
|
|
|
|
expect(moduleJsAfter).toContain('setComponentScope(MyCmpB,');
|
|
|
|
expect(moduleJsAfter).toContain('setComponentScope(MyCmpC,');
|
|
|
|
|
|
|
|
expectToHaveWritten([
|
|
|
|
// MyCmpC is written because its template was updated.
|
|
|
|
'/cmp-c.js',
|
|
|
|
|
|
|
|
// MyMod should be written because MyCmpC became remotely scoped
|
|
|
|
'/mod.js'
|
|
|
|
|
|
|
|
// MyCmpA and MyCmpB should not be written because none of their dependencies have
|
|
|
|
// changed in their public API.
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('NgModule declarations', () => {
|
|
|
|
it('should recompile components when a matching directive is added in the direct scope',
|
|
|
|
() => {
|
|
|
|
// Testing setup: A component Cmp has a template which would match a directive Dir,
|
|
|
|
// except Dir is not included in Cmp's NgModule.
|
|
|
|
//
|
|
|
|
// During the test, Dir is added to the NgModule, causing it to begin matching in Cmp's
|
|
|
|
// template. The test verifies that Cmp is re-emitted to account for this.
|
|
|
|
|
|
|
|
env.write('dir.ts', `
|
|
|
|
import {Directive} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({
|
|
|
|
selector: '[dir]',
|
|
|
|
})
|
|
|
|
export class Dir {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.write('cmp.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'test-cmp',
|
|
|
|
template: '<div dir></div>',
|
|
|
|
})
|
|
|
|
export class Cmp {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.write('mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {Cmp} from './cmp';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Cmp],
|
|
|
|
})
|
|
|
|
export class Mod {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
|
|
|
|
env.write('mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {Cmp} from './cmp';
|
|
|
|
import {Dir} from './dir';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Cmp, Dir],
|
|
|
|
})
|
|
|
|
export class Mod {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
expectToHaveWritten([
|
|
|
|
// Mod is written as it was directly changed.
|
|
|
|
'/mod.js',
|
|
|
|
|
|
|
|
// Cmp is written as a matching directive was added to Mod's scope.
|
|
|
|
'/cmp.js',
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should recompile components when a matching directive is removed from the direct scope',
|
|
|
|
() => {
|
|
|
|
// Testing setup: Cmp is a component with a template that matches a directive Dir.
|
|
|
|
//
|
|
|
|
// During the test, Dir is removed from Cmp's NgModule, which causes it to stop matching
|
|
|
|
// in Cmp's template. The test verifies that Cmp is re-emitted as a result.
|
|
|
|
|
|
|
|
env.write('dir.ts', `
|
|
|
|
import {Directive} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({
|
|
|
|
selector: '[dir]',
|
|
|
|
})
|
|
|
|
export class Dir {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.write('cmp.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'test-cmp',
|
|
|
|
template: '<div dir></div>',
|
|
|
|
})
|
|
|
|
export class Cmp {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.write('mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {Cmp} from './cmp';
|
|
|
|
import {Dir} from './dir';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Cmp, Dir],
|
|
|
|
})
|
|
|
|
export class Mod {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
|
|
|
|
env.write('mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {Cmp} from './cmp';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Cmp],
|
|
|
|
})
|
|
|
|
export class Mod {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
expectToHaveWritten([
|
|
|
|
// Mod is written as it was directly changed.
|
|
|
|
'/mod.js',
|
|
|
|
|
|
|
|
// Cmp is written as a matching directive was removed from Mod's scope.
|
|
|
|
'/cmp.js',
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should recompile components when a matching directive is added in the transitive scope',
|
|
|
|
() => {
|
|
|
|
// Testing setup: A component Cmp has a template which would match a directive Dir,
|
|
|
|
// except Dir is not included in Cmp's NgModule.
|
|
|
|
//
|
|
|
|
// During the test, Dir is added to the NgModule via an import, causing it to begin
|
|
|
|
// matching in Cmp's template. The test verifies that Cmp is re-emitted to account for
|
|
|
|
// this.
|
|
|
|
|
|
|
|
env.write('dir.ts', `
|
|
|
|
import {Directive} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({
|
|
|
|
selector: '[dir]',
|
|
|
|
})
|
|
|
|
export class Dir {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.write('cmp.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'test-cmp',
|
|
|
|
template: '<div dir></div>',
|
|
|
|
})
|
|
|
|
export class Cmp {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.write('deep.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [],
|
|
|
|
exports: [],
|
|
|
|
})
|
|
|
|
export class Deep {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.write('mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {Cmp} from './cmp';
|
|
|
|
import {Deep} from './deep';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Cmp],
|
|
|
|
imports: [Deep],
|
|
|
|
})
|
|
|
|
export class Mod {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
|
|
|
|
|
|
|
|
env.write('deep.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {Dir} from './dir';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Dir],
|
|
|
|
exports: [Dir],
|
|
|
|
})
|
|
|
|
export class Deep {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
expectToHaveWritten([
|
|
|
|
// Mod is written as it was directly changed.
|
|
|
|
'/deep.js',
|
|
|
|
|
|
|
|
// Mod is written as its direct dependency (Deep) was changed.
|
|
|
|
'/mod.js',
|
|
|
|
|
|
|
|
// Cmp is written as a matching directive was added to Mod's transitive scope.
|
|
|
|
'/cmp.js',
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should recompile components when a matching directive is removed from the transitive scope',
|
|
|
|
() => {
|
|
|
|
// Testing setup: Cmp is a component with a template that matches a directive Dir, due to
|
|
|
|
// Dir's NgModule being imported into Cmp's NgModule.
|
|
|
|
//
|
|
|
|
// During the test, this import link is removed, which causes Dir to stop matching in
|
|
|
|
// Cmp's template. The test verifies that Cmp is re-emitted as a result.
|
|
|
|
|
|
|
|
env.write('dir.ts', `
|
|
|
|
import {Directive} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({
|
|
|
|
selector: '[dir]',
|
|
|
|
})
|
|
|
|
export class Dir {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.write('cmp.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'test-cmp',
|
|
|
|
template: '<div dir></div>',
|
|
|
|
})
|
|
|
|
export class Cmp {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.write('deep.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {Dir} from './dir';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Dir],
|
|
|
|
exports: [Dir],
|
|
|
|
})
|
|
|
|
export class Deep {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.write('mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {Cmp} from './cmp';
|
|
|
|
import {Deep} from './deep';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Cmp],
|
|
|
|
imports: [Deep],
|
|
|
|
})
|
|
|
|
export class Mod {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
|
|
|
|
env.write('deep.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [],
|
|
|
|
exports: [],
|
|
|
|
})
|
|
|
|
export class Deep {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
expectToHaveWritten([
|
|
|
|
// Mod is written as it was directly changed.
|
|
|
|
'/deep.js',
|
|
|
|
|
|
|
|
// Mod is written as its direct dependency (Deep) was changed.
|
|
|
|
'/mod.js',
|
|
|
|
|
|
|
|
// Cmp is written as a matching directive was removed from Mod's transitive scope.
|
|
|
|
'/cmp.js',
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should not recompile components when a non-matching directive is added in scope', () => {
|
|
|
|
// Testing setup: A component Cmp has a template which does not match a directive Dir,
|
|
|
|
// and Dir is not included in Cmp's NgModule.
|
|
|
|
//
|
|
|
|
// During the test, Dir is added to the NgModule, making it visible in Cmp's template.
|
|
|
|
// However, Dir still does not match the template. The test verifies that Cmp is not
|
|
|
|
// re-emitted.
|
|
|
|
|
|
|
|
env.write('dir.ts', `
|
|
|
|
import {Directive} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({
|
|
|
|
selector: '[dir]',
|
|
|
|
})
|
|
|
|
export class Dir {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.write('cmp.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'test-cmp',
|
|
|
|
template: '<div></div>',
|
|
|
|
})
|
|
|
|
export class Cmp {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.write('mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {Cmp} from './cmp';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Cmp],
|
|
|
|
})
|
|
|
|
export class Mod {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
|
|
|
|
env.write('mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {Cmp} from './cmp';
|
|
|
|
import {Dir} from './dir';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Cmp, Dir],
|
|
|
|
})
|
|
|
|
export class Mod {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
expectToHaveWritten([
|
|
|
|
// Mod is written as it was directly changed.
|
|
|
|
'/mod.js',
|
|
|
|
|
|
|
|
// Cmp is not written as its used directives remains the same, since Dir does not match
|
|
|
|
// within its template.
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('error recovery', () => {
|
|
|
|
it('should recompile a component when a matching directive is added that first contains an error',
|
|
|
|
() => {
|
|
|
|
// Testing setup: Cmp is a component which would match a directive with the selector
|
|
|
|
// '[dir]'.
|
|
|
|
//
|
|
|
|
// During the test, an initial incremental compilation adds an import to a hypothetical
|
|
|
|
// directive Dir to the NgModule, and adds Dir as a declaration. However, the import
|
|
|
|
// points to a non-existent file.
|
|
|
|
//
|
|
|
|
// During a second incremental compilation, that missing file is added with a declaration
|
|
|
|
// for Dir as a directive with the selector '[dir]', causing it to begin matching in
|
|
|
|
// Cmp's template. The test verifies that Cmp is re-emitted once the program is correct.
|
|
|
|
|
|
|
|
env.write('cmp.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'test-cmp',
|
|
|
|
template: '<div dir></div>',
|
|
|
|
})
|
|
|
|
export class Cmp {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.write('mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {Cmp} from './cmp';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Cmp],
|
|
|
|
})
|
|
|
|
export class Mod {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
|
|
|
|
env.write('mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {Cmp} from './cmp';
|
|
|
|
import {Dir} from './dir';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Cmp, Dir],
|
|
|
|
})
|
|
|
|
export class Mod {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
expect(env.driveDiagnostics().length).not.toBe(0);
|
|
|
|
|
|
|
|
env.write('dir.ts', `
|
|
|
|
import {Directive} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({
|
|
|
|
selector: '[dir]',
|
|
|
|
})
|
|
|
|
export class Dir {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
expectToHaveWritten([
|
|
|
|
// Mod is written as it was changed in the first incremental compilation, but had
|
|
|
|
// errors and so was not written then.
|
|
|
|
'/mod.js',
|
|
|
|
|
|
|
|
// Dir is written as it was added in the second incremental compilation.
|
|
|
|
'/dir.js',
|
|
|
|
|
|
|
|
// Cmp is written as the cumulative effect of the two changes was to add Dir to its
|
|
|
|
// scope and thus match in Cmp's template.
|
|
|
|
'/cmp.js',
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should correctly emit components when public API changes during a broken program', () => {
|
|
|
|
// Testing setup: a component Cmp exists with a template that matches directive Dir. Cmp also
|
|
|
|
// references an extra file with a constant declaration.
|
|
|
|
//
|
|
|
|
// During the test, a first incremental compilation both adds an input to Dir (changing its
|
|
|
|
// public API) as well as introducing a compilation error by adding invalid syntax to the
|
|
|
|
// extra file.
|
|
|
|
//
|
|
|
|
// A second incremental compilation then fixes the invalid syntax, and the test verifies that
|
|
|
|
// Cmp is re-emitted due to the earlier public API change to Dir.
|
|
|
|
|
|
|
|
env.write('other.ts', `
|
|
|
|
export const FOO = true;
|
|
|
|
`);
|
|
|
|
env.write('dir.ts', `
|
|
|
|
import {Directive, Input} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({
|
|
|
|
selector: '[dir]',
|
|
|
|
})
|
|
|
|
export class Dir {
|
|
|
|
@Input()
|
|
|
|
dirIn!: string;
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
env.write('cmp.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
import './other';
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'test-cmp',
|
|
|
|
template: '<div dir></div>',
|
|
|
|
})
|
|
|
|
export class Cmp {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.write('mod.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {Cmp} from './cmp';
|
|
|
|
import {Dir} from './dir';
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Cmp, Dir],
|
|
|
|
})
|
|
|
|
export class Mod {}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
env.write('dir.ts', `
|
|
|
|
import {Directive, Input} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({
|
|
|
|
selector: '[dir]',
|
|
|
|
})
|
|
|
|
export class Dir {
|
|
|
|
@Input()
|
|
|
|
dirIn_changed!: string;
|
|
|
|
}
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.write('other.ts', `
|
|
|
|
export const FOO = ;
|
|
|
|
`);
|
|
|
|
expect(env.driveDiagnostics().length).not.toBe(0);
|
|
|
|
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
env.write('other.ts', `
|
|
|
|
export const FOO = false;
|
|
|
|
`);
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
expectToHaveWritten([
|
|
|
|
// Mod is written as its direct dependency (Dir) was changed.
|
|
|
|
'/mod.js',
|
|
|
|
|
|
|
|
// Dir is written as it was directly changed.
|
|
|
|
'/dir.js',
|
|
|
|
|
|
|
|
// other.js is written as it was directly changed.
|
|
|
|
'/other.js',
|
|
|
|
|
|
|
|
// Cmp is written as Dir's public API has changed.
|
|
|
|
'/cmp.js',
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|