angular-cn/packages/compiler-cli/test/ngtsc/incremental_semantic_change...

2319 lines
70 KiB
TypeScript

/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {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',
})
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: '<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',
})
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: '<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',
]);
});
});
});