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 3c3a2f0179..f6c7d3e84b 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/resource/src/loader.ts b/packages/compiler-cli/src/ngtsc/resource/src/loader.ts index 9db512d65b..ad18b05a6e 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 f77ac2b2b8..fc40367efa 100644 --- a/packages/language-service/ivy/adapters.ts +++ b/packages/language-service/ivy/adapters.ts @@ -18,6 +18,8 @@ import * as ts from 'typescript/lib/tsserverlibrary'; import {isTypeScriptFile} from './utils'; +const PRE_COMPILED_STYLE_EXTENSIONS = ['.scss', '.sass', '.less', '.styl']; + export class LanguageServiceAdapter implements NgCompilerAdapter { readonly entryPoint = null; readonly constructionDiagnostics: ts.Diagnostic[] = []; @@ -37,6 +39,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 PRE_COMPILED_STYLE_EXTENSIONS) { + 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/test/definitions_spec.ts b/packages/language-service/ivy/test/definitions_spec.ts index db341d9ab7..818c42ac42 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 c65aeca52f..3d6f249c4c 100644 --- a/packages/language-service/ivy/test/diagnostic_spec.ts +++ b/packages/language-service/ivy/test/diagnostic_spec.ts @@ -317,6 +317,50 @@ describe('getSemanticDiagnostics', () => { .toHaveBeenCalledWith(jasmine.stringMatching( /LanguageService\#LsDiagnostics\:.*\"LsDiagnostics\":\s*\d+.*/g)); }); + + it('does not produce diagnostics when pre-compiled file is found', () => { + const files = { + 'app.ts': ` + import {Component} from '@angular/core'; + + @Component({ + template: '', + styleUrls: ['./one.css', './two/two.css', './three.css', '../test/four.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(0); + }); + + it('produces missing resource diagnostic for missing css', () => { + const files = { + 'app.ts': ` + import {Component} from '@angular/core'; + + @Component({ + template: '', + styleUrls: ['./missing.css'], + }) + export class MyComponent {} + `, + }; + + 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 {