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

pantsbuild / pants / 18252174847

05 Oct 2025 01:36AM UTC coverage: 43.382% (-36.9%) from 80.261%
18252174847

push

github

web-flow
run tests on mac arm (#22717)

Just doing the minimal to pull forward the x86_64 pattern.

ref #20993

25776 of 59416 relevant lines covered (43.38%)

1.3 hits per line

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

0.0
/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
×
5

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

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

41

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

46
    version = StrOption(
×
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(
×
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
×
59
    def binary_name(self) -> str:
×
60
        """The binary name to run for this tool."""
61
        if self._binary_name:
×
62
            return self._binary_name
×
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)
×
67
        if not match:
×
68
            raise ValueError(f"Invalid npm package specification: {self.version}")
×
69
        return match.group(1) or match.group(2)
×
70

71
    install_from_resolve = StrOption(
×
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(
×
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
        timeout_seconds: int | None = None,
96
        extra_env: Mapping[str, str] | None = None,
97
    ) -> NodeJSToolRequest:
98
        return NodeJSToolRequest(
×
99
            package=self.version,
100
            binary_name=self.binary_name,
101
            resolve=self.install_from_resolve,
102
            args=args,
103
            input_digest=input_digest,
104
            description=description,
105
            level=level,
106
            output_files=output_files,
107
            output_directories=output_directories,
108
            append_only_caches=append_only_caches or FrozenDict(),
109
            timeout_seconds=timeout_seconds,
110
            extra_env=extra_env or FrozenDict(),
111
            options_scope=self.options_scope,
112
        )
113

114

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

131

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

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

169

170
async def _run_tool_with_resolve(request: NodeJSToolRequest, resolve: str) -> Process:
×
171
    resolves = await resolve_to_projects(**implicitly())
×
172

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

181
    all_first_party = await resolve_to_first_party_node_package(**implicitly())
×
182
    package_for_resolve = all_first_party[resolve]
×
183
    project = resolves[resolve]
×
184
    installed = await install_node_packages_for_address(
×
185
        InstalledNodePackageRequest(package_for_resolve.address), **implicitly()
186
    )
187
    return await setup_nodejs_project_environment_process(
×
188
        NodeJsProjectEnvironmentProcess(
189
            env=installed.project_env,
190
            args=(*project.package_manager.execute_args, request.binary_name, *request.args),
191
            description=request.description,
192
            input_digest=await merge_digests(
193
                MergeDigests([request.input_digest, installed.digest])
194
            ),
195
            output_files=request.output_files,
196
            output_directories=request.output_directories,
197
            per_package_caches=request.append_only_caches,
198
            timeout_seconds=request.timeout_seconds,
199
            extra_env=FrozenDict(request.extra_env),
200
        ),
201
        **implicitly(),
202
    )
203

204

205
@rule
×
206
async def prepare_tool_process(request: NodeJSToolRequest, nodejs: NodeJS) -> Process:
×
207
    if request.resolve is None:
×
208
        return await _run_tool_without_resolve(request, nodejs)
×
209
    return await _run_tool_with_resolve(request, request.resolve)
×
210

211

212
def rules() -> Iterable[Rule | UnionRule]:
×
213
    return [*collect_rules(), *nodejs_project_environment.rules(), *install_node_package.rules()]
×
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