2319 lines
70 KiB
TypeScript
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',
|
|
]);
|
|
});
|
|
});
|
|
});
|