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:
parent
bd34bc9e89
commit
de93a7a4bb
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue