Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/compiler-cli/src/ngtsc/incremental/src/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ export class IncrementalState implements DependencyTracker, MetadataReader, Meta
metadata.fileDependencies.add(dep);
}

trackFileDependencies(deps: ts.SourceFile[], src: ts.SourceFile) {
const metadata = this.ensureMetadata(src);
for (const dep of deps) {
metadata.fileDependencies.add(dep);
}
}

getFileDependencies(file: ts.SourceFile): ts.SourceFile[] {
if (!this.metadata.has(file)) {
return [];
Expand Down
38 changes: 31 additions & 7 deletions packages/compiler-cli/src/ngtsc/transform/src/compilation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,13 +307,37 @@ export class IvyCompilation {
const recordSpan = this.perf.start('recordDependencies');
this.scopeRegistry !.getCompilationScopes().forEach(scope => {
const file = scope.declaration.getSourceFile();
// Register the file containing the NgModule where the declaration is declared.
this.incrementalState.trackFileDependency(scope.ngModule.getSourceFile(), file);
scope.directives.forEach(
directive =>
this.incrementalState.trackFileDependency(directive.ref.node.getSourceFile(), file));
scope.pipes.forEach(
pipe => this.incrementalState.trackFileDependency(pipe.ref.node.getSourceFile(), file));
const ngModuleFile = scope.ngModule.getSourceFile();

// A change to any dependency of the declaration causes the declaration to be invalidated,
// which requires the NgModule to be invalidated as well.
const deps = this.incrementalState.getFileDependencies(file);
this.incrementalState.trackFileDependencies(deps, ngModuleFile);

// A change to the NgModule file should cause the declaration itself to be invalidated.
this.incrementalState.trackFileDependency(ngModuleFile, file);

// A change to any directive/pipe in the compilation scope should cause the declaration to be
// invalidated.
scope.directives.forEach(directive => {
const dirSf = directive.ref.node.getSourceFile();

// When a directive in scope is updated, the declaration needs to be recompiled as e.g.
// a selector may have changed.
this.incrementalState.trackFileDependency(dirSf, file);

// When any of the dependencies of the declaration changes, the NgModule scope may be
// affected so a component within scope must be recompiled. Only components need to be
// recompiled, as directives are not dependent upon the compilation scope.
if (directive.isComponent) {
this.incrementalState.trackFileDependencies(deps, dirSf);
}
});
scope.pipes.forEach(pipe => {
// When a pipe in scope is updated, the declaration needs to be recompiled as e.g.
// the pipe's name may have changed.
this.incrementalState.trackFileDependency(pipe.ref.node.getSourceFile(), file);
});
});
this.perf.stop(recordSpan);
}
Expand Down
56 changes: 56 additions & 0 deletions packages/compiler-cli/test/ngtsc/incremental_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,62 @@ runInEachFileSystem(() => {
expect(written).not.toContain('/foo_module.js');
});

// 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');
});

it('should rebuild components where their NgModule declared dependencies have changed', () => {
setupFooBarProgram(env);

Expand Down