Workspace package names (pnpm/npm/yarn workspaces) are never resolved — importers_of/impact_radius collapse at every package boundary

Author: nhuphuoc-bicCreated Sep 17, 2026Updated Sep 17, 2026

Summary

IMPORTS_FROM edges whose specifier is a package-manager workspace name (pnpm/npm/yarn workspaces — @core/policies, @acme/ui, …) are stored bare. TsconfigResolver only reads compilerOptions.paths; nothing reads the workspace's package.json name + exports/main. In a pnpm monorepo that is the primary import shape, so importers_of, impact_radius and references_to all collapse at every package boundary.

Measured (v2.3.8, pnpm monorepo, 1 491 files, 10 174 nodes)

Edge kinds by whether target_qualified resolved to a path:

kind resolved bare
IMPORTS_FROM 2 809 2 738
REFERENCES 3 534 1 343
CALLS 13 083 33 174

Top bare IMPORTS_FROM targets are all workspace names: @core/shared 237 files, @core/ui 135, @core/database/testing/ephemeral-pg 135, @core/database 109, @core/billing 67, @core/channels 55.

Concrete: importers_of("packages/policies/src/can.ts")1 result (its own unit test). grep -rl 'from "@core/policies'43 files. The same file is the authorization primitive every route in the app calls through.

Why tsconfig paths miss it: apps/web/tsconfig.json has only "@/*": ["./src/*"]. @core/* is not a path alias; each packages/*/package.json declares "name": "@core/policies" with "exports": { ".": "./src/index.ts" }, and pnpm links it. That is the standard monorepo layout (Turborepo, Nx, plain pnpm workspaces).

Where it lives

parser.py:13688 calls self._tsconfig_resolver.resolve_alias(module, file_path) and falls through to bare when it returns None. A WorkspaceResolver beside TsconfigResolver would close it:

  1. Find the workspace root (pnpm-workspace.yaml, or package.json with workspaces).
  2. Glob the declared package dirs, read each package.jsonname → dir, plus exports (subpath map, conditions) / main / module / types.
  3. On a bare specifier: longest-prefix match on package name; map the remainder through exports (. for the bare name, ./testing/ephemeral-pg for @core/database/testing/ephemeral-pg); probe _PROBE_EXTENSIONS like _probe_path does.
  4. Cache per workspace root; invalidate on package.json change (incremental already tracks file hashes).

Scope note: this makes importers_of("packages/policies/src/index.ts") correct. importers_of("…/can.ts") stays low because importers import the barrel — following export * from / export { x } from re-exports is a separate step, and worth naming in the same fix so the number is not read as complete.

Related: #969 (query-layer half of the same gap for references_to, merged via #988). This is the parser-layer half. I can send a PR if the shape above is acceptable — the repo I measured on is available for a before/after.

Source: tirth8205/code-review-graph