Skip to content

Conversation

leosvelperez
Copy link
Member

@leosvelperez leosvelperez commented Jun 4, 2025

Current Behavior

When using Node.js subpath imports with the same name in different projects, the Nx graph incorrectly picks up seemingly random dependencies between projects that shouldn't exist.

This happens because the result of the resolution performed with TypeScript is cached using the import path as the cache key. The problem with that is that multiple projects can have the same subpath import name pointing to internal files of the project, so when the resolution is made for the first project (say project1), the result will be cached and incorrectly reused for other projects with the same subpath import name. So, all projects with the same subpath import name would resolve the dependency to the first project (project1).

The same could happen to projects with TS path mappings defined in the project's tsconfig file. These TS path mappings would only apply to the project internally and therefore, other unrelated projects could also define them with the same name pointing to different files.

Expected Behavior

The Node.js subpath imports should be handled correctly. The TypeScript resolution result should be cached safely and scoped to the project from which the import is being done.

Related Issue(s)

Fixes #31223

@leosvelperez leosvelperez self-assigned this Jun 4, 2025
@leosvelperez leosvelperez requested a review from a team as a code owner June 4, 2025 15:07
@leosvelperez leosvelperez requested a review from xiongemi June 4, 2025 15:07
Copy link

vercel bot commented Jun 4, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Updated (UTC)
nx-dev ✅ Ready (Inspect) Visit Preview Jun 6, 2025 11:16am
Copy link
Contributor

nx-cloud bot commented Jun 4, 2025

View your CI Pipeline Execution ↗ for commit 042f92e.

Command Status Duration Result
nx affected --targets=lint,test,build,e2e,e2e-c... ✅ Succeeded 5m 39s View ↗
nx run-many -t check-imports check-commit check... ✅ Succeeded 18s View ↗
nx-cloud record -- nx-cloud conformance:check ✅ Succeeded 2s View ↗
nx-cloud record -- nx format:check ✅ Succeeded 5s View ↗
nx-cloud record -- nx sync:check ✅ Succeeded 4s View ↗
nx documentation ✅ Succeeded <1s View ↗

☁️ Nx Cloud last updated this comment at 2025-06-06 11:59:02 UTC

if (this.typescriptResolutionCache.has(normalizedImportExpr)) {
resolvedModule = this.typescriptResolutionCache.get(normalizedImportExpr);
let cacheKey: string;
if (normalizedImportExpr.startsWith('#')) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason why this is only done for imports that start with #? I know there's a convention that frameworks set this but is there something special about # in the typescript spec?

Copy link
Member Author

@leosvelperez leosvelperez Jun 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's part of the Node.js specification for subpath imports. These imports are aliases that can only be used internally for the project where they are specified. So, they don't need to be unique across projects, and that's why we were getting the wrong cache hits and needed to special case their cache key to scope it to the project.

If your question is whether other imports without # could also be internal to a project, I guess it could happen if you have TS path mappings defined in the project's tsconfig file. In that case, those TS path mappings only apply to the project and therefore, you could have TS path mappings with the same name in other projects.

I'll update this to always generate the cache key after the project root. It's a safer approach.

Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR scopes TypeScript module resolution cache keys by project for Node.js subpath imports and extends the resolution logic to correctly handle internal and external imports, along with adding an end-to-end test to verify the behavior.

  • Scope cache keys for subpath imports (#…) to the importing project root
  • Update resolution flow to distinguish between internal project files and npm packages
  • Add an e2e test for Node.js subpath import handling

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
packages/nx/src/plugins/js/project-graph/build-dependencies/target-project-locator.ts Scoped cache keys, refactored resolution logic to handle ESM and external deps
e2e/nx/src/graph-ts-solution.test.ts Added test case for subpath imports in the TS solution graph
Comments suppressed due to low confidence (4)

packages/nx/src/plugins/js/project-graph/build-dependencies/target-project-locator.ts:371

  • [nitpick] The variable name cacheKey is generic; consider renaming it to resolutionCacheKey or similar to clarify its purpose in module resolution.
let cacheKey: string;

packages/nx/src/plugins/js/project-graph/build-dependencies/target-project-locator.ts:146

  • [nitpick] Link this TODO to a specific issue or tracking ID to improve traceability and avoid losing context when the ESM rework is performed.
// TODO: this can be removed once we rework resolveImportWithRequire below

e2e/nx/src/graph-ts-solution.test.ts:323

  • [nitpick] The test verifies specific subpath imports (#file2, #file3) but does not validate the wildcard #* mapping. Consider adding assertions to cover wildcard subpath import resolution.
it('should detect dependencies correctly when using Node.js subpath imports', () => {

packages/nx/src/plugins/js/project-graph/build-dependencies/target-project-locator.ts:386

  • [nitpick] The indentation of this else block closing brace is inconsistent with the opening if. Align the brace with the if statement for better readability.
      } else {
@leosvelperez leosvelperez force-pushed the core/subpath-import-paths branch from cdbc88f to 965eba3 Compare June 6, 2025 07:49
@leosvelperez leosvelperez changed the title fix(core): handle node.js subpath imports correctly when processing the graph Jun 6, 2025
@leosvelperez leosvelperez requested a review from FrozenPandaz June 6, 2025 07:53
@leosvelperez leosvelperez force-pushed the core/subpath-import-paths branch from 965eba3 to e5a60c4 Compare June 6, 2025 08:50
@leosvelperez leosvelperez requested a review from a team as a code owner June 6, 2025 08:50
@leosvelperez leosvelperez force-pushed the core/subpath-import-paths branch from e5a60c4 to bca8217 Compare June 6, 2025 09:09
@leosvelperez leosvelperez force-pushed the core/subpath-import-paths branch from 10f2034 to 042f92e Compare June 6, 2025 11:03
@FrozenPandaz FrozenPandaz merged commit 8941362 into master Jun 6, 2025
7 checks passed
@FrozenPandaz FrozenPandaz deleted the core/subpath-import-paths branch June 6, 2025 17:18
@laneysmith
Copy link
Contributor

laneysmith commented Jun 6, 2025

Thank you, @leosvelperez & @FrozenPandaz ! 🙌

Copy link
Contributor

This pull request has already been merged/closed. If you experience issues related to these changes, please open a new issue referencing this pull request.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jun 12, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

8 participants