/
githubmirror
/
angular
Обзор
Документация
Войти
/
githubmirror
/
angular
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
vscode-ng-language-service/client/src/client.ts
863 строки
31 KB
Andrew Scott
fix(vscode-extension): resolve relative workspace tsdk paths to absolute
10 июн 2026, 21:09
10 июн 2026, 21:09
fbe807c
Код
Авторство
О чём код?
/** * @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.dev/license */ import * as fs from 'node:fs'; import * as path from 'node:path'; import * as vscode from 'vscode'; import * as lsp from 'vscode-languageclient/node'; import { OpenOutputChannel, ProjectLoadingFinish, ProjectLoadingStart, SuggestStrictMode, SuggestStrictModeParams, } from '../../common/notifications'; import { GetComponentsWithTemplateFile, GetTcbRequest, GetTemplateLocationForComponent, IsInAngularProject, } from '../../common/requests'; import {NodeModule, resolve, Version} from '../../common/resolver'; import { isInsideStringLiteral, isNotTypescriptOrSupportedDecoratorField, isNotTypescriptOrSupportedDecoratorRange, } from './embedded_support'; import {OpenJsDocLinkCommandId} from '../../common/initialize'; interface GetTcbResponse { uri: vscode.Uri; content: string; selections: vscode.Range[]; } export class AngularLanguageClient implements vscode.Disposable { private client: lsp.LanguageClient | null = null; private readonly disposables: vscode.Disposable[] = []; private readonly sessionDisposables: vscode.Disposable[] = []; private readonly outputChannel: vscode.OutputChannel; private readonly clientOptions: lsp.LanguageClientOptions; private readonly name = 'Angular Language Service'; private readonly virtualDocumentContents = new Map<string, string>(); /** A map that indicates whether Angular could be found in the file's project. */ private readonly fileToIsInAngularProjectMap = new Map<string, boolean>(); constructor(private readonly context: vscode.ExtensionContext) { this.disposables.push( vscode.workspace.registerTextDocumentContentProvider('angular-embedded-content', { provideTextDocumentContent: (uri) => { return this.virtualDocumentContents.get(uri.toString()); }, }), ); this.disposables.push( vscode.workspace.onDidChangeConfiguration(() => { this.fileToIsInAngularProjectMap.clear(); }), ); const config = vscode.workspace.getConfiguration(); const useClientSideWatching = config.get('angular.server.useClientSideFileWatcher'); const fileEvents = [ // Notify the server about file changes to tsconfig.json contained in the workspace vscode.workspace.createFileSystemWatcher('**/tsconfig.json'), ]; if (useClientSideWatching) { fileEvents.push(vscode.workspace.createFileSystemWatcher('**/*.ts')); fileEvents.push(vscode.workspace.createFileSystemWatcher('**/*.html')); // While we don't need general JSON watching, TypeScript relies on package.json for module resolution, type acquisition, and auto-imports. // If we don't watch it, npm install changes or dependency updates might be missed by the Language Service fileEvents.push(vscode.workspace.createFileSystemWatcher('**/package.json')); } this.outputChannel = vscode.window.createOutputChannel(this.name); // Options to control the language client this.clientOptions = { // Register the server for Angular templates and TypeScript documents documentSelector: [ // scheme: 'file' means listen to changes to files on disk only // other option is 'untitled', for buffer in the editor (like a new doc) {scheme: 'file', language: 'html'}, {scheme: 'file', language: 'typescript'}, ], synchronize: { configurationSection: ['angular', 'typescript', 'editor.inlayHints'], fileEvents, }, // Don't let our output console pop open revealOutputChannelOn: lsp.RevealOutputChannelOn.Never, outputChannel: this.outputChannel, markdown: { isTrusted: { enabledCommands: [OpenJsDocLinkCommandId], }, }, middleware: { provideCodeActions: async ( document: vscode.TextDocument, range: vscode.Range, context: vscode.CodeActionContext, token: vscode.CancellationToken, next: lsp.ProvideCodeActionsSignature, ) => { // Code actions can trigger also outside of `@Component(<...>)` fields. if (await this.isInAngularProject(document)) { return next(document, range, context, token); } }, prepareRename: async ( document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, next: lsp.PrepareRenameSignature, ) => { // We are able to provide renames for many types of string literals: template strings, // pipe names, and hopefully in the future selectors and input/output aliases. Because // TypeScript isn't able to provide renames for these, we can more or less // guarantee that the Angular Language service will be called for the rename as the // fallback. We specifically do not provide renames outside of string literals // because we cannot ensure our extension is prioritized for renames in TS files (see // https://github.com/microsoft/vscode/issues/115354) we disable renaming completely so we // can provide consistent expectations. if ( (await this.isInAngularProject(document)) && isInsideStringLiteral(document, position) ) { return next(document, position, token); } }, provideDefinition: async ( document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, next: lsp.ProvideDefinitionSignature, ) => { if ( (await this.isInAngularProject(document)) && isNotTypescriptOrSupportedDecoratorField(document, position) ) { return next(document, position, token); } }, provideTypeDefinition: async ( document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, next, ) => { if ( (await this.isInAngularProject(document)) && isNotTypescriptOrSupportedDecoratorField(document, position) ) { return next(document, position, token); } }, provideHover: async ( document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, next: lsp.ProvideHoverSignature, ) => { if ( !(await this.isInAngularProject(document)) || !isNotTypescriptOrSupportedDecoratorField(document, position) ) { return; } const angularResultsPromise = next(document, position, token); // Include results for inline HTML via virtual document and native html providers. if (document.languageId === 'typescript') { const vdocUri = this.createVirtualHtmlDoc(document); const htmlProviderResultsPromise = vscode.commands.executeCommand<vscode.Hover[]>( 'vscode.executeHoverProvider', vdocUri, position, ); const [angularResults, htmlProviderResults] = await Promise.all([ angularResultsPromise, htmlProviderResultsPromise, ]); return angularResults ?? htmlProviderResults?.[0]; } return angularResultsPromise; }, provideSignatureHelp: async ( document: vscode.TextDocument, position: vscode.Position, context: vscode.SignatureHelpContext, token: vscode.CancellationToken, next: lsp.ProvideSignatureHelpSignature, ) => { if ( (await this.isInAngularProject(document)) && isNotTypescriptOrSupportedDecoratorField(document, position) ) { return next(document, position, context, token); } }, provideCompletionItem: async ( document: vscode.TextDocument, position: vscode.Position, context: vscode.CompletionContext, token: vscode.CancellationToken, next: lsp.ProvideCompletionItemsSignature, ) => { // If not in inline template, do not perform request forwarding if ( !(await this.isInAngularProject(document)) || !isNotTypescriptOrSupportedDecoratorField(document, position) ) { return; } const angularCompletionsPromise = next(document, position, context, token) as Promise< vscode.CompletionItem[] | null | undefined >; // Include results for inline HTML via virtual document and native html providers. if (document.languageId === 'typescript') { const vdocUri = this.createVirtualHtmlDoc(document); // This will not include angular stuff because the vdoc is not associated with an // angular component const htmlProviderCompletionsPromise = vscode.commands.executeCommand<vscode.CompletionList>( 'vscode.executeCompletionItemProvider', vdocUri, position, context.triggerCharacter, ); const [angularCompletions, htmlProviderCompletions] = await Promise.all([ angularCompletionsPromise, htmlProviderCompletionsPromise, ]); return [...(angularCompletions ?? []), ...(htmlProviderCompletions?.items ?? [])]; } return angularCompletionsPromise; }, provideDocumentSymbols: async ( document: vscode.TextDocument, token: vscode.CancellationToken, next: lsp.ProvideDocumentSymbolsSignature, ) => { if (await this.isInAngularProject(document)) { return next(document, token); } }, provideFoldingRanges: async ( document: vscode.TextDocument, context: vscode.FoldingContext, token: vscode.CancellationToken, next, ) => { if (!(await this.isInAngularProject(document))) { return null; } return next(document, context, token); }, provideLinkedEditingRange: async ( document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, next: lsp.ProvideLinkedEditingRangeSignature, ) => { if ( (await this.isInAngularProject(document)) && isNotTypescriptOrSupportedDecoratorField(document, position) ) { return next(document, position, token); } }, provideInlayHints: async ( document: vscode.TextDocument, range: vscode.Range, token: vscode.CancellationToken, next, ) => { if (!(await this.isInAngularProject(document))) { return null; } // For TypeScript files, only request Angular inlay hints when the // visible range intersects supported decorator fields (e.g. inline // template/style metadata). This matches the guarding strategy used // by other Angular LSP features and avoids work on unsupported TS regions. if (!isNotTypescriptOrSupportedDecoratorRange(document, range)) { return null; } return next(document, range, token); }, }, }; } async applyWorkspaceEdits(workspaceEdits: lsp.WorkspaceEdit[]) { for (const edit of workspaceEdits) { const workspaceEdit = await this.client?.protocol2CodeConverter.asWorkspaceEdit(edit); if (workspaceEdit === undefined) { continue; } await vscode.workspace.applyEdit(workspaceEdit); } } private async isInAngularProject(doc: vscode.TextDocument): Promise<boolean> { if (this.client === null) { return false; } const uri = doc.uri.toString(); if (this.fileToIsInAngularProjectMap.has(uri)) { return this.fileToIsInAngularProjectMap.get(uri)!; } try { const response = await this.client.sendRequest(IsInAngularProject, { textDocument: this.client.code2ProtocolConverter.asTextDocumentIdentifier(doc), }); if (response == null) { // If the response indicates the answer can't be determined at the moment, return `false` // but do not cache the result so we can try to get the real answer on follow-up requests. return false; } if (response) { this.fileToIsInAngularProjectMap.set(uri, true); } return response; } catch { return false; } } private createVirtualHtmlDoc(document: vscode.TextDocument): vscode.Uri { const originalUri = document.uri.toString(); const vdocUri = vscode.Uri.file(encodeURIComponent(originalUri) + '.html').with({ scheme: 'angular-embedded-content', authority: 'html', }); this.virtualDocumentContents.set(vdocUri.toString(), document.getText()); return vdocUri; } /** * Spin up the language server in a separate process and establish a connection. */ async start(): Promise<void> { if (this.client !== null) { throw new Error(`An existing client is running. Call stop() first.`); } // Node module for the language server const args = await this.constructArgs(); const prodBundle = this.context.asAbsolutePath('server'); const devBundle = this.context.asAbsolutePath( path.join('bazel-bin', 'server', 'src', 'server.js'), ); // If the extension is launched in debug mode then the debug server options are used // Otherwise the run options are used const serverOptions: lsp.ServerOptions = { run: { module: this.context.asAbsolutePath('server'), transport: lsp.TransportKind.ipc, args, }, debug: { // VS Code Insider launches extensions in debug mode by default but users // install prod bundle so we have to check whether dev bundle exists. module: fs.existsSync(devBundle) ? devBundle : prodBundle, transport: lsp.TransportKind.ipc, options: { // Argv options for Node.js execArgv: [ // do not lazily evaluate the code so all breakpoints are respected '--nolazy', // If debugging port is changed, update .vscode/launch.json as well '--inspect-brk=6009', ], env: { NG_DEBUG: true, }, }, args, }, }; if (!extensionVersionCompatibleWithAllProjects(serverOptions.run.module)) { vscode.window.showWarningMessage( `A project in the workspace is using a newer version of Angular than the language service extension. ` + `This may cause the extension to show incorrect diagnostics.`, ); } // Create the language client and start the client. const forceDebug = process.env['NG_DEBUG'] === 'true'; this.client = new lsp.LanguageClient( // This is the ID for Angular-specific configurations, like "angular.log". // See contributes.configuration in package.json. 'angular', this.name, serverOptions, this.clientOptions, forceDebug, ); await this.client.start(); // Must wait for the client to be ready before registering notification // handlers. this.sessionDisposables.push( registerNotificationHandlers(this.client, () => { this.fileToIsInAngularProjectMap.clear(); }), ); } /** * Construct the arguments that's used to spawn the server process. * @param ctx vscode extension context */ private async constructArgs(): Promise<string[]> { const config = vscode.workspace.getConfiguration(); const args: string[] = ['--logToConsole']; const ngLog: string = config.get('angular.log', 'off'); if (ngLog !== 'off') { // Log file does not yet exist on disk. It is up to the server to create the file. const logFile = path.join(this.context.logUri.fsPath, 'nglangsvc.log'); args.push('--logFile', logFile); args.push('--logVerbosity', ngLog); } args.push('--ngProbeLocations', this.context.extensionPath); const includeAutomaticOptionalChainCompletions = config.get<boolean>( 'angular.suggest.includeAutomaticOptionalChainCompletions', ); if (includeAutomaticOptionalChainCompletions) { args.push('--includeAutomaticOptionalChainCompletions'); } const includeCompletionsWithSnippetText = config.get<boolean>( 'angular.suggest.includeCompletionsWithSnippetText', ); if (includeCompletionsWithSnippetText) { args.push('--includeCompletionsWithSnippetText'); } const includeCompletionsForModuleExports = config.get<boolean>('angular.suggest.autoImports'); args.push( '--includeCompletionsForModuleExports', includeCompletionsForModuleExports === undefined ? 'true' : includeCompletionsForModuleExports.toString(), ); // Sort the versions from oldest to newest. const angularVersions = (await getAngularVersionsInWorkspace(this.outputChannel)).sort( (a, b) => (a.version.greaterThanOrEqual(b.version) ? 1 : -1), ); // Only disable block syntax if we find angular/core and every one we find does not support // block syntax if (angularVersions.length > 0) { const disableBlocks = angularVersions.every((v) => v.version.major < 17); const disableLet = angularVersions.every((v) => { return v.version.major < 18 || (v.version.major === 18 && v.version.minor < 1); }); if (disableBlocks) { args.push('--disableBlockSyntax'); this.outputChannel.appendLine( `All workspace roots are using versions of Angular that do not support control flow block syntax.` + ` Block syntax parsing in templates will be disabled.`, ); } if (disableLet) { args.push('--disableLetSyntax'); this.outputChannel.appendLine( `All workspace roots are using versions of Angular that do not support @let syntax.` + ` @let syntax parsing in templates will be disabled.`, ); } } setAngularVersionAndShowMultipleVersionsWarning(angularVersions, args, this.outputChannel); const forceStrictTemplates = config.get<boolean>('angular.forceStrictTemplates'); if (forceStrictTemplates) { args.push('--forceStrictTemplates'); } const suppressAngularDiagnosticCodes = config.get<string>( 'angular.suppressAngularDiagnosticCodes', ); if (suppressAngularDiagnosticCodes) { args.push('--suppressAngularDiagnosticCodes', suppressAngularDiagnosticCodes); } // Note: Document symbols settings (angular.documentSymbols.enabled, // angular.documentSymbols.showImplicitForVariables) are now fetched // dynamically via workspace/configuration request by the server. // This allows users to change these settings without restarting. const tsdk = await this.getTsdkPath(); if (tsdk.trim().length > 0) { args.push('--tsdk', tsdk); } args.push('--tsProbeLocations', this.context.extensionPath); const supportClientSide = config.get('angular.server.useClientSideFileWatcher'); if (supportClientSide) { args.push('--useClientSideFileWatcher'); } return args; } /** * Kill the language client and perform some clean ups. */ async stop(): Promise<void> { if (this.client === null) { return; } await this.client.stop(); this.outputChannel.clear(); this.disposeSessionDisposables(); this.client = null; this.fileToIsInAngularProjectMap.clear(); this.virtualDocumentContents.clear(); } /** * Requests a template typecheck block at the current cursor location in the * specified editor. */ async getTcbUnderCursor(textEditor: vscode.TextEditor): Promise<GetTcbResponse | undefined> { if (this.client === null) { return undefined; } const c2pConverter = this.client.code2ProtocolConverter; // Craft a request by converting vscode params to LSP. The corresponding // response is in LSP. const response = await this.client.sendRequest(GetTcbRequest, { textDocument: c2pConverter.asTextDocumentIdentifier(textEditor.document), position: c2pConverter.asPosition(textEditor.selection.active), }); if (response === null) { return undefined; } const p2cConverter = this.client.protocol2CodeConverter; // Convert the response from LSP back to vscode. return { uri: p2cConverter.asUri(response.uri), content: response.content, selections: await p2cConverter.asRanges(response.selections), }; } get initializeResult(): lsp.InitializeResult | undefined { return this.client?.initializeResult; } async getComponentsForOpenExternalTemplate( textEditor: vscode.TextEditor, ): Promise<vscode.Location[] | undefined> { if (this.client === null) { return undefined; } const response = await this.client.sendRequest(GetComponentsWithTemplateFile, { textDocument: this.client.code2ProtocolConverter.asTextDocumentIdentifier( textEditor.document, ), }); if (response === undefined) { return undefined; } const p2cConverter = this.client.protocol2CodeConverter; return response.map( (v) => new vscode.Location(p2cConverter.asUri(v.uri), p2cConverter.asRange(v.range)), ); } async getTemplateLocationForComponent( textEditor: vscode.TextEditor, ): Promise<vscode.Location | null> { if (this.client === null) { return null; } const c2pConverter = this.client.code2ProtocolConverter; // Craft a request by converting vscode params to LSP. The corresponding // response is in LSP. const response = await this.client.sendRequest(GetTemplateLocationForComponent, { textDocument: c2pConverter.asTextDocumentIdentifier(textEditor.document), position: c2pConverter.asPosition(textEditor.selection.active), }); if (response === null) { return null; } const p2cConverter = this.client.protocol2CodeConverter; return new vscode.Location( p2cConverter.asUri(response.uri), p2cConverter.asRange(response.range), ); } private disposeSessionDisposables(): void { for ( let disposable = this.sessionDisposables.pop(); disposable !== undefined; disposable = this.sessionDisposables.pop() ) { disposable.dispose(); } } private async getTsdkPath(): Promise<string> { const config = vscode.workspace.getConfiguration(); const jsTsTsdkInspect = config.inspect<string>('js/ts.tsdk.path'); const tsTsdkInspect = config.inspect<string>('typescript.tsdk'); // 1. Check workspace/folder settings first (highest priority) // ONLY check or load workspace-level settings if the workspace is trusted if (vscode.workspace.isTrusted) { const jsTsWorkspaceTsdk = ( jsTsTsdkInspect?.workspaceValue ?? jsTsTsdkInspect?.workspaceFolderValue )?.trim(); const tsWorkspaceTsdk = ( tsTsdkInspect?.workspaceValue ?? tsTsdkInspect?.workspaceFolderValue )?.trim(); const workspaceTsdk = jsTsWorkspaceTsdk || tsWorkspaceTsdk; if (workspaceTsdk) { const stateKey = `approvedTsdk:${workspaceTsdk}`; const isApproved = this.context.workspaceState.get<boolean>(stateKey); if (isApproved) { if (!path.isAbsolute(workspaceTsdk)) { const workspaceFolders = vscode.workspace.workspaceFolders || []; for (const folder of workspaceFolders) { const absolutePath = path.join(folder.uri.fsPath, workspaceTsdk); if (fs.existsSync(path.join(absolutePath, 'tsserverlibrary.js'))) { return absolutePath; } } if (workspaceFolders.length > 0) { return path.join(workspaceFolders[0].uri.fsPath, workspaceTsdk); } } return workspaceTsdk; } if (isApproved === undefined) { // Prompt the user asynchronously, without blocking current server initialization this.promptForTsdkApproval(workspaceTsdk, stateKey); } // Fall back to globalValue (or bundled) while waiting for approval or if rejected } } return jsTsTsdkInspect?.globalValue?.trim() ?? tsTsdkInspect?.globalValue?.trim() ?? ''; } private async promptForTsdkApproval(workspaceTsdk: string, stateKey: string): Promise<void> { const allowOption = 'Allow'; const disallowOption = 'Disallow'; const choice = await vscode.window.showWarningMessage( `This workspace configures a custom TypeScript compiler path (${workspaceTsdk}) via 'js/ts.tsdk.path' or 'typescript.tsdk'. ` + `Do you want to allow the Angular Language Service to load the TypeScript compiler from this path?`, allowOption, disallowOption, ); if (choice === allowOption) { await this.context.workspaceState.update(stateKey, true); // Restart the language server so it launches with the newly authorized compiler path await vscode.commands.executeCommand('angular.restartNgServer'); } else { // Remember rejection to avoid repeatedly prompting on startup/activation await this.context.workspaceState.update(stateKey, false); } } dispose() { this.disposeSessionDisposables(); for (let d = this.disposables.pop(); d !== undefined; d = this.disposables.pop()) { d.dispose(); } } } function registerNotificationHandlers( client: lsp.LanguageClient, onProjectStateChange: () => void, ): vscode.Disposable { const disposables: vscode.Disposable[] = []; disposables.push( client.onNotification(ProjectLoadingStart, () => { onProjectStateChange(); vscode.window.withProgress( { location: vscode.ProgressLocation.Window, title: 'Initializing Angular language features', }, () => new Promise<void>((resolve) => { client.onNotification(ProjectLoadingFinish, () => { onProjectStateChange(); resolve(); }); }), ); }), ); disposables.push( client.onNotification(SuggestStrictMode, async (params: SuggestStrictModeParams) => { const config = vscode.workspace.getConfiguration(); if ( config.get('angular.enable-strict-mode-prompt') === false || config.get('angular.forceStrictTemplates') ) { return; } const openTsConfig = 'Open tsconfig.json'; // Markdown is not generally supported in `showInformationMessage()`, // but links are supported. See // https://github.com/microsoft/vscode/issues/20595#issuecomment-281099832 const doNotPromptAgain = 'Do not show again for this workspace'; const selection = await vscode.window.showInformationMessage( 'Some language features are not available. To access all features, enable ' + '[strictTemplates](https://angular.dev/reference/configs/angular-compiler-options#stricttemplates) in ' + '[angularCompilerOptions](https://angular.dev/reference/configs/angular-compiler-options).', openTsConfig, doNotPromptAgain, ); if (selection === openTsConfig) { const document = await vscode.workspace.openTextDocument(params.configFilePath); vscode.window.showTextDocument(document); } else if (selection === doNotPromptAgain) { config.update( 'angular.enable-strict-mode-prompt', false, vscode.ConfigurationTarget.Workspace, ); } }), ); disposables.push( client.onNotification(OpenOutputChannel, () => { client.outputChannel.show(); }), ); return vscode.Disposable.from(...disposables); } function extensionVersionCompatibleWithAllProjects(serverModuleLocation: string): boolean { const languageServiceVersion = resolve( '@angular/language-service', serverModuleLocation, )?.version; if (languageServiceVersion === undefined) { return true; } const workspaceFolders = vscode.workspace.workspaceFolders || []; for (const workspaceFolder of workspaceFolders) { const angularCore = resolve('@angular/core', workspaceFolder.uri.fsPath); if (angularCore === undefined) { continue; } if (!languageServiceVersion.greaterThanOrEqual(angularCore.version, 'minor')) { return false; } } return true; } /** * Traverses through the currently open VSCode workspace (i.e. all open folders) * and finds all `@angular/core` versions installed based on `package.json` files. */ async function getAngularVersionsInWorkspace( outputChannel: vscode.OutputChannel, ): Promise<NodeModule[]> { const packageJsonFiles = await vscode.workspace.findFiles( '**/package.json', // Skip looking inside `node_module` folders as those contain irrelevant files. '**/node_modules/**', ); const packageJsonRoots = packageJsonFiles.map((f) => path.dirname(f.fsPath)); const angularCoreModules = new Set<NodeModule>(); outputChannel.appendLine(`package.json roots detected: ${packageJsonRoots.join(',\n ')}`); for (const packageJsonRoot of packageJsonRoots) { const angularCore = resolve('@angular/core', packageJsonRoot); if (angularCore === undefined) { continue; } angularCoreModules.add(angularCore); } return Array.from(angularCoreModules); } // TODO(atscott): Now that language service resolves the version of Angular local to the project, do we need this? function setAngularVersionAndShowMultipleVersionsWarning( angularVersions: NodeModule[], args: string[], outputChannel: vscode.OutputChannel, ) { if (angularVersions.length === 0) { return; } if (angularVersions[0].version.toString() === '0.0.0') { // If only version 0.x is found, update it to 999 instead (0.0.0 is used for the version when building locally) angularVersions[0].version = new Version('999.999.999'); } // Pass the earliest Angular version along to the compiler for maximum compatibility. // For example, if we tell the v21 compiler that we're using v21 but there's a v13 project, // the compiler may attempt to import and use APIs from angular core that don't exist in v13. args.push('--angularCoreVersion', angularVersions[0].version.toString()); outputChannel.appendLine( `Using Angular version ${angularVersions[0].version.toString()} by default. If ` + `the project-specific version cannot be resolved, this version will be used.`, ); let minorVersions = new Map<string, NodeModule>(); for (const v of angularVersions) { minorVersions.set(`${v.version.major}.${v.version.minor}`, v); } if (minorVersions.size > 1) { vscode.window.showWarningMessage( `Multiple versions of Angular detected in the workspace. This can lead to compatibility issues for the language service. ` + `See the output panel for more details.`, ); outputChannel.appendLine(`Multiple Angular versions detected in the workspace:`); for (const v of minorVersions.values()) { outputChannel.appendLine( ` Angular version ${v.version.toString()} detected at ${v.resolvedPath}`, ); } } }