diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index 5487ac5ee3790e..5567b563cf9c35 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -11,6 +11,7 @@ import * as ts from 'typescript'; import {Cycle, CycleAnalyzer, CycleHandlingStrategy} from '../../cycles'; import {ErrorCode, FatalDiagnosticError, makeDiagnostic, makeRelatedInformation} from '../../diagnostics'; +import {ComponentResourceNotFoundContext} from '../../diagnostics/src/error'; import {absoluteFrom, relative} from '../../file_system'; import {DefaultImportRecorder, ImportedFile, ModuleResolver, Reference, ReferenceEmitter} from '../../imports'; import {DependencyTracker} from '../../incremental/api'; @@ -322,7 +323,6 @@ export class ComponentDecoratorHandler implements this.literalCache.delete(decorator); let diagnostics: ts.Diagnostic[]|undefined; - let isPoisoned = false; // @Component inherits @Directive, so begin by extracting the @Directive metadata and building // on it. const directiveResult = extractDirectiveMetadata( @@ -420,9 +420,13 @@ export class ComponentDecoratorHandler implements styleUrl.source === ResourceTypeForDiagnostics.StylesheetFromDecorator ? ResourceTypeForDiagnostics.StylesheetFromDecorator : ResourceTypeForDiagnostics.StylesheetFromTemplate; - diagnostics.push( - this.makeResourceNotFoundError(styleUrl.url, styleUrl.nodeForError, resourceType) - .toDiagnostic()); + const diagnostic: ts.DiagnosticWithLocation&ComponentResourceNotFoundContext = { + ...this.makeResourceNotFoundError(styleUrl.url, styleUrl.nodeForError, resourceType) + .toDiagnostic(), + fromFile: containingFile, + url: styleUrl.url, + }; + diagnostics.push(diagnostic); } } @@ -501,7 +505,7 @@ export class ComponentDecoratorHandler implements styles: styleResources, template: templateResource, }, - isPoisoned, + isPoisoned: false, }, diagnostics, }; diff --git a/packages/compiler-cli/src/ngtsc/core/api/src/interfaces.ts b/packages/compiler-cli/src/ngtsc/core/api/src/interfaces.ts index 3c3a2f0179eb17..f6c7d3e84bf363 100644 --- a/packages/compiler-cli/src/ngtsc/core/api/src/interfaces.ts +++ b/packages/compiler-cli/src/ngtsc/core/api/src/interfaces.ts @@ -33,8 +33,13 @@ export interface ResourceHost { /** * Converts a file path for a resource that is used in a source file or another resource * into a filepath. + * + * The optional `fallbackResolve` method can be used as a way to attempt a fallback resolution if + * the implementation's `resourceNameToFileName` resolution fails. */ - resourceNameToFileName(resourceName: string, containingFilePath: string): string|null; + resourceNameToFileName( + resourceName: string, containingFilePath: string, + fallbackResolve?: (url: string, fromFile: string) => string | null): string|null; /** * Load a referenced resource either statically or asynchronously. If the host returns a diff --git a/packages/compiler-cli/src/ngtsc/diagnostics/src/error.ts b/packages/compiler-cli/src/ngtsc/diagnostics/src/error.ts index 0822840a9aa7a3..b05ca522addc2d 100644 --- a/packages/compiler-cli/src/ngtsc/diagnostics/src/error.ts +++ b/packages/compiler-cli/src/ngtsc/diagnostics/src/error.ts @@ -57,3 +57,13 @@ export function makeRelatedInformation( export function isFatalDiagnosticError(err: any): err is FatalDiagnosticError { return err._isFatalDiagnosticError === true; } + +export interface ComponentResourceNotFoundContext { + fromFile: string; + url: string; +} + +export function isResourceNotFoundWithContextError(err: ts.Diagnostic): + err is ts.DiagnosticWithLocation&ComponentResourceNotFoundContext { + return err.code === ngErrorCode(ErrorCode.COMPONENT_RESOURCE_NOT_FOUND) && 'fromFile' in err; +} \ No newline at end of file diff --git a/packages/compiler-cli/src/ngtsc/resource/src/loader.ts b/packages/compiler-cli/src/ngtsc/resource/src/loader.ts index 9db512d65bbafc..ad18b05a6e45e5 100644 --- a/packages/compiler-cli/src/ngtsc/resource/src/loader.ts +++ b/packages/compiler-cli/src/ngtsc/resource/src/loader.ts @@ -46,7 +46,8 @@ export class AdapterResourceLoader implements ResourceLoader { resolve(url: string, fromFile: string): string { let resolvedUrl: string|null = null; if (this.adapter.resourceNameToFileName) { - resolvedUrl = this.adapter.resourceNameToFileName(url, fromFile); + resolvedUrl = this.adapter.resourceNameToFileName( + url, fromFile, (url: string, fromFile: string) => this.fallbackResolve(url, fromFile)); } else { resolvedUrl = this.fallbackResolve(url, fromFile); } diff --git a/packages/language-service/ivy/adapters.ts b/packages/language-service/ivy/adapters.ts index f77ac2b2b88e5b..fad8969c04f496 100644 --- a/packages/language-service/ivy/adapters.ts +++ b/packages/language-service/ivy/adapters.ts @@ -37,6 +37,24 @@ export class LanguageServiceAdapter implements NgCompilerAdapter { this.rootDirs = getRootDirs(this, project.getCompilationSettings()); } + resourceNameToFileName( + url: string, fromFile: string, + fallbackResolve?: (url: string, fromFile: string) => string | null): string|null { + // If we are trying to resolve a `.css` file, see if we can find a pre-compiled file with the + // same name instead. That way, we can provide go-to-definition for the pre-compiled files which + // would generally be the desired behavior. + if (url.endsWith('.css')) { + const styleUrl = p.resolve(fromFile, '..', url); + for (const ext of ['.scss', '.sass', '.less', '.styl']) { + const precompiledFileUrl = styleUrl.replace(/(\.css)$/, ext); + if (this.fileExists(precompiledFileUrl)) { + return precompiledFileUrl; + } + } + } + return fallbackResolve?.(url, fromFile) ?? null; + } + isShim(sf: ts.SourceFile): boolean { return isShim(sf); } diff --git a/packages/language-service/ivy/compiler_factory.ts b/packages/language-service/ivy/compiler_factory.ts index 2c634d1026fc60..69cd10f56ed1e9 100644 --- a/packages/language-service/ivy/compiler_factory.ts +++ b/packages/language-service/ivy/compiler_factory.ts @@ -9,12 +9,10 @@ import {CompilationTicket, freshCompilationTicket, incrementalFromCompilerTicket, NgCompiler, resourceChangeTicket} from '@angular/compiler-cli/src/ngtsc/core'; import {NgCompilerOptions} from '@angular/compiler-cli/src/ngtsc/core/api'; import {TrackedIncrementalBuildStrategy} from '@angular/compiler-cli/src/ngtsc/incremental'; -import {ActivePerfRecorder} from '@angular/compiler-cli/src/ngtsc/perf'; import {TypeCheckingProgramStrategy} from '@angular/compiler-cli/src/ngtsc/typecheck/api'; import * as ts from 'typescript/lib/tsserverlibrary'; import {LanguageServiceAdapter} from './adapters'; -import {isExternalTemplate} from './utils'; /** * Manages the `NgCompiler` instance which backs the language service, updating or replacing it as diff --git a/packages/language-service/ivy/test/definitions_spec.ts b/packages/language-service/ivy/test/definitions_spec.ts index db341d9ab775a5..818c42ac4284b2 100644 --- a/packages/language-service/ivy/test/definitions_spec.ts +++ b/packages/language-service/ivy/test/definitions_spec.ts @@ -152,6 +152,31 @@ describe('definitions', () => { assertFileNames([def, def2], ['dir2.ts', 'dir.ts']); }); + it('should go to the pre-compiled style sheet', () => { + initMockFileSystem('Native'); + const files = { + 'app.ts': ` + import {Component} from '@angular/core'; + + @Component({ + template: '', + styleUrls: ['./style.css'], + }) + export class AppCmp {} + `, + 'style.scss': '', + }; + const env = LanguageServiceTestEnv.setup(); + const project = createModuleAndProjectWithDeclarations(env, 'test', files); + const appFile = project.openFile('app.ts'); + appFile.moveCursorToText(`['./styl¦e.css']`); + const {textSpan, definitions} = getDefinitionsAndAssertBoundSpan(env, appFile); + expect(appFile.contents.substr(textSpan.start, textSpan.length)).toEqual('./style.css'); + + expect(definitions.length).toEqual(1); + assertFileNames(definitions, ['style.scss']); + }); + function getDefinitionsAndAssertBoundSpan(env: LanguageServiceTestEnv, file: OpenBuffer) { env.expectNoSourceDiagnostics(); const definitionAndBoundSpan = file.getDefinitionAndBoundSpan(); diff --git a/packages/language-service/ivy/test/diagnostic_spec.ts b/packages/language-service/ivy/test/diagnostic_spec.ts index 3fadd8f966c356..ce186c3f26a0b6 100644 --- a/packages/language-service/ivy/test/diagnostic_spec.ts +++ b/packages/language-service/ivy/test/diagnostic_spec.ts @@ -297,6 +297,32 @@ describe('getSemanticDiagnostics', () => { .toHaveBeenCalledWith(jasmine.stringMatching( /LanguageService\#LsDiagnostics\:.*\"LsDiagnostics\":\s*\d+.*/g)); }); + + it('ignores style url resource missing', () => { + const files = { + 'app.ts': ` + import {Component} from '@angular/core'; + + @Component({ + template: '', + styleUrls: ['./one.css', './two/two.css', './three.css', '../test/four.css', './missing.css'], + }) + export class MyComponent {} + `, + 'one.scss': '', + 'two/two.sass': '', + 'three.less': '', + 'four.styl': '', + }; + + const project = createModuleAndProjectWithDeclarations(env, 'test', files); + const diags = project.getDiagnosticsForFile('app.ts'); + expect(diags.length).toBe(1); + const diag = diags[0]; + expect(diag.code).toBe(ngErrorCode(ErrorCode.COMPONENT_RESOURCE_NOT_FOUND)); + expect(diag.category).toBe(ts.DiagnosticCategory.Error); + expect(getTextOfDiagnostic(diag)).toBe(`'./missing.css'`); + }); }); function getTextOfDiagnostic(diag: ts.Diagnostic): string {