mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
d48cfbca75
When an external template is read, adds the template file to to the project which contains. This is necessary to keep the projects open when navigating away from HTML files. Since a `tsconfig` cannot express including non-TS files, we need another way to indicate the template files are considered part of the project. Note that this does not ensure that the project in question _directly_ contains the component file. That is, the project might just include the component file through the program rather than directly in the `include` glob of the `tsconfig`. This distinction is somewhat important because the TypeScript language service/server prefers projects which _directly_ contain the TS file (see `projectContainsInfoDirectly` in the TS codebase). What this means it that there can possibly be a different project used between the TS and HTML files. For example, in Nx projects, the referenced configs are `tsconfig.app.json` and `tsconfig.editor.json`. `tsconfig.app.json` comes first in the base `tsconfig.json` and contains the entry point of the app. `tsconfig.editor.json` contains the `**.ts` glob of all TS files. This means that `tsconfig.editor.json` will be preferred by the TS server for TS files but the `tsconfig.app.json` will be used for HTML files since it comes first and we cannot effectively express `projectContainsInfoDirectly` for HTML files. We could consider also updating the language server implementation to attempt to select the project to use for the template file based on which project contains its component file directly, using either the internal `project.projectContainsInfoDirectly` or as a workaround, check `project.isRoot(componentTsFile)`. Finally, keeping the projects open is hugely important in the solution style config case like Nx. When a TS file is opened, TypeScript will only retain `tsconfig.editor.json` and not `tsconfig.app.json`. However, if our extension does not also know to select `tsconfig.editor.json`, it will automatically select `tsconfig.app.json` since it is defined first in the `tsconfig.json` file. So we need to teach TS server that we are (1) interested in keeping projects open when there is an HTML file open and (2) optionally attempt to do this _only_ for projects that we know the TS language service will prioritize in TS files (i.e., attempt to only keep `tsconfig.editor.json` open and allow `tsconfig.app.json` to close) and prioritize that project for all requests. fixes https://github.com/angular/vscode-ng-language-service/issues/1623 fixes https://github.com/angular/vscode-ng-language-service/issues/876 PR Close #45601
185 lines
6.7 KiB
TypeScript
185 lines
6.7 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.io/license
|
|
*/
|
|
|
|
/** @fileoverview provides adapters for communicating with the ng compiler */
|
|
|
|
import {ConfigurationHost} from '@angular/compiler-cli';
|
|
import {NgCompilerAdapter} from '@angular/compiler-cli/src/ngtsc/core/api';
|
|
import {AbsoluteFsPath, FileStats, PathSegment, PathString} from '@angular/compiler-cli/src/ngtsc/file_system';
|
|
import {isShim} from '@angular/compiler-cli/src/ngtsc/shims';
|
|
import {getRootDirs} from '@angular/compiler-cli/src/ngtsc/util/src/typescript';
|
|
import * as p from 'path';
|
|
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[] = [];
|
|
readonly ignoreForEmit: Set<ts.SourceFile> = new Set();
|
|
readonly factoryTracker = null; // no .ngfactory shims
|
|
readonly unifiedModulesHost = null; // only used in Bazel
|
|
readonly rootDirs: AbsoluteFsPath[];
|
|
|
|
/**
|
|
* Map of resource filenames to the version of the file last read via `readResource`.
|
|
*
|
|
* Used to implement `getModifiedResourceFiles`.
|
|
*/
|
|
private readonly lastReadResourceVersion = new Map<string, string>();
|
|
|
|
constructor(private readonly project: ts.server.Project) {
|
|
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);
|
|
}
|
|
|
|
isResource(sf: ts.SourceFile): boolean {
|
|
const scriptInfo = this.project.getScriptInfo(sf.fileName);
|
|
return scriptInfo?.scriptKind === ts.ScriptKind.Unknown;
|
|
}
|
|
|
|
fileExists(fileName: string): boolean {
|
|
return this.project.fileExists(fileName);
|
|
}
|
|
|
|
readFile(fileName: string): string|undefined {
|
|
return this.project.readFile(fileName);
|
|
}
|
|
|
|
getCurrentDirectory(): string {
|
|
return this.project.getCurrentDirectory();
|
|
}
|
|
|
|
getCanonicalFileName(fileName: string): string {
|
|
return this.project.projectService.toCanonicalFileName(fileName);
|
|
}
|
|
|
|
/**
|
|
* Return the real path of a symlink. This method is required in order to
|
|
* resolve symlinks in node_modules.
|
|
*/
|
|
realpath(path: string): string {
|
|
return this.project.realpath?.(path) ?? path;
|
|
}
|
|
|
|
/**
|
|
* readResource() is an Angular-specific method for reading files that are not
|
|
* managed by the TS compiler host, namely templates and stylesheets.
|
|
* It is a method on ExtendedTsCompilerHost, see
|
|
* packages/compiler-cli/src/ngtsc/core/api/src/interfaces.ts
|
|
*/
|
|
readResource(fileName: string): string {
|
|
if (isTypeScriptFile(fileName)) {
|
|
throw new Error(`readResource() should not be called on TS file: ${fileName}`);
|
|
}
|
|
// Calling getScriptSnapshot() will actually create a ScriptInfo if it does
|
|
// not exist! The same applies for getScriptVersion().
|
|
// getScriptInfo() will not create one if it does not exist.
|
|
// In this case, we *want* a script info to be created so that we could
|
|
// keep track of its version.
|
|
const version = this.project.getScriptVersion(fileName);
|
|
this.lastReadResourceVersion.set(fileName, version);
|
|
const scriptInfo = this.project.getScriptInfo(fileName);
|
|
if (!scriptInfo) {
|
|
// // This should not happen because it would have failed already at `getScriptVersion`.
|
|
throw new Error(`Failed to get script info when trying to read ${fileName}`);
|
|
}
|
|
// Add external resources as root files to the project since we project language service
|
|
// features for them (this is currently only the case for HTML files, but we could investigate
|
|
// css file features in the future). This prevents the project from being closed when navigating
|
|
// away from a resource file.
|
|
if (!this.project.isRoot(scriptInfo)) {
|
|
this.project.addRoot(scriptInfo);
|
|
}
|
|
const snapshot = scriptInfo.getSnapshot();
|
|
return snapshot.getText(0, snapshot.getLength());
|
|
}
|
|
|
|
getModifiedResourceFiles(): Set<string>|undefined {
|
|
const modifiedFiles = new Set<string>();
|
|
for (const [fileName, oldVersion] of this.lastReadResourceVersion) {
|
|
if (this.project.getScriptVersion(fileName) !== oldVersion) {
|
|
modifiedFiles.add(fileName);
|
|
}
|
|
}
|
|
return modifiedFiles.size > 0 ? modifiedFiles : undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Used to read configuration files.
|
|
*
|
|
* A language service parse configuration host is independent of the adapter
|
|
* because signatures of calls like `FileSystem#readFile` are a bit stricter
|
|
* than those on the adapter.
|
|
*/
|
|
export class LSParseConfigHost implements ConfigurationHost {
|
|
constructor(private readonly serverHost: ts.server.ServerHost) {}
|
|
exists(path: AbsoluteFsPath): boolean {
|
|
return this.serverHost.fileExists(path) || this.serverHost.directoryExists(path);
|
|
}
|
|
readFile(path: AbsoluteFsPath): string {
|
|
const content = this.serverHost.readFile(path);
|
|
if (content === undefined) {
|
|
throw new Error(`LanguageServiceFS#readFile called on unavailable file ${path}`);
|
|
}
|
|
return content;
|
|
}
|
|
lstat(path: AbsoluteFsPath): FileStats {
|
|
return {
|
|
isFile: () => {
|
|
return this.serverHost.fileExists(path);
|
|
},
|
|
isDirectory: () => {
|
|
return this.serverHost.directoryExists(path);
|
|
},
|
|
isSymbolicLink: () => {
|
|
throw new Error(`LanguageServiceFS#lstat#isSymbolicLink not implemented`);
|
|
},
|
|
};
|
|
}
|
|
pwd(): AbsoluteFsPath {
|
|
return this.serverHost.getCurrentDirectory() as AbsoluteFsPath;
|
|
}
|
|
extname(path: AbsoluteFsPath|PathSegment): string {
|
|
return p.extname(path);
|
|
}
|
|
resolve(...paths: string[]): AbsoluteFsPath {
|
|
return p.resolve(...paths) as AbsoluteFsPath;
|
|
}
|
|
dirname<T extends PathString>(file: T): T {
|
|
return p.dirname(file) as T;
|
|
}
|
|
join<T extends PathString>(basePath: T, ...paths: string[]): T {
|
|
return p.join(basePath, ...paths) as T;
|
|
}
|
|
}
|