Skip to content

language-server: external templates permanently lose language service in solution-style (Nx) workspaces — getDefaultProjectForScriptInfo lacks the #2165 fallback #69768

Description

@ShayanAbbas1

Which @angular/* package(s) are the source of the bug?

language-server (vscode-ng-language-service/server)

Is this a regression?

Partially. The class of bug was reported in angular/vscode-ng-language-service#2149 and fixed by angular/vscode-ng-language-service#2165, but that fix only covers one of the two code paths that need it.

Description

In workspaces whose app tsconfig.json is solution-style (references only — the default layout Nx generates), external templates (.component.html) permanently lose all language service features (go-to-definition, hover, diagnostics) once their association to a configured project is dropped, e.g. after the corresponding component .ts file is closed or after project-graph churn. Every subsequent request for the template fails with:

Err ... No config file for /path/to/foo.component.html

and returns null — silently, from the client's perspective — until the user manually opens the component .ts file again.

Root cause

A solution-style tsconfig.json contains only references, so ts.server.ProjectService.openClientFile() can never map an .html file to a configured project — no referenced tsconfig lists html files. PR #2165 addressed this in Session.onDidOpenTextDocument with a best-effort fallback: briefly open the sibling .ts file (which is included in a referenced project), forcing the right project to load, then re-open the template:

https://github.com/angular/angular/blob/main/vscode-ng-language-service/server/src/session.ts (in onDidOpenTextDocument):

// https://github.com/angular/vscode-ng-language-service/issues/2149
if (result.configFileName === undefined && languageId === LanguageId.HTML) {
  const maybeComponentTsPath = filePath.replace(/\.html$/, '.ts');
  ...

However, Session.getDefaultProjectForScriptInfo — the request-time recovery path used by getLSAndScriptInfo (hover/definitions/etc.) and onDidChangeTextDocument whenever a script info has no configured project — did not receive the same fallback. It still performs a bare lookup and dead-ends:

if (!project || project.projectKind !== ts.server.ProjectKind.Configured) {
  const {configFileName} = this.projectService.openClientFile(scriptInfo.fileName);
  if (!configFileName) {
    // Failed to find a config file. There is nothing we could do.
    this.error(`No config file for ${scriptInfo.fileName}`);
    return null;
  }
  ...

So whenever a template's project attachment is torn down after open (tab lifecycle, project updates), the server can never recover it. The template stays dead until the user opens the sibling .ts themselves, re-triggering the didOpen-time fallback indirectly.

This is editor-agnostic: reproduced both in Zed (where an aggressive buffer lifecycle makes template features unusable in Nx workspaces — see nathansbradshaw/zed-angular#89 for user reports) and in VS Code itself with the Angular extension v22.0.1 (server 22.x — see VS Code reproduction below). It is likely also a contributing factor in "No config file" reports that persist post-#2165 (e.g. #65514 shows the same terminal error string for a different trigger).

Reproduction

Against a large Nx monorepo (Angular 19 app, ~30 projects, solution-style apps/*/tsconfig.json), driving @angular/language-server 21.1.5 over raw LSP stdio:

  1. initialize with the workspace root, didOpen a .component.html (languageId html) → feat(router): add routing to async components #2165's fallback loads the project, textDocument/definition on a component selector in the template resolves correctly (~9s cold / ~1ms warm). ✅
  2. didOpen the sibling .component.ts, then didClose it, plus further open/close churn of other project files → the template's script info loses its configured project.
  3. Any subsequent textDocument/definition/hover on the still-open template → server logs No config file for ...component.html and responds null, indefinitely (verified over minutes; 40+ consecutive failures). ❌

With --logFile/verbose tsserver logging, step 3 shows getDefaultProjectForScriptInfo's openClientFile returning no configFileName on every request (expected for a solution-style config), with no recovery attempt.

VS Code reproduction (Angular extension v22.0.1)

Same workspace, no custom LSP client involved:

  1. Close all editors (cmd+K W).
  2. Open an external template (*.component.html) directly — no TypeScript file open.
  3. Restart VS Code so the session restores with only the template open.
  4. Go to Definition on a component selector in the template does nothing, indefinitely.

Angular Language Service output panel:

[Error - 8:50:36 PM] No config file for /path/to/workspace/apps/my-app/src/app/components/foo/foo.component.html
[Error - 8:51:57 PM] No config file for /path/to/workspace/apps/my-app/src/app/components/foo/foo.component.html

Notably this is a template-opened-first session — the scenario #2149/#2165 was meant to cover — so the open-time best-effort is not sufficient on its own, and there is no recovery at request time.

Proposed fix

Mirror the #2165 fallback inside getDefaultProjectForScriptInfo: when configFileName comes back undefined and the file is an external template, briefly open/close the sibling .ts (guarded by projectService.openFiles, same as #2165) and retry; if the config lookup still fails but the sibling's script info has a configured project, attach the template's script info to that project directly.

We have been running exactly this patch (applied to the compiled 21.1.5 bundle) in daily use on the Nx monorepo above: template features now self-heal on the next request after any detach, with an A/B experiment confirming stock code fails (40+ No config file errors in 30s of normal use) and patched code recovers every time.

Happy to submit a PR with this change.

Please provide the environment you discovered this bug in

  • @angular/language-server: 21.1.5 (bug also present in current main source of session.ts)
  • Workspace: Nx 20.3 monorepo, Angular 19.0.7, TypeScript 5.5, solution-style app tsconfigs (references to tsconfig.app.json / tsconfig.spec.json / tsconfig.editor.json)
  • Clients reproduced: VS Code (Angular extension v22.0.1, server 22.x), Zed 1.10.3 (community Angular extension, server 21.1.5), and a raw LSP stdio client (no editor involved)
  • OS: macOS 15 (arm64), node 24.9

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: language-serviceIssues related to Angular's VS Code language servicegemini-triagedLabel noting that an issue has been triaged by gemini

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions