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
|
* Converts a file path for a resource that is used in a source file or another resource
|
||||||
* into a filepath.
|
* 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
|
* 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 {
|
resolve(url: string, fromFile: string): string {
|
||||||
let resolvedUrl: string|null = null;
|
let resolvedUrl: string|null = null;
|
||||||
if (this.adapter.resourceNameToFileName) {
|
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 {
|
} else {
|
||||||
resolvedUrl = this.fallbackResolve(url, fromFile);
|
resolvedUrl = this.fallbackResolve(url, fromFile);
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,8 @@ import * as ts from 'typescript/lib/tsserverlibrary';
|
|||||||
|
|
||||||
import {isTypeScriptFile} from './utils';
|
import {isTypeScriptFile} from './utils';
|
||||||
|
|
||||||
|
const PRE_COMPILED_STYLE_EXTENSIONS = ['.scss', '.sass', '.less', '.styl'];
|
||||||
|
|
||||||
export class LanguageServiceAdapter implements NgCompilerAdapter {
|
export class LanguageServiceAdapter implements NgCompilerAdapter {
|
||||||
readonly entryPoint = null;
|
readonly entryPoint = null;
|
||||||
readonly constructionDiagnostics: ts.Diagnostic[] = [];
|
readonly constructionDiagnostics: ts.Diagnostic[] = [];
|
||||||
@ -37,6 +39,24 @@ export class LanguageServiceAdapter implements NgCompilerAdapter {
|
|||||||
this.rootDirs = getRootDirs(this, project.getCompilationSettings());
|
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 {
|
isShim(sf: ts.SourceFile): boolean {
|
||||||
return isShim(sf);
|
return isShim(sf);
|
||||||
}
|
}
|
||||||
|
@ -152,6 +152,31 @@ describe('definitions', () => {
|
|||||||
assertFileNames([def, def2], ['dir2.ts', 'dir.ts']);
|
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) {
|
function getDefinitionsAndAssertBoundSpan(env: LanguageServiceTestEnv, file: OpenBuffer) {
|
||||||
env.expectNoSourceDiagnostics();
|
env.expectNoSourceDiagnostics();
|
||||||
const definitionAndBoundSpan = file.getDefinitionAndBoundSpan();
|
const definitionAndBoundSpan = file.getDefinitionAndBoundSpan();
|
||||||
|
@ -317,6 +317,50 @@ describe('getSemanticDiagnostics', () => {
|
|||||||
.toHaveBeenCalledWith(jasmine.stringMatching(
|
.toHaveBeenCalledWith(jasmine.stringMatching(
|
||||||
/LanguageService\#LsDiagnostics\:.*\"LsDiagnostics\":\s*\d+.*/g));
|
/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 {
|
function getTextOfDiagnostic(diag: ts.Diagnostic): string {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user