fix(language-service): resolve to the pre-compiled style when compiled css url is provided (#41538)

With this commit, the language service will first try to locate a
pre-compiled style file with the same name when a `css` is provided in
the `styleUrls`. This prevents a missing resource diagnostic for when the
compiled file is not available in the language service environment and also
allows "go to definition" to go to that pre-compiled file.

Fixes angular/vscode-ng-language-service#1263

PR Close #41538
This commit is contained in:
Andrew Scott 2021-04-09 10:24:08 -07:00 committed by Zach Arend
parent bd34bc9e89
commit de93a7a4bb
5 changed files with 97 additions and 2 deletions

View File

@ -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

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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();

View File

@ -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 {