• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pantsbuild / pants / 18427419357

11 Oct 2025 09:11AM UTC coverage: 80.25%. First build
18427419357

Pull #22461

github

web-flow
Merge 1d59c7492 into 4846d0735
Pull Request #22461: Add typescript typechecking support

410 of 531 new or added lines in 10 files covered. (77.21%)

77631 of 96737 relevant lines covered (80.25%)

3.35 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.32
/src/python/pants/backend/javascript/subsystems/nodejs_tool.py
1
# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
from __future__ import annotations
8✔
5

6
import re
8✔
7
from collections.abc import Iterable, Mapping
8✔
8
from dataclasses import dataclass, field
8✔
9
from typing import ClassVar
8✔
10

11
from pants.backend.javascript import install_node_package, nodejs_project_environment
8✔
12
from pants.backend.javascript.install_node_package import (
8✔
13
    InstalledNodePackageRequest,
14
    install_node_packages_for_address,
15
)
16
from pants.backend.javascript.nodejs_project_environment import (
8✔
17
    NodeJsProjectEnvironmentProcess,
18
    setup_nodejs_project_environment_process,
19
)
20
from pants.backend.javascript.package_manager import PackageManager
8✔
21
from pants.backend.javascript.resolve import (
8✔
22
    resolve_to_first_party_node_package,
23
    resolve_to_projects,
24
)
25
from pants.backend.javascript.subsystems.nodejs import (
8✔
26
    NodeJS,
27
    NodeJSToolProcess,
28
    setup_node_tool_process,
29
)
30
from pants.engine.internals.native_engine import Digest, MergeDigests
8✔
31
from pants.engine.intrinsics import merge_digests
8✔
32
from pants.engine.process import Process
8✔
33
from pants.engine.rules import Rule, collect_rules, implicitly, rule
8✔
34
from pants.engine.unions import UnionRule
8✔
35
from pants.option.option_types import StrOption
8✔
36
from pants.option.subsystem import Subsystem
8✔
37
from pants.util.frozendict import FrozenDict
8✔
38
from pants.util.logging import LogLevel
8✔
39
from pants.util.strutil import softwrap
8✔
40

41

42
class NodeJSToolBase(Subsystem):
8✔
43
    # Subclasses must set.
44
    default_version: ClassVar[str]
8✔
45

46
    version = StrOption(
8✔
47
        advanced=True,
48
        default=lambda cls: cls.default_version,
49
        help="Version string for the tool in the form package@version (e.g. prettier@3.5.2)",
50
    )
51

52
    _binary_name = StrOption(
8✔
53
        advanced=True,
54
        default=None,
55
        help="Override the binary to run for this tool. Defaults to the package name.",
56
    )
57

58
    @property
8✔
59
    def binary_name(self) -> str:
8✔
60
        """The binary name to run for this tool."""
61
        if self._binary_name:
1✔
62
            return self._binary_name
1✔
63

64
        # For scoped packages (@scope/package), use the scope name (often matches the binary)
65
        # For regular packages, use the full package name
66
        match = re.match(r"^(?:@([^/]+)/[^@]+|([^@]+))", self.version)
1✔
67
        if not match:
1✔
68
            raise ValueError(f"Invalid npm package specification: {self.version}")
1✔
69
        return match.group(1) or match.group(2)
1✔
70

71
    install_from_resolve = StrOption(
8✔
72
        advanced=True,
73
        default=None,
74
        help=lambda cls: softwrap(
75
            f"""\
76
            If specified, install the tool using the lockfile for this named resolve,
77
            instead of the version configured in this subsystem.
78

79
            If unspecified, the tool will use the default configured package manager
80
            [{NodeJS.options_scope}].package_manager`, and install the tool without a
81
            lockfile.
82
            """
83
        ),
84
    )
85

86
    def request(
8✔
87
        self,
88
        args: tuple[str, ...],
89
        input_digest: Digest,
90
        description: str,
91
        level: LogLevel,
92
        output_files: tuple[str, ...] = (),
93
        output_directories: tuple[str, ...] = (),
94
        append_only_caches: FrozenDict[str, str] | None = None,
95
        project_caches: FrozenDict[str, str] | None = None,
96
        timeout_seconds: int | None = None,
97
        extra_env: Mapping[str, str] | None = None,
98
    ) -> NodeJSToolRequest:
99
        return NodeJSToolRequest(
1✔
100
            package=self.version,
101
            binary_name=self.binary_name,
102
            resolve=self.install_from_resolve,
103
            args=args,
104
            input_digest=input_digest,
105
            description=description,
106
            level=level,
107
            output_files=output_files,
108
            output_directories=output_directories,
109
            append_only_caches=append_only_caches or FrozenDict(),
110
            project_caches=project_caches or FrozenDict(),
111
            timeout_seconds=timeout_seconds,
112
            extra_env=extra_env or FrozenDict(),
113
            options_scope=self.options_scope,
114
        )
115

116

117
@dataclass(frozen=True)
8✔
118
class NodeJSToolRequest:
8✔
119
    package: str
8✔
120
    binary_name: str
8✔
121
    resolve: str | None
8✔
122
    args: tuple[str, ...]
8✔
123
    input_digest: Digest
8✔
124
    description: str
8✔
125
    level: LogLevel
8✔
126
    options_scope: str
8✔
127
    output_files: tuple[str, ...] = ()
8✔
128
    output_directories: tuple[str, ...] = ()
8✔
129
    append_only_caches: FrozenDict[str, str] = field(default_factory=FrozenDict)
8✔
130
    project_caches: FrozenDict[str, str] = field(default_factory=FrozenDict)
8✔
131
    timeout_seconds: int | None = None
8✔
132
    extra_env: Mapping[str, str] = field(default_factory=FrozenDict)
8✔
133

134

135
async def _run_tool_without_resolve(request: NodeJSToolRequest, nodejs: NodeJS) -> Process:
8✔
136
    pkg_manager_version = nodejs.package_managers.get(nodejs.package_manager)
×
137
    pkg_manager_and_version = nodejs.default_package_manager
×
138
    if pkg_manager_version is None or pkg_manager_and_version is None:
×
139
        # Occurs when a user configures a custom package manager but without a resolve.
140
        # Corepack requires a package.json to make a decision on a "good known release".
141
        raise ValueError(
×
142
            softwrap(
143
                f"""
144
                Version for {nodejs.package_manager} has to be configured
145
                in [{nodejs.options_scope}].package_managers when running
146
                the tool '{request.binary_name}' without setting [{request.options_scope}].install_from_resolve.
147
                """
148
            )
149
        )
150
    pkg_manager = PackageManager.from_string(pkg_manager_and_version)
×
151

152
    return await setup_node_tool_process(
×
153
        NodeJSToolProcess(
154
            pkg_manager.name,
155
            pkg_manager.version,
156
            args=pkg_manager.make_download_and_execute_args(
157
                request.package,
158
                request.binary_name,
159
                request.args,
160
            ),
161
            description=request.description,
162
            input_digest=request.input_digest,
163
            output_files=request.output_files,
164
            output_directories=request.output_directories,
165
            append_only_caches=request.append_only_caches,
166
            timeout_seconds=request.timeout_seconds,
167
            extra_env=FrozenDict({**pkg_manager.extra_env, **request.extra_env}),
168
        ),
169
        **implicitly(),
170
    )
171

172

173
async def _run_tool_with_resolve(request: NodeJSToolRequest, resolve: str) -> Process:
8✔
174
    resolves = await resolve_to_projects(**implicitly())
×
175

176
    if request.resolve not in resolves:
×
177
        reason = (
×
178
            f"Available resolves are {', '.join(resolves.keys())}."
179
            if resolves
180
            else "This project contains no resolves."
181
        )
182
        raise ValueError(f"{resolve} is not a named NodeJS resolve. {reason}")
×
183

184
    all_first_party = await resolve_to_first_party_node_package(**implicitly())
×
185
    package_for_resolve = all_first_party[resolve]
×
186
    project = resolves[resolve]
×
187

188
    installed = await install_node_packages_for_address(
×
189
        InstalledNodePackageRequest(package_for_resolve.address), **implicitly()
190
    )
191
    merged_input_digest = await merge_digests(
×
192
        MergeDigests([request.input_digest, installed.digest])
193
    )
194

NEW
195
    return await setup_nodejs_project_environment_process(
×
196
        NodeJsProjectEnvironmentProcess(
197
            env=installed.project_env,
198
            args=(*project.package_manager.execute_args, request.binary_name, *request.args),
199
            description=request.description,
200
            input_digest=merged_input_digest,
201
            output_files=request.output_files,
202
            output_directories=request.output_directories,
203
            per_package_caches=request.append_only_caches,
204
            project_caches=request.project_caches,
205
            timeout_seconds=request.timeout_seconds,
206
            extra_env=FrozenDict(request.extra_env),
207
        ),
208
        **implicitly(),
209
    )
210

211

212
@rule
8✔
213
async def prepare_tool_process(request: NodeJSToolRequest, nodejs: NodeJS) -> Process:
8✔
214
    if request.resolve is None:
×
215
        return await _run_tool_without_resolve(request, nodejs)
×
216
    return await _run_tool_with_resolve(request, request.resolve)
×
217

218

219
def rules() -> Iterable[Rule | UnionRule]:
8✔
220
    return [*collect_rules(), *nodejs_project_environment.rules(), *install_node_package.rules()]
8✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc