/** * @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(); 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(); } `); env.write('a/cmp.ts', ` import {Component} from '@angular/core'; @Component({ selector: 'a-cmp', template: '', }) 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: '', }) 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(); // 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(); } `); 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(); } `); 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: '
', }) 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: '
', }) 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: '
', }) 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: '
', }) 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: '
', }) 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: '
', }) 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(); // 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(); } `); env.write('cmp.ts', ` import {Component} from '@angular/core'; @Component({ selector: 'cmp', template: '
', }) 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(); // 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(); } `); env.write('cmp.ts', ` import {Component} from '@angular/core'; @Component({ selector: 'cmp', template: '
', }) 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: '
', }) 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; } export declare class ExternalMod { static ɵmod: ng.ɵɵNgModuleDefWithMeta; } `); env.write('cmp-a.ts', ` import {Component} from '@angular/core'; @Component({ template: '
', }) export class MyCmpA {} `); env.write('cmp-b.ts', ` import {Component} from '@angular/core'; @Component({ template: '
', }) 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; } export declare class ExternalMod { static ɵmod: ng.ɵɵNgModuleDefWithMeta; } `); env.write('cmp-a.ts', ` import {Component} from '@angular/core'; @Component({ template: '
', }) export class MyCmpA {} `); env.write('cmp-b.ts', ` import {Component} from '@angular/core'; @Component({ template: '
', }) 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: '', }) 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: '', }) 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: '', }) 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: '', }) export class CmpUser {} `); env.write('cmp-dep.ts', ` import {Component} from '@angular/core'; export {CmpDep as CmpDepExport}; @Component({ selector: 'cmp-dep', template: 'Dep', }) class CmpDep {} `); 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', }) class CmpDep {} `); 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: '
', }) 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: '{{1 | dep}}', }) export class CmpUser {} `); env.write('dep.ts', ` import {Directive, Pipe} from '@angular/core'; @Directive({ selector: 'dep', }) export class DepA {} @Pipe({ name: 'dep', }) export class DepB { transform() {} } `); 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', }) export class DepA { transform() {} } @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: '
', }) 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', ``); 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', ``); 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', `Update`); 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', ``); 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', ``); 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', ``); 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', ``); 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', ``); 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', ``); 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', `Update`); 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', ` `); 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', ``); 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', `Update`); 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: '
', }) 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: '
', }) 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: '
', }) 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: '
', }) 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: '
', }) 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: '
', }) 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: '
', }) 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', ]); }); }); });