Skip to content

Commit d48cfbc

Browse files
atscottAndrewKushnir
authored andcommitted
fix(language-service): Add resource files as roots to their associated projects (angular#45601)
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 angular/vscode-ng-language-service#1623 fixes angular/vscode-ng-language-service#876 PR Close angular#45601
1 parent 43ba4ab commit d48cfbc

File tree

9 files changed

+68
-20
lines changed

9 files changed

+68
-20
lines changed

packages/compiler-cli/ngcc/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ ts_library(
2828
"//packages/compiler-cli/src/ngtsc/perf",
2929
"//packages/compiler-cli/src/ngtsc/reflection",
3030
"//packages/compiler-cli/src/ngtsc/scope",
31+
"//packages/compiler-cli/src/ngtsc/shims",
3132
"//packages/compiler-cli/src/ngtsc/sourcemaps",
3233
"//packages/compiler-cli/src/ngtsc/transform",
3334
"//packages/compiler-cli/src/ngtsc/translator",

packages/compiler-cli/ngcc/src/analysis/ngcc_trait_compiler.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {IncrementalBuild} from '../../../src/ngtsc/incremental/api';
1111
import {SemanticSymbol} from '../../../src/ngtsc/incremental/semantic_graph';
1212
import {NOOP_PERF_RECORDER} from '../../../src/ngtsc/perf';
1313
import {ClassDeclaration, Decorator} from '../../../src/ngtsc/reflection';
14+
import {isShim} from '../../../src/ngtsc/shims';
1415
import {CompilationMode, DecoratorHandler, DtsTransformRegistry, HandlerFlags, Trait, TraitCompiler} from '../../../src/ngtsc/transform';
1516
import {NgccReflectionHost} from '../host/ngcc_host';
1617
import {isDefined} from '../utils';
@@ -28,7 +29,7 @@ export class NgccTraitCompiler extends TraitCompiler {
2829
super(
2930
handlers, ngccReflector, NOOP_PERF_RECORDER, new NoIncrementalBuild(),
3031
/* compileNonExportedClasses */ true, CompilationMode.FULL, new DtsTransformRegistry(),
31-
/* semanticDepGraphUpdater */ null);
32+
/* semanticDepGraphUpdater */ null, {isShim, isResource: () => false});
3233
}
3334

3435
get analyzedFiles(): ts.SourceFile[] {

packages/compiler-cli/src/ngtsc/core/api/src/adapter.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ export interface NgCompilerAdapter extends
4343
// incompatible with the `ts.CompilerHost` version which isn't. The combination of these two
4444
// still satisfies `ts.ModuleResolutionHost`.
4545
Omit<ts.ModuleResolutionHost, 'getCurrentDirectory'>,
46-
Pick<ExtendedTsCompilerHost, 'getCurrentDirectory'|ExtendedCompilerHostMethods> {
46+
Pick<ExtendedTsCompilerHost, 'getCurrentDirectory'|ExtendedCompilerHostMethods>,
47+
SourceFileTypeIdentifier {
4748
/**
4849
* A path to a single file which represents the entrypoint of an Angular Package Format library,
4950
* if the current program is one.
@@ -86,7 +87,9 @@ export interface NgCompilerAdapter extends
8687
* Resolved list of root directories explicitly set in, or inferred from, the tsconfig.
8788
*/
8889
readonly rootDirs: ReadonlyArray<AbsoluteFsPath>;
90+
}
8991

92+
export interface SourceFileTypeIdentifier {
9093
/**
9194
* Distinguishes between shim files added by Angular to the compilation process (both those
9295
* intended for output, like ngfactory files, as well as internal shims like ngtypecheck files)
@@ -96,4 +99,14 @@ export interface NgCompilerAdapter extends
9699
* `true` if a file was written by the user, and `false` if a file was added by the compiler.
97100
*/
98101
isShim(sf: ts.SourceFile): boolean;
102+
103+
/**
104+
* Distinguishes between resource files added by Angular to the project and original files in the
105+
* user's program.
106+
*
107+
* This is necessary only for the language service because it adds resource files as root files
108+
* when they are read. This is done to indicate to TS Server that these resources are part of the
109+
* project and ensures that projects are retained properly when navigating around the workspace.
110+
*/
111+
isResource(sf: ts.SourceFile): boolean;
99112
}

packages/compiler-cli/src/ngtsc/core/src/compiler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1050,7 +1050,7 @@ export class NgCompiler {
10501050
const traitCompiler = new TraitCompiler(
10511051
handlers, reflector, this.delegatingPerfRecorder, this.incrementalCompilation,
10521052
this.options.compileNonExportedClasses !== false, compilationMode, dtsTransforms,
1053-
semanticDepGraphUpdater);
1053+
semanticDepGraphUpdater, this.adapter);
10541054

10551055
// Template type-checking may use the `ProgramDriver` to produce new `ts.Program`(s). If this
10561056
// happens, they need to be tracked by the `NgCompiler`.

packages/compiler-cli/src/ngtsc/core/src/host.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,16 @@ export class NgCompilerHost extends DelegatingCompilerHost implements
234234
return isShim(sf);
235235
}
236236

237+
/**
238+
* Check whether the given `ts.SourceFile` is a resource file.
239+
*
240+
* This simply returns `false` for the compiler-cli since resource files are not added as root
241+
* files to the project.
242+
*/
243+
isResource(sf: ts.SourceFile): boolean {
244+
return false;
245+
}
246+
237247
getSourceFile(
238248
fileName: string, languageVersion: ts.ScriptTarget,
239249
onError?: ((message: string) => void)|undefined,

packages/compiler-cli/src/ngtsc/transform/src/compilation.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,16 @@
99
import {ConstantPool} from '@angular/compiler';
1010
import ts from 'typescript';
1111

12+
import {SourceFileTypeIdentifier} from '../../core/api';
1213
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
1314
import {IncrementalBuild} from '../../incremental/api';
1415
import {SemanticDepGraphUpdater, SemanticSymbol} from '../../incremental/semantic_graph';
1516
import {IndexingContext} from '../../indexer';
1617
import {PerfEvent, PerfRecorder} from '../../perf';
1718
import {ClassDeclaration, DeclarationNode, Decorator, isNamedClassDeclaration, ReflectionHost} from '../../reflection';
18-
import {isShim} from '../../shims';
1919
import {ProgramTypeCheckAdapter, TypeCheckContext} from '../../typecheck/api';
2020
import {ExtendedTemplateChecker} from '../../typecheck/extended/api';
21-
import {getSourceFile, isExported} from '../../util/src/typescript';
21+
import {getSourceFile} from '../../util/src/typescript';
2222
import {Xi18nContext} from '../../xi18n';
2323

2424
import {AnalysisOutput, CompilationMode, CompileResult, DecoratorHandler, HandlerFlags, HandlerPrecedence, ResolveResult} from './api';
@@ -97,11 +97,15 @@ export class TraitCompiler implements ProgramTypeCheckAdapter {
9797

9898
constructor(
9999
private handlers: DecoratorHandler<unknown, unknown, SemanticSymbol|null, unknown>[],
100-
private reflector: ReflectionHost, private perf: PerfRecorder,
100+
private reflector: ReflectionHost,
101+
private perf: PerfRecorder,
101102
private incrementalBuild: IncrementalBuild<ClassRecord, unknown>,
102-
private compileNonExportedClasses: boolean, private compilationMode: CompilationMode,
103+
private compileNonExportedClasses: boolean,
104+
private compilationMode: CompilationMode,
103105
private dtsTransforms: DtsTransformRegistry,
104-
private semanticDepGraphUpdater: SemanticDepGraphUpdater|null) {
106+
private semanticDepGraphUpdater: SemanticDepGraphUpdater|null,
107+
private sourceFileTypeIdentifier: SourceFileTypeIdentifier,
108+
) {
105109
for (const handler of handlers) {
106110
this.handlersByName.set(handler.name, handler);
107111
}
@@ -118,8 +122,9 @@ export class TraitCompiler implements ProgramTypeCheckAdapter {
118122
private analyze(sf: ts.SourceFile, preanalyze: false): void;
119123
private analyze(sf: ts.SourceFile, preanalyze: true): Promise<void>|undefined;
120124
private analyze(sf: ts.SourceFile, preanalyze: boolean): Promise<void>|undefined {
121-
// We shouldn't analyze declaration files.
122-
if (sf.isDeclarationFile || isShim(sf)) {
125+
// We shouldn't analyze declaration, shim, or resource files.
126+
if (sf.isDeclarationFile || this.sourceFileTypeIdentifier.isShim(sf) ||
127+
this.sourceFileTypeIdentifier.isResource(sf)) {
123128
return undefined;
124129
}
125130

packages/compiler-cli/src/ngtsc/transform/test/compilation_spec.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ import {getDeclaration, makeProgram} from '../../testing';
1717
import {CompilationMode, DetectResult, DtsTransformRegistry, TraitCompiler} from '../../transform';
1818
import {AnalysisOutput, CompileResult, DecoratorHandler, HandlerPrecedence} from '../src/api';
1919

20+
const fakeSfTypeIdentifier = {
21+
isShim: () => false,
22+
isResource: () => false
23+
};
24+
2025
runInEachFileSystem(() => {
2126
describe('TraitCompiler', () => {
2227
let _: typeof absoluteFrom;
@@ -49,7 +54,7 @@ runInEachFileSystem(() => {
4954
const reflectionHost = new TypeScriptReflectionHost(checker);
5055
const compiler = new TraitCompiler(
5156
[new FakeDecoratorHandler()], reflectionHost, NOOP_PERF_RECORDER, NOOP_INCREMENTAL_BUILD,
52-
true, CompilationMode.FULL, new DtsTransformRegistry(), null);
57+
true, CompilationMode.FULL, new DtsTransformRegistry(), null, fakeSfTypeIdentifier);
5358
const sourceFile = program.getSourceFile('lib.d.ts')!;
5459
const analysis = compiler.analyzeSync(sourceFile);
5560

@@ -138,7 +143,7 @@ runInEachFileSystem(() => {
138143
const compiler = new TraitCompiler(
139144
[new PartialDecoratorHandler(), new FullDecoratorHandler()], reflectionHost,
140145
NOOP_PERF_RECORDER, NOOP_INCREMENTAL_BUILD, true, CompilationMode.PARTIAL,
141-
new DtsTransformRegistry(), null);
146+
new DtsTransformRegistry(), null, fakeSfTypeIdentifier);
142147
const sourceFile = program.getSourceFile('test.ts')!;
143148
compiler.analyzeSync(sourceFile);
144149
compiler.resolve();
@@ -168,7 +173,7 @@ runInEachFileSystem(() => {
168173
const compiler = new TraitCompiler(
169174
[new PartialDecoratorHandler(), new FullDecoratorHandler()], reflectionHost,
170175
NOOP_PERF_RECORDER, NOOP_INCREMENTAL_BUILD, true, CompilationMode.FULL,
171-
new DtsTransformRegistry(), null);
176+
new DtsTransformRegistry(), null, fakeSfTypeIdentifier);
172177
const sourceFile = program.getSourceFile('test.ts')!;
173178
compiler.analyzeSync(sourceFile);
174179
compiler.resolve();

packages/language-service/src/adapters.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ export class LanguageServiceAdapter implements NgCompilerAdapter {
6161
return isShim(sf);
6262
}
6363

64+
isResource(sf: ts.SourceFile): boolean {
65+
const scriptInfo = this.project.getScriptInfo(sf.fileName);
66+
return scriptInfo?.scriptKind === ts.ScriptKind.Unknown;
67+
}
68+
6469
fileExists(fileName: string): boolean {
6570
return this.project.fileExists(fileName);
6671
}
@@ -100,14 +105,21 @@ export class LanguageServiceAdapter implements NgCompilerAdapter {
100105
// getScriptInfo() will not create one if it does not exist.
101106
// In this case, we *want* a script info to be created so that we could
102107
// keep track of its version.
103-
const snapshot = this.project.getScriptSnapshot(fileName);
104-
if (!snapshot) {
105-
// This would fail if the file does not exist, or readFile() fails for
106-
// whatever reasons.
107-
throw new Error(`Failed to get script snapshot while trying to read ${fileName}`);
108-
}
109108
const version = this.project.getScriptVersion(fileName);
110109
this.lastReadResourceVersion.set(fileName, version);
110+
const scriptInfo = this.project.getScriptInfo(fileName);
111+
if (!scriptInfo) {
112+
// // This should not happen because it would have failed already at `getScriptVersion`.
113+
throw new Error(`Failed to get script info when trying to read ${fileName}`);
114+
}
115+
// Add external resources as root files to the project since we project language service
116+
// features for them (this is currently only the case for HTML files, but we could investigate
117+
// css file features in the future). This prevents the project from being closed when navigating
118+
// away from a resource file.
119+
if (!this.project.isRoot(scriptInfo)) {
120+
this.project.addRoot(scriptInfo);
121+
}
122+
const snapshot = scriptInfo.getSnapshot();
111123
return snapshot.getText(0, snapshot.getLength());
112124
}
113125

packages/language-service/testing/src/project.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,8 @@ export class Project {
153153
const ngCompiler = this.ngLS.compilerFactory.getOrCreate();
154154

155155
for (const sf of program.getSourceFiles()) {
156-
if (sf.isDeclarationFile || sf.fileName.endsWith('.ngtypecheck.ts')) {
156+
if (sf.isDeclarationFile || sf.fileName.endsWith('.ngtypecheck.ts') ||
157+
!sf.fileName.endsWith('.ts')) {
157158
continue;
158159
}
159160

0 commit comments

Comments
 (0)