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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user