Skip to content

Workspace root-import resolution via exports field returns a node_modules-symlink path that never matches the tracked file node #2288

Description

@carlos-alm

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.

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