Skip to content

PNPM workspace 'Go To Definition' resolves to symlinked node_modules path instead of workspace source #450

@apellerano-pw

Description

@apellerano-pw

Summary

When using the Ember Language Server with a PNPM workspace, 'Go To Definition' in Ember templates resolves addon definitions to the symlinked package path under node_modules instead of the actual workspace source path.

In practice, this means navigating to a component like MyButton from a template opens:

packages/my-app/node_modules/my-addon/addon/components/my-button.hbs

instead of:

packages/my-addon/addon/components/my-button.hbs

Environment

  • VSCode on macOS
  • embertooling.vscode-ember 3.0.66
  • bundled @ember-tooling/ember-language-server 2.30.9
  • PNPM workspace / monorepo layout
  • Ember app consuming another workspace package through PNPM

Reproduction

Workspace shape

Example monorepo structure:

packages/
  my-app/
    app/components/some-cool-button-needing-thing.hbs
    node_modules/my-addon -> ../../my-addon
  my-addon/
    addon/components/my-button.hbs

Repro steps

  1. Set up a PNPM workspace where one Ember app depends on another local workspace package.
  2. Ensure the dependency is linked into node_modules as a PNPM symlink.
  3. In a template such as:
<MyButton />
  1. Trigger Go To Definition on MyButton.

Actual behavior

The language server returns a location under the symlinked package path, for example:

packages/my-app/node_modules/my-addon/addon/components/my-button.hbs

Expected behavior

The language server should prefer the canonical workspace source file, for example:

packages/my-addon/addon/components/my-button.hbs

If both paths resolve to the same real file, the returned definition should use the workspace-realpath location rather than the symlinked node_modules location.

Investigation

This does not appear to be controlled by TypeScript or JavaScript project settings. Adding jsconfig.json path aliases did not change template Go To Definition behavior.

Likely root cause

From the bundled source map for @ember-tooling/ember-language-server@2.30.9:

Addon root resolution prefers node_modules first

src/utils/layout-helpers.ts contains:

export async function resolvePackageRoot(root: string, addonName: string, packagesFolder = 'node_modules'): Promise<string | false> {
  const roots = root.split(path.sep);

  while (roots.length) {
    const prefix = roots.join(path.sep);
    const maybePath = path.join(prefix, packagesFolder, addonName);
    const linkedPath = path.join(prefix, addonName);

    if (await fsProvider().exists(path.join(maybePath, 'package.json'))) {
      return maybePath;
    } else if (await fsProvider().exists(path.join(linkedPath, 'package.json'))) {
      return linkedPath;
    }

    roots.pop();
  }

  return false;
}

With PNPM workspaces, maybePath is the symlinked path in node_modules, so that path wins.

Returned locations use the discovered path verbatim

src/utils/definition-helpers.ts contains:

export function pathsToLocations(...paths: string[]): Location[] {
  return paths.map((modulePath) => {
    return Location.create(URI.file(modulePath).toString(), Range.create(0, 0, 0, 0));
  });
}

and:

return Location.create(URI.file(modulePath).toString(), Range.create(useIndex, start, useIndex, end));

So once the symlinked addon root is discovered, the definition provider returns that path directly.

Realpath support exists but is not used here

src/fs-provider.ts exposes realpathSync, but the relevant definition and layout helper codepaths do not use it when resolving or serializing definition results.

Suggested fixes

Any of these would address the issue:

  1. Canonicalize definition result paths with fs.realpathSync before URI.file(...).
  2. Prefer in-repo/workspace addon roots over node_modules symlink roots during addon resolution.
  3. Deduplicate candidate paths by realpath, and prefer a path that is inside an opened workspace root.

Minimal expected behavior change

If two candidate paths refer to the same real file, return the workspace/canonical path instead of the symlinked node_modules path.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions