2019-03-18 15:25:26 -04:00
|
|
|
/**
|
|
|
|
* @license
|
|
|
|
* Copyright Google Inc. 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
|
|
|
|
*/
|
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing';
|
|
|
|
import {loadStandardTestFiles} from '../helpers/src/mock_file_loading';
|
2019-06-10 11:22:56 -04:00
|
|
|
|
2019-03-18 15:25:26 -04:00
|
|
|
import {NgtscTestEnvironment} from './env';
|
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
const testFiles = loadStandardTestFiles();
|
|
|
|
|
|
|
|
runInEachFileSystem(() => {
|
|
|
|
describe('ngtsc incremental compilation', () => {
|
|
|
|
let env !: NgtscTestEnvironment;
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
env = NgtscTestEnvironment.setup(testFiles);
|
|
|
|
env.enableMultipleCompilations();
|
|
|
|
env.tsconfig();
|
|
|
|
});
|
2019-03-18 15:25:26 -04:00
|
|
|
|
2019-06-27 19:25:00 -04:00
|
|
|
it('should not crash if CLI does not provide getModifiedResourceFiles()', () => {
|
|
|
|
env.write('component1.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({selector: 'cmp', templateUrl: './component1.template.html'})
|
|
|
|
export class Cmp1 {}
|
|
|
|
`);
|
|
|
|
env.write('component1.template.html', 'cmp1');
|
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
// Simulate a change to `component1.html`
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
env.invalidateCachedFile('component1.html');
|
|
|
|
env.simulateLegacyCLICompilerHost();
|
|
|
|
env.driveMain();
|
|
|
|
});
|
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
it('should skip unchanged services', () => {
|
|
|
|
env.write('service.ts', `
|
2019-03-18 15:25:26 -04:00
|
|
|
import {Injectable} from '@angular/core';
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
export class Service {}
|
|
|
|
`);
|
2019-06-06 15:22:32 -04:00
|
|
|
env.write('test.ts', `
|
2019-03-18 15:25:26 -04:00
|
|
|
import {Component} from '@angular/core';
|
|
|
|
import {Service} from './service';
|
|
|
|
|
|
|
|
@Component({selector: 'cmp', template: 'cmp'})
|
|
|
|
export class Cmp {
|
|
|
|
constructor(service: Service) {}
|
|
|
|
}
|
|
|
|
`);
|
2019-06-06 15:22:32 -04:00
|
|
|
env.driveMain();
|
|
|
|
env.flushWrittenFileTracking();
|
2019-03-18 15:25:26 -04:00
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
// Pretend a change was made to test.ts.
|
|
|
|
env.invalidateCachedFile('test.ts');
|
|
|
|
env.driveMain();
|
|
|
|
const written = env.getFilesWrittenSinceLastFlush();
|
2019-03-18 15:25:26 -04:00
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
// The changed file should be recompiled, but not the service.
|
|
|
|
expect(written).toContain('/test.js');
|
|
|
|
expect(written).not.toContain('/service.js');
|
|
|
|
});
|
2019-04-24 13:26:47 -04:00
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
it('should rebuild components that have changed', () => {
|
|
|
|
env.write('component1.ts', `
|
2019-05-08 10:10:50 -04:00
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({selector: 'cmp', template: 'cmp'})
|
|
|
|
export class Cmp1 {}
|
|
|
|
`);
|
2019-06-06 15:22:32 -04:00
|
|
|
env.write('component2.ts', `
|
2019-05-08 10:10:50 -04:00
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({selector: 'cmp2', template: 'cmp'})
|
|
|
|
export class Cmp2 {}
|
|
|
|
`);
|
2019-06-06 15:22:32 -04:00
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
// Pretend a change was made to Cmp1
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
env.invalidateCachedFile('component1.ts');
|
|
|
|
env.driveMain();
|
|
|
|
const written = env.getFilesWrittenSinceLastFlush();
|
|
|
|
expect(written).toContain('/component1.js');
|
|
|
|
expect(written).not.toContain('/component2.js');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should rebuild components whose templates have changed', () => {
|
|
|
|
env.write('component1.ts', `
|
2019-06-10 11:22:56 -04:00
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({selector: 'cmp', templateUrl: './component1.template.html'})
|
|
|
|
export class Cmp1 {}
|
|
|
|
`);
|
2019-06-06 15:22:32 -04:00
|
|
|
env.write('component1.template.html', 'cmp1');
|
|
|
|
env.write('component2.ts', `
|
2019-06-10 11:22:56 -04:00
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({selector: 'cmp2', templateUrl: './component2.template.html'})
|
|
|
|
export class Cmp2 {}
|
|
|
|
`);
|
2019-06-06 15:22:32 -04:00
|
|
|
env.write('component2.template.html', 'cmp2');
|
2019-06-10 11:22:56 -04:00
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
env.driveMain();
|
2019-06-10 11:22:56 -04:00
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
// Make a change to Cmp1 template
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
env.write('component1.template.html', 'changed');
|
|
|
|
env.driveMain();
|
|
|
|
const written = env.getFilesWrittenSinceLastFlush();
|
|
|
|
expect(written).toContain('/component1.js');
|
|
|
|
expect(written).not.toContain('/component2.js');
|
|
|
|
});
|
2019-06-10 11:22:56 -04:00
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
it('should rebuild components whose partial-evaluation dependencies have changed', () => {
|
|
|
|
env.write('component1.ts', `
|
2019-05-08 10:10:50 -04:00
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({selector: 'cmp', template: 'cmp'})
|
|
|
|
export class Cmp1 {}
|
|
|
|
`);
|
2019-06-06 15:22:32 -04:00
|
|
|
env.write('component2.ts', `
|
2019-05-08 10:10:50 -04:00
|
|
|
import {Component} from '@angular/core';
|
|
|
|
import {SELECTOR} from './constants';
|
|
|
|
|
|
|
|
@Component({selector: SELECTOR, template: 'cmp'})
|
|
|
|
export class Cmp2 {}
|
|
|
|
`);
|
2019-06-06 15:22:32 -04:00
|
|
|
env.write('constants.ts', `
|
2019-05-08 10:10:50 -04:00
|
|
|
export const SELECTOR = 'cmp';
|
|
|
|
`);
|
2019-06-06 15:22:32 -04:00
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
// Pretend a change was made to SELECTOR
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
env.invalidateCachedFile('constants.ts');
|
|
|
|
env.driveMain();
|
|
|
|
const written = env.getFilesWrittenSinceLastFlush();
|
|
|
|
expect(written).toContain('/constants.js');
|
|
|
|
expect(written).not.toContain('/component1.js');
|
|
|
|
expect(written).toContain('/component2.js');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should rebuild components whose imported dependencies have changed', () => {
|
|
|
|
setupFooBarProgram(env);
|
|
|
|
|
|
|
|
// Pretend a change was made to BarDir.
|
|
|
|
env.invalidateCachedFile('bar_directive.ts');
|
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
let written = env.getFilesWrittenSinceLastFlush();
|
|
|
|
expect(written).toContain('/bar_directive.js');
|
|
|
|
expect(written).toContain('/bar_component.js');
|
|
|
|
expect(written).toContain('/bar_module.js');
|
|
|
|
expect(written).not.toContain('/foo_component.js');
|
|
|
|
expect(written).not.toContain('/foo_pipe.js');
|
|
|
|
expect(written).not.toContain('/foo_module.js');
|
|
|
|
});
|
|
|
|
|
fix(ivy): ensure module scope is rebuild on dependent change (#33522)
During incremental compilations, ngtsc needs to know which metadata
from a previous compilation can be reused, versus which metadata has to
be recomputed as some dependency was updated. Changes to
directives/components should cause the NgModule in which they are
declared to be recompiled, as the NgModule's compilation is dependent
on its directives/components.
When a dependent source file of a directive/component is updated,
however, a more subtle dependency should also cause to NgModule's source
file to be invalidated. During the reconciliation of state from a
previous compilation into the new program, the component's source file
is invalidated because one of its dependency has changed, ergo the
NgModule needs to be invalidated as well. Up until now, this implicit
dependency was not imposed on the NgModule. Additionally, any change to
a dependent file may influence the module scope to change, so all
components within the module must be invalidated as well.
This commit fixes the bug by introducing additional file dependencies,
as to ensure a proper rebuild of the module scope and its components.
Fixes #32416
PR Close #33522
2019-10-31 15:30:05 -04:00
|
|
|
// https://github.com/angular/angular/issues/32416
|
|
|
|
it('should rebuild full NgModule scope when a dependency of a declaration has changed', () => {
|
|
|
|
env.write('component1.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
import {SELECTOR} from './dep';
|
|
|
|
|
|
|
|
@Component({selector: SELECTOR, template: 'cmp'})
|
|
|
|
export class Cmp1 {}
|
|
|
|
`);
|
|
|
|
env.write('component2.ts', `
|
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
|
|
|
@Component({selector: 'cmp2', template: 'cmp2'})
|
|
|
|
export class Cmp2 {}
|
|
|
|
`);
|
|
|
|
env.write('dep.ts', `
|
|
|
|
export const SELECTOR = 'cmp';
|
|
|
|
`);
|
|
|
|
env.write('directive.ts', `
|
|
|
|
import {Directive} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({selector: 'dir'})
|
|
|
|
export class Dir {}
|
|
|
|
`);
|
|
|
|
env.write('pipe.ts', `
|
|
|
|
import {Pipe} from '@angular/core';
|
|
|
|
|
|
|
|
@Pipe({name: 'myPipe'})
|
|
|
|
export class MyPipe {}
|
|
|
|
`);
|
|
|
|
env.write('module.ts', `
|
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {Cmp1} from './component1';
|
|
|
|
import {Cmp2} from './component2';
|
|
|
|
import {Dir} from './directive';
|
|
|
|
import {MyPipe} from './pipe';
|
|
|
|
|
|
|
|
@NgModule({declarations: [Cmp1, Cmp2, Dir, MyPipe]})
|
|
|
|
export class Mod {}
|
|
|
|
`);
|
|
|
|
env.driveMain();
|
|
|
|
|
|
|
|
// Pretend a change was made to 'dep'. Since this may affect the NgModule scope, like it does
|
|
|
|
// here if the selector is updated, all components in the module scope need to be recompiled.
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
env.invalidateCachedFile('dep.ts');
|
|
|
|
env.driveMain();
|
|
|
|
const written = env.getFilesWrittenSinceLastFlush();
|
|
|
|
expect(written).not.toContain('/directive.js');
|
|
|
|
expect(written).not.toContain('/pipe.js');
|
|
|
|
expect(written).toContain('/component1.js');
|
|
|
|
expect(written).toContain('/component2.js');
|
|
|
|
expect(written).toContain('/dep.js');
|
|
|
|
expect(written).toContain('/module.js');
|
|
|
|
});
|
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
it('should rebuild components where their NgModule declared dependencies have changed', () => {
|
|
|
|
setupFooBarProgram(env);
|
|
|
|
|
|
|
|
// Pretend a change was made to FooPipe.
|
|
|
|
env.invalidateCachedFile('foo_pipe.ts');
|
|
|
|
env.driveMain();
|
|
|
|
const written = env.getFilesWrittenSinceLastFlush();
|
|
|
|
expect(written).not.toContain('/bar_directive.js');
|
|
|
|
expect(written).not.toContain('/bar_component.js');
|
|
|
|
expect(written).not.toContain('/bar_module.js');
|
|
|
|
expect(written).toContain('/foo_component.js');
|
|
|
|
expect(written).toContain('/foo_pipe.js');
|
|
|
|
expect(written).toContain('/foo_module.js');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should rebuild components where their NgModule has changed', () => {
|
|
|
|
setupFooBarProgram(env);
|
|
|
|
|
|
|
|
// Pretend a change was made to FooPipe.
|
|
|
|
env.invalidateCachedFile('foo_module.ts');
|
|
|
|
env.driveMain();
|
|
|
|
const written = env.getFilesWrittenSinceLastFlush();
|
|
|
|
expect(written).not.toContain('/bar_directive.js');
|
|
|
|
expect(written).not.toContain('/bar_component.js');
|
|
|
|
expect(written).not.toContain('/bar_module.js');
|
|
|
|
expect(written).toContain('/foo_component.js');
|
|
|
|
expect(written).toContain('/foo_pipe.js');
|
|
|
|
expect(written).toContain('/foo_module.js');
|
|
|
|
});
|
|
|
|
|
2019-07-31 12:11:33 -04:00
|
|
|
it('should rebuild only a Component (but with the correct CompilationScope) if its template has changed',
|
|
|
|
() => {
|
|
|
|
setupFooBarProgram(env);
|
|
|
|
|
|
|
|
// Make a change to the template of BarComponent.
|
|
|
|
env.write('bar_component.html', '<div bar>changed</div>');
|
|
|
|
|
|
|
|
env.driveMain();
|
|
|
|
const written = env.getFilesWrittenSinceLastFlush();
|
|
|
|
expect(written).not.toContain('/bar_directive.js');
|
|
|
|
expect(written).toContain('/bar_component.js');
|
|
|
|
expect(written).not.toContain('/bar_module.js');
|
|
|
|
expect(written).not.toContain('/foo_component.js');
|
|
|
|
expect(written).not.toContain('/foo_pipe.js');
|
|
|
|
expect(written).not.toContain('/foo_module.js');
|
|
|
|
// Ensure that the used directives are included in the component's generated template.
|
|
|
|
expect(env.getContents('/built/bar_component.js')).toMatch(/directives:\s*\[.+\.BarDir\]/);
|
|
|
|
});
|
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
it('should rebuild everything if a typings file changes', () => {
|
|
|
|
setupFooBarProgram(env);
|
|
|
|
|
|
|
|
// Pretend a change was made to a typings file.
|
|
|
|
env.invalidateCachedFile('foo_selector.d.ts');
|
|
|
|
env.driveMain();
|
|
|
|
const written = env.getFilesWrittenSinceLastFlush();
|
|
|
|
expect(written).toContain('/bar_directive.js');
|
|
|
|
expect(written).toContain('/bar_component.js');
|
|
|
|
expect(written).toContain('/bar_module.js');
|
|
|
|
expect(written).toContain('/foo_component.js');
|
|
|
|
expect(written).toContain('/foo_pipe.js');
|
|
|
|
expect(written).toContain('/foo_module.js');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should compile incrementally with template type-checking turned on', () => {
|
|
|
|
env.tsconfig({ivyTemplateTypeCheck: true});
|
|
|
|
env.write('main.ts', 'export class Foo {}');
|
|
|
|
env.driveMain();
|
|
|
|
env.invalidateCachedFile('main.ts');
|
|
|
|
env.driveMain();
|
|
|
|
// If program reuse were configured incorrectly (as was responsible for
|
|
|
|
// https://github.com/angular/angular/issues/30079), this would have crashed.
|
|
|
|
});
|
2019-07-08 10:50:19 -04:00
|
|
|
|
|
|
|
// https://github.com/angular/angular/pull/26036
|
|
|
|
it('should handle redirected source files', () => {
|
|
|
|
env.tsconfig({fullTemplateTypeCheck: true});
|
|
|
|
|
|
|
|
// This file structure has an identical version of "a" under the root node_modules and inside
|
|
|
|
// of "b". Because their package.json file indicates it is the exact same version of "a",
|
|
|
|
// TypeScript will transform the source file of "node_modules/b/node_modules/a/index.d.ts"
|
|
|
|
// into a redirect to "node_modules/a/index.d.ts". During incremental compilations, we must
|
|
|
|
// assure not to reintroduce "node_modules/b/node_modules/a/index.d.ts" as its redirected
|
|
|
|
// source file, but instead use its original file.
|
|
|
|
env.write('node_modules/a/index.js', `export class ServiceA {}`);
|
|
|
|
env.write('node_modules/a/index.d.ts', `export declare class ServiceA {}`);
|
|
|
|
env.write('node_modules/a/package.json', `{"name": "a", "version": "1.0"}`);
|
|
|
|
env.write('node_modules/b/node_modules/a/index.js', `export class ServiceA {}`);
|
|
|
|
env.write('node_modules/b/node_modules/a/index.d.ts', `export declare class ServiceA {}`);
|
|
|
|
env.write('node_modules/b/node_modules/a/package.json', `{"name": "a", "version": "1.0"}`);
|
|
|
|
env.write('node_modules/b/index.js', `export {ServiceA as ServiceB} from 'a';`);
|
|
|
|
env.write('node_modules/b/index.d.ts', `export {ServiceA as ServiceB} from 'a';`);
|
|
|
|
env.write('test.ts', `
|
|
|
|
import {ServiceA} from 'a';
|
|
|
|
import {ServiceB} from 'b';
|
|
|
|
`);
|
|
|
|
env.driveMain();
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
|
|
|
|
// Pretend a change was made to test.ts. If redirect sources were introduced into the new
|
|
|
|
// program, this would fail due to an assertion failure in TS.
|
|
|
|
env.invalidateCachedFile('test.ts');
|
|
|
|
env.driveMain();
|
|
|
|
});
|
2019-05-08 10:10:50 -04:00
|
|
|
});
|
|
|
|
|
2019-06-06 15:22:32 -04:00
|
|
|
function setupFooBarProgram(env: NgtscTestEnvironment) {
|
|
|
|
env.write('foo_component.ts', `
|
2019-05-08 10:10:50 -04:00
|
|
|
import {Component} from '@angular/core';
|
|
|
|
import {fooSelector} from './foo_selector';
|
|
|
|
|
|
|
|
@Component({selector: fooSelector, template: 'foo'})
|
|
|
|
export class FooCmp {}
|
|
|
|
`);
|
2019-06-06 15:22:32 -04:00
|
|
|
env.write('foo_pipe.ts', `
|
2019-05-08 10:10:50 -04:00
|
|
|
import {Pipe} from '@angular/core';
|
|
|
|
|
|
|
|
@Pipe({name: 'foo'})
|
|
|
|
export class FooPipe {}
|
|
|
|
`);
|
2019-06-06 15:22:32 -04:00
|
|
|
env.write('foo_module.ts', `
|
2019-05-08 10:10:50 -04:00
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {FooCmp} from './foo_component';
|
|
|
|
import {FooPipe} from './foo_pipe';
|
|
|
|
import {BarModule} from './bar_module';
|
|
|
|
@NgModule({
|
|
|
|
declarations: [FooCmp, FooPipe],
|
|
|
|
imports: [BarModule],
|
|
|
|
})
|
|
|
|
export class FooModule {}
|
|
|
|
`);
|
2019-06-06 15:22:32 -04:00
|
|
|
env.write('bar_component.ts', `
|
2019-05-08 10:10:50 -04:00
|
|
|
import {Component} from '@angular/core';
|
|
|
|
|
2019-07-31 12:11:33 -04:00
|
|
|
@Component({selector: 'bar', templateUrl: './bar_component.html'})
|
2019-05-08 10:10:50 -04:00
|
|
|
export class BarCmp {}
|
|
|
|
`);
|
2019-07-31 12:11:33 -04:00
|
|
|
env.write('bar_component.html', '<div bar></div>');
|
2019-06-06 15:22:32 -04:00
|
|
|
env.write('bar_directive.ts', `
|
2019-05-08 10:10:50 -04:00
|
|
|
import {Directive} from '@angular/core';
|
|
|
|
|
|
|
|
@Directive({selector: '[bar]'})
|
|
|
|
export class BarDir {}
|
|
|
|
`);
|
2019-06-06 15:22:32 -04:00
|
|
|
env.write('bar_module.ts', `
|
2019-05-08 10:10:50 -04:00
|
|
|
import {NgModule} from '@angular/core';
|
|
|
|
import {BarCmp} from './bar_component';
|
|
|
|
import {BarDir} from './bar_directive';
|
|
|
|
@NgModule({
|
|
|
|
declarations: [BarCmp, BarDir],
|
|
|
|
exports: [BarCmp],
|
|
|
|
})
|
|
|
|
export class BarModule {}
|
|
|
|
`);
|
2019-06-06 15:22:32 -04:00
|
|
|
env.write('foo_selector.d.ts', `
|
2019-05-08 10:10:50 -04:00
|
|
|
export const fooSelector = 'foo';
|
|
|
|
`);
|
2019-06-06 15:22:32 -04:00
|
|
|
env.driveMain();
|
|
|
|
env.flushWrittenFileTracking();
|
|
|
|
}
|
|
|
|
});
|