Compare commits

...

1 Commits

Author SHA1 Message Date
FrozenPandaz 4e0be14679 fix(core): fall back to workspace project lookup when require resolves a hoisted copy
`findProjectFromImport` previously returned undefined whenever
`resolveImportWithRequire` succeeded but the resolved path lived under
`node_modules/`. In pnpm workspaces, an import like `@nx/jest` from a
package that doesn't declare it as a dep can resolve into `.pnpm/` (the
hoisted external copy) instead of the workspace symlink — silently
dropping the edge to the corresponding workspace project.

Now, if the resolved path doesn't map to a project, fall through to
`findImportInWorkspaceProjects` (the workspace-package-metadata lookup),
which correctly resolves these names to their workspace projects.

Verified on this repo: adds `eslint -> jest` (static) and
`@nx/nx-source -> eslint-rules` (static) edges that were previously
dropped. No edges removed. All 208 target-project-locator tests pass.
2026-04-22 13:49:08 -04:00
2 changed files with 56 additions and 2 deletions
@@ -1232,6 +1232,56 @@ describe('TargetProjectLocator', () => {
});
});
describe('findProjectFromImport workspace fallback', () => {
it('should fall back to findImportInWorkspaceProjects when require resolves into node_modules', () => {
// Scenario: workspace package `@org/pkg1` is imported from a file in
// another workspace project that does NOT declare `@org/pkg1` as a
// dependency. In pnpm layouts, Node's resolver can walk up and find a
// hoisted copy of the package in `node_modules/.pnpm/...`, causing
// `resolveImportWithRequire` to return a path under `node_modules/`.
// The edge to `pkg1` would previously be dropped; it should now be
// recovered via the workspace-metadata fallback.
const projects: Record<string, ProjectGraphProjectNode> = {
pkg1: {
name: 'pkg1',
type: 'lib',
data: {
root: 'pkg1',
metadata: {
js: {
packageName: '@org/pkg1',
packageExports: 'dist/index.js',
isInPackageManagerWorkspaces: true,
},
},
},
},
consumer: {
name: 'consumer',
type: 'lib',
data: { root: 'consumer' },
},
};
const locator = new TargetProjectLocator(projects, {}, new Map());
// Force the require-resolution step to return a node_modules path,
// emulating a hoisted pnpm copy.
jest
.spyOn(locator as any, 'resolveImportWithRequire')
.mockReturnValue(
'node_modules/.pnpm/@org+pkg1@1.0.0/node_modules/@org/pkg1/dist/index.js'
);
const result = locator.findProjectFromImport(
'@org/pkg1',
'consumer/src/index.ts'
);
expect(result).toEqual('pkg1');
});
});
describe('findImportInWorkspaceProjects', () => {
it.each`
exports | importPath
@@ -165,11 +165,15 @@ export class TargetProjectLocator {
filePath
);
return this.findProjectOfResolvedModule(resolvedModule);
const resolvedProject = this.findProjectOfResolvedModule(resolvedModule);
if (resolvedProject) {
return resolvedProject;
}
} catch {}
// fall back to see if it's a locally linked workspace project where the
// output might not exist yet
// output might not exist yet (e.g. the import resolved into node_modules
// via a hoisted copy instead of the workspace symlink)
const localProject = this.findImportInWorkspaceProjects(importExpr);
if (localProject) {
return localProject;