diff --git a/docs/notes/2.32.x.md b/docs/notes/2.32.x.md index d4d59d790d7..5317632c99e 100644 --- a/docs/notes/2.32.x.md +++ b/docs/notes/2.32.x.md @@ -158,6 +158,8 @@ The `runtime_package_dependencies` of `python_test` now traverse generic `target #### Javascript +Fixed a bug where workspace member `node_modules/` directories were excluded from the sandbox. Packages that npm places in a workspace member's `node_modules/` (e.g. due to a version conflict preventing hoisting) are now correctly available in the sandbox. + #### TypeScript #### Go diff --git a/src/python/pants/backend/javascript/nodejs_project_environment.py b/src/python/pants/backend/javascript/nodejs_project_environment.py index 0767e0752c0..4593e58c346 100644 --- a/src/python/pants/backend/javascript/nodejs_project_environment.py +++ b/src/python/pants/backend/javascript/nodejs_project_environment.py @@ -62,8 +62,11 @@ def root_dir(self) -> str: @property def node_modules_directories(self) -> Iterable[str]: yield "node_modules" - if self.package and not self.project.single_workspace: - yield os.path.join(self.relative_workspace_directory(), "node_modules") + if not self.project.single_workspace: + for workspace in self.project.workspaces: + if workspace.root_dir != self.project.root_dir: + rel_dir = fast_relpath(workspace.root_dir, self.project.root_dir) + yield os.path.join(rel_dir, "node_modules") @property def target(self) -> Target | None: diff --git a/src/python/pants/backend/typescript/goals/check_test.py b/src/python/pants/backend/typescript/goals/check_test.py index 3123c09a0fe..f741bbfa19c 100644 --- a/src/python/pants/backend/typescript/goals/check_test.py +++ b/src/python/pants/backend/typescript/goals/check_test.py @@ -595,6 +595,43 @@ def test_check_javascript_enabled_via_tsconfig( assert "Type 'number' is not assignable to type 'string'" in results.results[0].stdout +def test_typescript_check_resolves_packages_from_workspace_member_node_modules() -> None: + """Regression test for bug: workspace member node_modules is excluded from tsc sandbox. + + When an npm workspace member has a dependency that npm places in + `/node_modules/` rather than hoisting it to root `node_modules/` + (e.g. due to a version conflict with the root), that package is absent from the + pants tsc sandbox. + + In this test project, `clsx@1.2.1` is installed only at `member/node_modules/clsx` + (the lockfile explicitly does not hoist it to root, simulating a version conflict + scenario). pants runs `npm ci` from the resolve root, which correctly installs clsx + into `member/node_modules/clsx`. However, the sandbox snapshot captures only + `node_modules/` (the root), never `member/node_modules/`. + """ + rule_runner = _create_rule_runner("npm") + test_files = _load_project_test_files("workspace_member_node_modules") + rule_runner.write_files(test_files) + + target = rule_runner.get_target( + Address( + "workspace_member_node_modules/member/src", + target_name="ts_sources", + relative_file_path="index.ts", + ) + ) + field_set = TypeScriptCheckFieldSet.create(target) + request = TypeScriptCheckRequest([field_set]) + results = rule_runner.request(CheckResults, [request]) + + assert len(results.results) == 1 + result = results.results[0] + assert result.exit_code == 0, ( + f"TypeScript check failed - workspace member node_modules likely missing from sandbox.\n" + f"stdout: {result.stdout}\nstderr: {result.stderr}" + ) + + def test_check_javascript_disabled_via_tsconfig( basic_rule_runner: RuleRunnerWithProjectAndPackageManager, ) -> None: diff --git a/src/python/pants/backend/typescript/test_resources/workspace_member_node_modules/BUILD b/src/python/pants/backend/typescript/test_resources/workspace_member_node_modules/BUILD new file mode 100644 index 00000000000..4ab13ae5cfd --- /dev/null +++ b/src/python/pants/backend/typescript/test_resources/workspace_member_node_modules/BUILD @@ -0,0 +1,3 @@ +# Copyright 2025 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). +package_json() diff --git a/src/python/pants/backend/typescript/test_resources/workspace_member_node_modules/member/BUILD b/src/python/pants/backend/typescript/test_resources/workspace_member_node_modules/member/BUILD new file mode 100644 index 00000000000..4ab13ae5cfd --- /dev/null +++ b/src/python/pants/backend/typescript/test_resources/workspace_member_node_modules/member/BUILD @@ -0,0 +1,3 @@ +# Copyright 2025 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). +package_json() diff --git a/src/python/pants/backend/typescript/test_resources/workspace_member_node_modules/member/package.json b/src/python/pants/backend/typescript/test_resources/workspace_member_node_modules/member/package.json new file mode 100644 index 00000000000..0287d3f7ddd --- /dev/null +++ b/src/python/pants/backend/typescript/test_resources/workspace_member_node_modules/member/package.json @@ -0,0 +1,8 @@ +{ + "name": "@test/member", + "version": "1.0.0", + "private": true, + "dependencies": { + "clsx": "1.2.1" + } +} diff --git a/src/python/pants/backend/typescript/test_resources/workspace_member_node_modules/member/src/BUILD b/src/python/pants/backend/typescript/test_resources/workspace_member_node_modules/member/src/BUILD new file mode 100644 index 00000000000..c6cdfaffab3 --- /dev/null +++ b/src/python/pants/backend/typescript/test_resources/workspace_member_node_modules/member/src/BUILD @@ -0,0 +1,3 @@ +# Copyright 2025 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). +typescript_sources(name="ts_sources") diff --git a/src/python/pants/backend/typescript/test_resources/workspace_member_node_modules/member/src/index.ts b/src/python/pants/backend/typescript/test_resources/workspace_member_node_modules/member/src/index.ts new file mode 100644 index 00000000000..88eca075048 --- /dev/null +++ b/src/python/pants/backend/typescript/test_resources/workspace_member_node_modules/member/src/index.ts @@ -0,0 +1,8 @@ +// Imports clsx which is installed only in member/node_modules/clsx (not at root). +// If pants incorrectly omits member/node_modules from the tsc sandbox, this fails with: +// TS2307: Cannot find module 'clsx' or its corresponding type declarations. +import { clsx } from 'clsx'; + +export function buildClassName(...args: Parameters): string { + return clsx(...args); +} diff --git a/src/python/pants/backend/typescript/test_resources/workspace_member_node_modules/member/tsconfig.json b/src/python/pants/backend/typescript/test_resources/workspace_member_node_modules/member/tsconfig.json new file mode 100644 index 00000000000..bafb86d517a --- /dev/null +++ b/src/python/pants/backend/typescript/test_resources/workspace_member_node_modules/member/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["src/**/*"], + "references": [] +} diff --git a/src/python/pants/backend/typescript/test_resources/workspace_member_node_modules/package-lock.json b/src/python/pants/backend/typescript/test_resources/workspace_member_node_modules/package-lock.json new file mode 100644 index 00000000000..68f30a648d1 --- /dev/null +++ b/src/python/pants/backend/typescript/test_resources/workspace_member_node_modules/package-lock.json @@ -0,0 +1,52 @@ +{ + "name": "@test/workspace-member-node-modules", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@test/workspace-member-node-modules", + "version": "1.0.0", + "workspaces": [ + "member" + ], + "devDependencies": { + "typescript": "5.9.3" + } + }, + "member": { + "name": "@test/member", + "version": "1.0.0", + "dependencies": { + "clsx": "1.2.1" + } + }, + "node_modules/@test/member": { + "resolved": "member", + "link": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "member/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/src/python/pants/backend/typescript/test_resources/workspace_member_node_modules/package.json b/src/python/pants/backend/typescript/test_resources/workspace_member_node_modules/package.json new file mode 100644 index 00000000000..d739d218506 --- /dev/null +++ b/src/python/pants/backend/typescript/test_resources/workspace_member_node_modules/package.json @@ -0,0 +1,11 @@ +{ + "name": "@test/workspace-member-node-modules", + "version": "1.0.0", + "private": true, + "workspaces": [ + "member" + ], + "devDependencies": { + "typescript": "5.9.3" + } +} diff --git a/src/python/pants/backend/typescript/test_resources/workspace_member_node_modules/tsconfig.json b/src/python/pants/backend/typescript/test_resources/workspace_member_node_modules/tsconfig.json new file mode 100644 index 00000000000..2b311e7b61e --- /dev/null +++ b/src/python/pants/backend/typescript/test_resources/workspace_member_node_modules/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "composite": true, + "declaration": true, + "outDir": "dist", + "rootDir": "." + }, + "references": [ + { "path": "./member" } + ] +}