Skip to content

Commit

Permalink
fix(compiler-cli): do not persist component analysis if template/styl…
Browse files Browse the repository at this point in the history
…es are missing (#49184)

Consider the following scenario:

1. A TS file with a component and templateUrl exists
2. The template file does not exist.
3. First build: ngtsc will properly report the error, via a FatalDiagnosticError
4. The template file is now created
5. Second build: ngtsc still reports the same errror.

ngtsc persists the analysis data of the component and never invalidates
it when the template/style file becomes available later.

This breaks incremental builds and potentially common workflows
where resource files are added later after the TS file is created. This
did surface as an issue in the Angular CLI yet because Webpack requires
users to re-start the process when a new file is added. With ESBuild
this will change and this also breaks incremental builds with
Bazel/Blaze workers.

To fix this, we have a few options:

* Invalidate the analysis when e.g. the template file is missing. Never
  caching it means that it will be re-analyzed on every build iteration.
* Add the resource dependency to ngtsc's incremental file graph. ngtsc
  will then know via `host.getModifiedResources` when the file becomes
  available- and fresh analysis of component would occur.

The first approach is straightforward to implement and was chosen here.
The second approach would allow ngtsc to re-use more of the analysis
when we know that e.g. the template file still not there, but it
increases complexity unnecessarily because there is no **single**
obvious resource path for e.g. a `templateUrl`. The URL is attempted
to be resolved using multiple strategies, such as TS program root dirs,
or there is support for a custom resolution through
`host.resourceNameToFileName`.

It would be possible to determine some candidate paths and add them to
the dependency tracker, but it seems incomplete given possible external
resolvers like `resourceNameToFileName` and also would likely not have
a sufficient-enough impact given that a broken component decorator is
not expected to remain for too long between N incremental build
iterations.

PR Close #49184
  • Loading branch information
devversion authored and AndrewKushnir committed Feb 24, 2023
1 parent 92b0bda commit 04d8b6c
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -318,8 +318,8 @@ export class ComponentDecoratorHandler implements
template = preanalyzed;
} else {
const templateDecl = parseTemplateDeclaration(
decorator, component, containingFile, this.evaluator, this.resourceLoader,
this.defaultPreserveWhitespaces);
node, decorator, component, containingFile, this.evaluator, this.depTracker,
this.resourceLoader, this.defaultPreserveWhitespaces);
template = extractTemplate(
node, templateDecl, this.evaluator, this.depTracker, this.resourceLoader, {
enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat,
Expand Down Expand Up @@ -353,6 +353,13 @@ export class ComponentDecoratorHandler implements
this.depTracker.addResourceDependency(node.getSourceFile(), absoluteFrom(resourceUrl));
}
} catch {
if (this.depTracker !== null) {
// The analysis of this file cannot be re-used if one of the style URLs could
// not be resolved or loaded. Future builds should re-analyze and re-attempt
// resolution/loading.
this.depTracker.recordDependencyAnalysisFailure(node.getSourceFile());
}

if (diagnostics === undefined) {
diagnostics = [];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,9 +262,9 @@ function parseExtractedTemplate(
}

export function parseTemplateDeclaration(
decorator: Decorator, component: Map<string, ts.Expression>, containingFile: string,
evaluator: PartialEvaluator, resourceLoader: ResourceLoader,
defaultPreserveWhitespaces: boolean): TemplateDeclaration {
node: ClassDeclaration, decorator: Decorator, component: Map<string, ts.Expression>,
containingFile: string, evaluator: PartialEvaluator, depTracker: DependencyTracker|null,
resourceLoader: ResourceLoader, defaultPreserveWhitespaces: boolean): TemplateDeclaration {
let preserveWhitespaces: boolean = defaultPreserveWhitespaces;
if (component.has('preserveWhitespaces')) {
const expr = component.get('preserveWhitespaces')!;
Expand Down Expand Up @@ -305,6 +305,12 @@ export function parseTemplateDeclaration(
resolvedTemplateUrl: resourceUrl,
};
} catch (e) {
if (depTracker !== null) {
// The analysis of this file cannot be re-used if the template URL could
// not be resolved. Future builds should re-analyze and re-attempt resolution.
depTracker.recordDependencyAnalysisFailure(node.getSourceFile());
}

throw makeResourceNotFoundError(
templateUrl, templateUrlExpr, ResourceTypeForDiagnostics.Template);
}
Expand Down Expand Up @@ -348,7 +354,7 @@ export function preloadAndParseTemplate(
if (templatePromise !== undefined) {
return templatePromise.then(() => {
const templateDecl = parseTemplateDeclaration(
decorator, component, containingFile, evaluator, resourceLoader,
node, decorator, component, containingFile, evaluator, depTracker, resourceLoader,
defaultPreserveWhitespaces);
const template =
extractTemplate(node, templateDecl, evaluator, depTracker, resourceLoader, options);
Expand All @@ -359,12 +365,18 @@ export function preloadAndParseTemplate(
return Promise.resolve(null);
}
} catch (e) {
if (depTracker !== null) {
// The analysis of this file cannot be re-used if the template URL could
// not be resolved. Future builds should re-analyze and re-attempt resolution.
depTracker.recordDependencyAnalysisFailure(node.getSourceFile());
}

throw makeResourceNotFoundError(
templateUrl, templateUrlExpr, ResourceTypeForDiagnostics.Template);
}
} else {
const templateDecl = parseTemplateDeclaration(
decorator, component, containingFile, evaluator, resourceLoader,
node, decorator, component, containingFile, evaluator, depTracker, resourceLoader,
defaultPreserveWhitespaces);
const template =
extractTemplate(node, templateDecl, evaluator, depTracker, resourceLoader, options);
Expand Down
45 changes: 45 additions & 0 deletions packages/compiler-cli/test/ngtsc/incremental_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {ErrorCode, ngErrorCode} from '../../src/ngtsc/diagnostics';
import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing';
import {loadStandardTestFiles} from '../../src/ngtsc/testing';

Expand Down Expand Up @@ -945,6 +946,50 @@ runInEachFileSystem(() => {
});
});
});

describe('missing resource files', () => {
it('should re-analyze a component if a template file becomes available later', () => {
env.write('app.ts', `
import {Component} from '@angular/core';
@Component({
selector: 'app',
templateUrl: './some-template.html',
})
export class AppComponent {}
`);

const firstDiagnostics = env.driveDiagnostics();
expect(firstDiagnostics.length).toBe(1);
expect(firstDiagnostics[0].code).toBe(ngErrorCode(ErrorCode.COMPONENT_RESOURCE_NOT_FOUND));

env.write('some-template.html', `
<span>Test</span>
`);

env.driveMain();
});

it('should re-analyze if component style file becomes available later', () => {
env.write('app.ts', `
import {Component} from '@angular/core';
@Component({
selector: 'app',
template: 'Works',
styleUrls: ['./some-style.css'],
})
export class AppComponent {}
`);

const firstDiagnostics = env.driveDiagnostics();
expect(firstDiagnostics.length).toBe(1);
expect(firstDiagnostics[0].code).toBe(ngErrorCode(ErrorCode.COMPONENT_RESOURCE_NOT_FOUND));

env.write('some-style.css', `body {}`);
env.driveMain();
});
});
});

function setupFooBarProgram(env: NgtscTestEnvironment) {
Expand Down

0 comments on commit 04d8b6c

Please sign in to comment.