Skip to content

Resolve npm-aliased imports (the "alias": "npm:@scope/pkg@ver" pattern) back to local library sources #343

@chris-deriv

Description

@chris-deriv

Summary

The graph's import resolver treats modules imported via an npm alias (the "alias-name": "npm:@scope/real-package@version" syntax in package.json) as external. As a result, importers_of / callers_of / impact_radius queries on files inside a locally-vendored package miss consumers that import via the alias — even when both the consumer and the library source are inside the graph's scanned tree.

Reproducer (generic)

  1. A multi-app repository with multiple consumer apps (appA/, appB/) sharing a locally-developed library (sharedLib/) — a common pattern for monorepos that also publish the shared library to a private registry.
  2. sharedLib/package.json declares "name": "@scope/shared".
  3. Each consumer appX/package.json pins the library via the npm-alias syntax:
    "dependencies": {
      "sharedLib": "npm:@scope/shared@^1.0.0"
    }
  4. Consumer source imports look like:
    import foo from 'sharedLib/some/module';
  5. Build/update the graph on the repository root:
    code-review-graph build .
    
  6. Query:
    query_graph_tool pattern=importers_of target=sharedLib/some/module.js
    
    Expected: returns appA/src/entry.js and similar consumers in appB.
    Actual: returns 0 results. The alias-based imports are not reflected as edges in the graph.

The same gap shows up in get_impact_radius_tool, callers_of on exported functions, and any downstream feature that relies on IMPORTS_FROM edges between consumer modules and the aliased library.

Why it matters

Alias-based local vendoring is a common pattern in monorepos / multi-app repos:

  • A shared UI or utility library is developed inside the repository.
  • It's published to a private registry for production consumption.
  • Consumer apps pin it via the "alias": "npm:@scope/pkg@version" syntax so their original import paths (import X from 'alias/...') keep working after the publish-and-install cycle, without a repo-wide refactor.

For repos following this pattern, every cross-app / cross-module question about the shared library currently falls through the graph — users have to fall back to grep for any importers_of / callers_of query that crosses the alias boundary. That undermines the graph's main value proposition for exactly the codebases that would benefit most from structural analysis.

Proposed behaviour

When building/updating the graph:

  1. For each package.json in the scanned tree, scan dependencies and devDependencies for entries whose value starts with npm:. Parse out the alias → real-package-name mapping.
  2. For each npm: entry where the real-package-name is also present in the graph's scanned tree (as a local package.json with a matching name field), record a local alias mapping: alias-name → local-library-path.
  3. During import analysis, when the analyzer sees import X from 'alias-name/<subpath>', resolve it against local-library-path/<subpath> rather than treating it as external. Emit the IMPORTS_FROM edge accordingly.

Smaller first step

If package.json scanning is too invasive for a first pass, an explicit config option would unblock users with this pattern:

# .code-review-graph.yml
alias_mappings:
  - alias: "sharedLib"
    local_path: "./packages/shared-library"

The resolver then treats imports of sharedLib/* as resolving inside ./packages/shared-library/*. Manual config gives users control today; auto-detection from package.json can come later.

Current workaround

Users of repos with this pattern fall back to grep -rnE "import.*['\"]alias-name/..." for alias-crossing queries, and use the graph only for intra-module traversal.

Environment

  • code-review-graph 2.3.2
  • Installed via uv tool install
  • MCP server used via Claude Code

Happy to contribute a PR if the approach above sounds right. LMK which direction you'd prefer (package.json auto-detection vs explicit config first).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions