Repository navigation
fix: resolve npm/yarn/pnpm workspace package imports (#1030) - #1093
1 commit merged into
Conversation
Imports of sibling workspace packages (`@acme/shared`, `@acme/sdk/browser`) were stored as bare specifiers, so importers_of, the impact radius and references_to stopped at every package boundary. Add WorkspaceResolver, consulted after tsconfig paths for JS/TS imports: - find the nearest pnpm-workspace.yaml or package.json `workspaces` inside the repository and map member names to their directories - read `exports` (subpaths, conditions, fallback arrays, `*` patterns with Node's key priority, `null` blocking), then source/module/main/types/typings - retry build-output targets (dist/, build/, out/, lib/) under src/ first - prefer a missing edge to a wrong one: unscoped names need a declared dependency, Node builtin names never resolve, probes are case-exact on every platform, and results stay inside the repository and must be indexed Closes tirth8205#1030
|
Tested this branch (f800ca2) on a pnpm + Turborepo monorepo: 5,076 indexed files, NestJS apps importing internal Against a 2.3.9 build of the same checkout:
It closes the biggest gap we hit on this repo. |
|
Thank you for this PR. I ran a full build on small npm and pnpm workspaces. Before I can merge it, please fix these:
After you push the fix, I will test it again. |
There was a problem hiding this comment.
🟡 Changes recommended
Workspace resolution defects can leave imports stale after manifest changes, misidentify workspace members, and resolve imports to incorrect source files or unsupported targets without graph nodes.
5 open issues
What changed in this PR
Adds workspace package import resolution to the code graph, addressing #1030 so imports and calls can cross npm, yarn and pnpm package boundaries.
Changes:
- Introduces workspace discovery, package entry resolution and source-file fallbacks for compiled output.
- Integrates resolution after tsconfig aliases, preserving alias precedence.
- Adds resolver and graph regression tests, plus documentation updates.
| File | Description |
|---|---|
tests/test_workspace_resolver.py |
Tests discovery, entry resolution, caching and defensive handling. |
tests/test_workspace_package_imports.py |
Tests cross-package imports, calls, graph queries and full builds. |
docs/architecture.md |
Adds the workspace resolver to the module inventory. |
code_review_graph/workspace_resolver.py |
Implements workspace discovery and package-to-source resolution. |
code_review_graph/parser.py |
Consults workspace resolution after tsconfig aliases. |
CLAUDE.md |
Documents the new resolver and test module. |
CHANGELOG.md |
Describes workspace resolution behavior and safeguards. |
Review effort: Balanced
| return resolved | ||
| # Then a sibling package of an npm/yarn/pnpm workspace (#1030), | ||
| # but never an edge to a file the build would not index. | ||
| resolved = self._workspace_resolver.resolve(module, file_path) |
| if resolved and ( | ||
| self._repo_root is None | ||
| or self._path_is_indexed(Path(resolved), self._repo_root) | ||
| ): |
| candidate = base.with_suffix(ext) if not base.suffix else Path(str(base) + ext) | ||
| if exists(candidate): | ||
| return candidate | ||
| if base.is_dir(): |
| if "package.json" not in filenames: | ||
| continue | ||
| directory = Path(dirpath) | ||
| if fnmatch.fnmatch(directory.relative_to(root).as_posix(), pattern): |
| candidates.extend([stripped, kept]) | ||
| else: | ||
| candidates.append(kept) | ||
| candidates.append(base.joinpath(*parts)) |
0160eb0
…ce probes (review of tirth8205#1093) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Linked issue
Closes #1030
What & why
In an npm, yarn or pnpm workspace, an import such as
from '@acme/policies'names a sibling package through the package manager's links, not acompilerOptions.pathsalias.TsconfigResolvertherefore left it as a bare specifier, and, as #1030 measured,importers_of, the impact radius andreferences_tocollapsed at every package boundary. Calls through a package's barrel file (export { can } from './can') stayed bare as well.This PR adds
WorkspaceResolver(code_review_graph/workspace_resolver.py), consulted in_do_resolve_moduleright after tsconfigpathsfor javascript, typescript, tsx and vue imports. It follows the design sketched in #1030:pnpm-workspace.yamlorpackage.jsonwithworkspaces(array or yarn's object form), walking up from the importer but never above the repository root.!negation;**through anos.walkthat never entersnode_modules) and map each member'snameto its directory.@scope/nameplus subpath) and resolve the subpath throughexports: exact keys, condition objects (every reachable target is tried,typeslast), fallback arrays,*patterns with Node'sPATTERN_KEY_COMPAREpriority, andnullblocking. Thensource,module,main,typesandtypings, thensrc/<subpath>and<subpath>.dist/, which a checkout does not contain. A target underdist/,build/,out/orlib/is therefore tried undersrc/with source extensions first (dist/index.d.tsbecomessrc/index.ts;dist/types/index.d.tsbecomessrc/index.tsunless the subpath itself namestypes), then probed as written.A wrong edge is worse than a missing one, so:
package.jsondeclares it (otherwisecomponents/Buttoncould just as well be abaseUrlimport of the importer's ownsrc/components/), and Node builtin names (events,path, ...) never resolve to a workspace member;_file_exists_exact, plus every directory component below the package), so Windows and macOS do not get edges that Linux would not;_path_is_indexedrejects;RecursionError, non-UTF-8 YAML, absolute or..workspace patterns) are caught, logged and cached, so one bad file cannot fail every later parse.The parser change is 12 lines, and tsconfig
pathsstill win when both apply.Measured on public monorepos
Stock 2.3.9 against this branch, full builds of the same checkout:
cd0cc5f(npm workspaces, 16 packages)a70296b(pnpm, 20 packages)IMPORTS_FROMedgesCALLSedgesREFERENCESedgespackages/prisma/index.ts)packages/database/src/index.ts)The 144 still bare in documenso are all
@documenso/prisma/generated/..., which is generated at build time and absent from the checkout, so they stay bare by design. In formbricks, 5 of the 6 left arestorybook/testandstorybook/preview-api, which come from the npmstorybookpackage even though an app in the workspace is also namedstorybook(neither subpath exists in that app), and the last is a Vite?inlineCSS import.How it was tested
tests/test_workspace_resolver.pycovers entry points, workspace discovery, specifiers, caching and the hardening cases above.tests/test_workspace_package_imports.pyis the Workspace package names (pnpm/npm/yarn workspaces) are never resolved — importers_of/impact_radius collapse at every package boundary #1030 regression, run throughCodeParser,GraphStore,query_graph("importers_of"),get_impact_radiusand a realfull_build.python:3.12-slim, this exact commit): 4,311 passed. The only failure,tests/test_e2e_real_repo.py::test_hybrid_search_ranks_impact_radius_nodes_first, fails the same way on unmodifiedstagingin that container.stagingtoo, mostly path-separator assertions such as'C:/...' == 'C:\\...'. The same 103 node IDs fail with and without this branch, and nothing else fails.fcntlerrors indaemon.py.Checklist
uv run pytest tests/ --tb=short -q(see the note above)uv run ruff check code_review_graph/uv run mypy code_review_graph/ --ignore-missing-imports --no-strict-optionalCHANGELOG.md,CLAUDE.md,docs/architecture.md)Out of scope, noticed while measuring
tsconfig alias imports written with NodeNext-style
.jsextensions (@/utils/format.jsforsrc/utils/format.ts) also stay bare. The relative-import branch maps.jsto.ts, butTsconfigResolverdoes not. I can send that as a separate PR.