Summary
resolveViaWorkspace()'s root-import branch (src/domain/graph/resolve.ts) tries the package's exports field first via resolveViaExports(), which locates the package directory through findPackageDir() — a node_modules walk. For a real monorepo where workspace tools (npm/yarn/pnpm workspaces) symlink workspace packages into node_modules, this returns a path like node_modules/@myorg/core/lib/index.js.
But detectWorkspaces()/expandWorkspacePatterns() (the same file's workspace detection, driven by the workspaces/packages glob in the root package.json) registers each package's info.dir from the glob-matched real path (packages/core), which is also where the project's own file collector finds and tracks the source file (node_modules is excluded from file collection via IGNORE_DIRS).
So for a workspace package whose package.json has only an exports field (no main), the root-import resolution returns node_modules/@myorg/core/lib/index.js — a path string that never matches the graph's actual tracked node for that file (packages/core/lib/index.js). The result: no imports edge, no calls edge, and no cross-file call attribution for that import — even though the file itself is correctly parsed and present in the graph under its real path.
Reproduction
root/
package.json { "workspaces": ["packages/*"] }
node_modules/@myorg/core -> ../../packages/core (symlink, as workspace tools create)
packages/core/
package.json { "name": "@myorg/core", "exports": "./lib/index.js" }
lib/index.js function greet() { ... }
packages/app/src/main.js import { greet } from "@myorg/core"; console.log(greet());
$ codegraph build --engine wasm # or --engine native — identical result
$ sqlite3 .codegraph/graph.db "SELECT DISTINCT kind FROM edges;"
contains
No imports or calls edge at all. Changing packages/core/package.json to use "main": "./lib/index.js" instead of exports makes it work correctly (resolveViaWorkspace()'s entry fallback uses info.entry, which IS the real packages/-based path) — confirming the exports-first branch specifically is what returns the wrong path shape.
Confirmed pre-existing, not engine-specific
Verified this reproduces identically on both the WASM/JS engine (calling resolveViaWorkspace()/resolveViaExports() directly from dist/domain/graph/resolve.js) and the native engine (after porting resolveViaExports() to Rust in #2060) — both return the same node_modules-relative path, and both produce byte-identical (zero) imports/calls edges for this scenario. This is a pre-existing WASM-side design gap that #2060's native port faithfully mirrors (correctly, for parity purposes) rather than something introduced by that port.
Suggested fix
When a workspace package is resolved via its exports field, prefer relativizing to the workspace-detected info.dir (the real, non-symlinked path already known from detectWorkspaces()) rather than the node_modules-discovered packageDir from findPackageDir()'s walk — e.g. resolve the exports target relative to info.dir directly instead of going through the node_modules symlink lookup at all for workspace packages specifically. Needs the equivalent fix mirrored in both resolveViaWorkspace() (resolve.ts) and resolve_via_workspace() (resolve.rs).
Discovery context
Found while end-to-end-verifying #2060's native exports-field port against a real monorepo fixture (workspace package with only exports, no main — the exact scenario #2060 was scoped to fix). Confirmed via direct unit tests that #2060's native port itself is correct (produces the identical path the JS engine already produces) — this is a separate, pre-existing gap in how that resolved path interacts with workspace detection's own directory tracking, not a flaw in the #2060 port.
Summary
resolveViaWorkspace()'s root-import branch (src/domain/graph/resolve.ts) tries the package'sexportsfield first viaresolveViaExports(), which locates the package directory throughfindPackageDir()— anode_moduleswalk. For a real monorepo where workspace tools (npm/yarn/pnpm workspaces) symlink workspace packages intonode_modules, this returns a path likenode_modules/@myorg/core/lib/index.js.But
detectWorkspaces()/expandWorkspacePatterns()(the same file's workspace detection, driven by theworkspaces/packagesglob in the rootpackage.json) registers each package'sinfo.dirfrom the glob-matched real path (packages/core), which is also where the project's own file collector finds and tracks the source file (node_modulesis excluded from file collection viaIGNORE_DIRS).So for a workspace package whose
package.jsonhas only anexportsfield (nomain), the root-import resolution returnsnode_modules/@myorg/core/lib/index.js— a path string that never matches the graph's actual tracked node for that file (packages/core/lib/index.js). The result: noimportsedge, nocallsedge, and no cross-file call attribution for that import — even though the file itself is correctly parsed and present in the graph under its real path.Reproduction
No
importsorcallsedge at all. Changingpackages/core/package.jsonto use"main": "./lib/index.js"instead ofexportsmakes it work correctly (resolveViaWorkspace()'s entry fallback usesinfo.entry, which IS the realpackages/-based path) — confirming theexports-first branch specifically is what returns the wrong path shape.Confirmed pre-existing, not engine-specific
Verified this reproduces identically on both the WASM/JS engine (calling
resolveViaWorkspace()/resolveViaExports()directly fromdist/domain/graph/resolve.js) and the native engine (after portingresolveViaExports()to Rust in #2060) — both return the samenode_modules-relative path, and both produce byte-identical (zero)imports/callsedges for this scenario. This is a pre-existing WASM-side design gap that #2060's native port faithfully mirrors (correctly, for parity purposes) rather than something introduced by that port.Suggested fix
When a workspace package is resolved via its
exportsfield, prefer relativizing to the workspace-detectedinfo.dir(the real, non-symlinked path already known fromdetectWorkspaces()) rather than thenode_modules-discoveredpackageDirfromfindPackageDir()'s walk — e.g. resolve theexportstarget relative toinfo.dirdirectly instead of going through thenode_modulessymlink lookup at all for workspace packages specifically. Needs the equivalent fix mirrored in bothresolveViaWorkspace()(resolve.ts) andresolve_via_workspace()(resolve.rs).Discovery context
Found while end-to-end-verifying #2060's native
exports-field port against a real monorepo fixture (workspace package with onlyexports, nomain— the exact scenario #2060 was scoped to fix). Confirmed via direct unit tests that #2060's native port itself is correct (produces the identical path the JS engine already produces) — this is a separate, pre-existing gap in how that resolved path interacts with workspace detection's own directory tracking, not a flaw in the #2060 port.