-
Notifications
You must be signed in to change notification settings - Fork 42
Description
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-ember3.0.66- bundled
@ember-tooling/ember-language-server2.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
- Set up a PNPM workspace where one Ember app depends on another local workspace package.
- Ensure the dependency is linked into
node_modulesas a PNPM symlink. - In a template such as:
<MyButton />- 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:
- Canonicalize definition result paths with
fs.realpathSyncbeforeURI.file(...). - Prefer in-repo/workspace addon roots over
node_modulessymlink roots during addon resolution. - 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.