diff --git a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts index bd41257a64b..681e434f5af 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts @@ -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, @@ -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 = []; } diff --git a/packages/compiler-cli/src/ngtsc/annotations/component/src/resources.ts b/packages/compiler-cli/src/ngtsc/annotations/component/src/resources.ts index 3ebbbc872eb..1d157e96494 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/src/resources.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/src/resources.ts @@ -262,9 +262,9 @@ function parseExtractedTemplate( } export function parseTemplateDeclaration( - decorator: Decorator, component: Map, containingFile: string, - evaluator: PartialEvaluator, resourceLoader: ResourceLoader, - defaultPreserveWhitespaces: boolean): TemplateDeclaration { + node: ClassDeclaration, decorator: Decorator, component: Map, + 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')!; @@ -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); } @@ -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); @@ -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); diff --git a/packages/compiler-cli/test/ngtsc/incremental_spec.ts b/packages/compiler-cli/test/ngtsc/incremental_spec.ts index 4681119d85a..154cdce157b 100644 --- a/packages/compiler-cli/test/ngtsc/incremental_spec.ts +++ b/packages/compiler-cli/test/ngtsc/incremental_spec.ts @@ -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'; @@ -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', ` + Test + `); + + 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) {