fix: resolve workspace-member imports in installed git-dep monorepo plugins

A plugin installed as a bun git dependency contains its full workspace
tree but no node_modules links: bun materializes a git dep's regular npm
dependencies into the host tree and skips workspace:*/file: edges. Bare
imports between workspace siblings inside such a plugin therefore failed
with 'Cannot find module' - the extension host's package walk only
looked at node_modules dirs.

Teach findNodePackageRoot to also recognize workspace roots on the
walk-up: when a directory's package.json declares workspaces (array or
yarn-style { packages }), scan member manifests for the requested name.
node_modules candidates at the same level still win, so explicitly
installed copies shadow workspace members. Exports conditions (bun ->
source) apply to the member manifest as usual, so unbuilt source-only
members resolve.

Contract tests: a fake installed monorepo tree resolves a bare member
import to the member's bun-condition source entry; an installed
node_modules copy shadows the member.
This commit is contained in:
Sami Jawhar
2026-08-16 16:55:43 +00:00
parent de6b7974a0
commit b8db96e9fe
3 changed files with 149 additions and 0 deletions
@@ -1351,6 +1351,10 @@ async function findNodePackageRootUncached(packageName: string, importerPath: st
if (await pathExists(path.join(candidate, "package.json"))) {
return candidate;
}
const workspaceMember = await findWorkspaceMemberPackageRoot(dir, packageName);
if (workspaceMember) {
return workspaceMember;
}
const parent = path.dirname(dir);
if (parent === dir) {
return null;
@@ -1359,6 +1363,49 @@ async function findNodePackageRootUncached(packageName: string, importerPath: st
}
}
/**
* Resolve `packageName` as a workspace member when `dir` is a workspace root.
*
* An installed git dependency of a monorepo plugin contains the full
* workspace tree but no node_modules links: `bun install` materializes a git
* dependency's regular npm dependencies into the host tree and skips its
* `workspace:*` / `file:` edges. Bare imports between workspace siblings
* therefore never resolve through the node_modules walk above. When a
* directory on that walk declares `workspaces` (array form or the yarn-style
* `{ packages: [...] }` object), scan the member manifests for the requested
* package name. node_modules candidates at the same level win, so an
* explicitly installed copy still shadows the workspace member.
*/
async function findWorkspaceMemberPackageRoot(dir: string, packageName: string): Promise<string | null> {
if (!(await pathExists(path.join(dir, "package.json")))) {
return null;
}
const manifest = await readPackageManifest(dir);
const rawWorkspaces = manifest?.workspaces;
const patterns = Array.isArray(rawWorkspaces)
? rawWorkspaces
: isRecord(rawWorkspaces) && Array.isArray(rawWorkspaces.packages)
? rawWorkspaces.packages
: null;
if (!patterns) {
return null;
}
for (const pattern of patterns) {
if (typeof pattern !== "string" || pattern.startsWith("!")) {
continue;
}
const glob = new Bun.Glob(path.join(pattern, "package.json"));
for await (const match of glob.scan({ cwd: dir, onlyFiles: true })) {
const memberRoot = path.dirname(path.join(dir, match));
const memberManifest = await readPackageManifest(memberRoot);
if (memberManifest?.name === packageName) {
return memberRoot;
}
}
}
return null;
}
async function readPackageManifest(packageRoot: string): Promise<Record<string, unknown> | null> {
const cached = packageManifestCache.get(packageRoot);
if (cached) return cached;