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

pantsbuild / pants / 22285099215

22 Feb 2026 08:52PM UTC coverage: 75.854% (-17.1%) from 92.936%
22285099215

Pull #23121

github

web-flow
Merge c7299df9c into ba8359840
Pull Request #23121: fix issue with optional fields in dependency validator

28 of 29 new or added lines in 2 files covered. (96.55%)

11174 existing lines in 400 files now uncovered.

53694 of 70786 relevant lines covered (75.85%)

1.88 hits per line

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

85.96
/src/python/pants/backend/javascript/package/rules.py
1
# Copyright 2023 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3
from __future__ import annotations
1✔
4

5
import re
1✔
6
from collections.abc import Iterable
1✔
7
from dataclasses import dataclass
1✔
8
from typing import ClassVar
1✔
9

10
from pants.backend.javascript import install_node_package
1✔
11
from pants.backend.javascript.install_node_package import (
1✔
12
    InstalledNodePackageRequest,
13
    add_sources_to_installed_node_package,
14
)
15
from pants.backend.javascript.nodejs_project_environment import NodeJsProjectEnvironmentProcess
1✔
16
from pants.backend.javascript.package_json import (
1✔
17
    NodeBuildScript,
18
    NodeBuildScriptEntryPointField,
19
    NodeBuildScriptExtraCaches,
20
    NodeBuildScriptExtraEnvVarsField,
21
    NodeBuildScriptOutputDirectoriesField,
22
    NodeBuildScriptOutputFilesField,
23
    NodeBuildScriptSourcesField,
24
    NodePackageNameField,
25
    NodePackageVersionField,
26
    NPMDistributionTarget,
27
    PackageJsonSourceField,
28
)
29
from pants.build_graph.address import Address
1✔
30
from pants.core.goals.package import (
1✔
31
    BuiltPackage,
32
    BuiltPackageArtifact,
33
    OutputPathField,
34
    PackageFieldSet,
35
)
36
from pants.core.target_types import ResourceSourceField
1✔
37
from pants.core.util_rules.env_vars import environment_vars_subset
1✔
38
from pants.engine.env_vars import EnvironmentVarsRequest
1✔
39
from pants.engine.internals.native_engine import AddPrefix
1✔
40
from pants.engine.intrinsics import add_prefix, digest_to_snapshot
1✔
41
from pants.engine.process import ProcessResult, fallible_to_exec_result_or_raise
1✔
42
from pants.engine.rules import Rule, collect_rules, implicitly, rule
1✔
43
from pants.engine.target import GeneratedSources, GenerateSourcesRequest
1✔
44
from pants.engine.unions import UnionRule
1✔
45
from pants.util.frozendict import FrozenDict
1✔
46
from pants.util.logging import LogLevel
1✔
47
from pants.util.strutil import softwrap
1✔
48

49

50
@dataclass(frozen=True)
1✔
51
class NodePackageTarFieldSet(PackageFieldSet):
1✔
52
    required_fields = (PackageJsonSourceField, OutputPathField)
1✔
53
    source: PackageJsonSourceField
1✔
54
    output_path: OutputPathField
1✔
55

56

57
@dataclass(frozen=True)
1✔
58
class NodeBuildScriptPackageFieldSet(PackageFieldSet):
1✔
59
    required_fields = (
1✔
60
        NodeBuildScriptSourcesField,
61
        OutputPathField,
62
        NodeBuildScriptEntryPointField,
63
        NodeBuildScriptOutputFilesField,
64
        NodeBuildScriptOutputDirectoriesField,
65
        NodeBuildScriptExtraCaches,
66
    )
67
    source: NodeBuildScriptSourcesField
1✔
68
    output_path: OutputPathField
1✔
69
    script_name: NodeBuildScriptEntryPointField
1✔
70
    output_directories: NodeBuildScriptOutputDirectoriesField
1✔
71
    output_files: NodeBuildScriptOutputFilesField
1✔
72
    extra_caches: NodeBuildScriptExtraCaches
1✔
73
    extra_env_vars: NodeBuildScriptExtraEnvVarsField
1✔
74

75

76
@dataclass(frozen=True)
1✔
77
class GenerateResourcesFromNodeBuildScriptRequest(GenerateSourcesRequest):
1✔
78
    input = NodeBuildScriptSourcesField
1✔
79
    output = ResourceSourceField
1✔
80

81
    exportable: ClassVar[bool] = True
1✔
82

83

84
@rule
1✔
85
async def pack_node_package_into_tgz_for_publication(
1✔
86
    field_set: NodePackageTarFieldSet,
87
) -> BuiltPackage:
88
    installation = await add_sources_to_installed_node_package(
1✔
89
        InstalledNodePackageRequest(field_set.address)
90
    )
91
    node_package = installation.project_env.ensure_target()
1✔
92
    name = node_package.get(NodePackageNameField).value
1✔
93
    version = node_package.get(NodePackageVersionField).value
1✔
94
    if version is None:
1✔
95
        raise ValueError(
×
96
            f"{field_set.source.file_path}#version must be set in order to package a {NPMDistributionTarget.alias}."
97
        )
98
    archive_file = installation.project_env.project.pack_archive_format.format(name, version)
1✔
99
    result = await fallible_to_exec_result_or_raise(
1✔
100
        **implicitly(
101
            NodeJsProjectEnvironmentProcess(
102
                installation.project_env,
103
                args=("pack",),
104
                description=f"Packaging .tgz archive for {name}@{version}",
105
                input_digest=installation.digest,
106
                output_files=(installation.join_relative_workspace_directory(archive_file),),
107
                level=LogLevel.INFO,
108
            )
109
        )
110
    )
111
    if field_set.output_path.value:
1✔
112
        output_path = field_set.output_path.value_or_default(file_ending=None)
1✔
113
        digest = await add_prefix(AddPrefix(result.output_digest, output_path))
1✔
114
    else:
115
        digest = result.output_digest
×
116

117
    return BuiltPackage(
1✔
118
        digest, (BuiltPackageArtifact(archive_file, tuple(result.stderr.decode().splitlines())),)
119
    )
120

121

122
_NOT_ALPHANUMERIC = re.compile("[^0-9a-zA-Z]+")
1✔
123

124

125
@dataclass(frozen=True)
1✔
126
class NodeBuildScriptResult:
1✔
127
    process: ProcessResult
1✔
128
    project_directory: str
1✔
129

130

131
@dataclass(frozen=True)
1✔
132
class NodeBuildScriptRequest:
1✔
133
    address: Address
1✔
134
    output_files: tuple[str, ...]
1✔
135
    output_directories: tuple[str, ...]
1✔
136
    script_name: str
1✔
137
    extra_caches: tuple[str, ...]
1✔
138
    extra_env_vars: tuple[str, ...]
1✔
139

140
    def __post_init__(self) -> None:
1✔
141
        if not (self.output_directories or self.output_files):
1✔
142
            raise ValueError(
×
143
                softwrap(
144
                    f"""
145
                    Neither the {NodeBuildScriptOutputDirectoriesField.alias} nor the
146
                    {NodeBuildScriptOutputFilesField.alias} field was provided.
147

148
                    One of the fields have to be set, or else the `{NodeBuildScript.alias}`
149
                    output will not be captured for further use in the build.
150
                    """
151
                )
152
            )
153

154
    @classmethod
1✔
155
    def from_generate_request(
1✔
156
        cls, req: GenerateResourcesFromNodeBuildScriptRequest
157
    ) -> NodeBuildScriptRequest:
158
        return cls(
1✔
159
            address=req.protocol_target.address,
160
            output_files=req.protocol_target[NodeBuildScriptOutputFilesField].value or (),
161
            output_directories=req.protocol_target[NodeBuildScriptOutputDirectoriesField].value
162
            or (),
163
            script_name=req.protocol_target[NodeBuildScriptEntryPointField].value,
164
            extra_caches=req.protocol_target[NodeBuildScriptExtraCaches].value or (),
165
            extra_env_vars=req.protocol_target[NodeBuildScriptExtraEnvVarsField].value or (),
166
        )
167

168
    @classmethod
1✔
169
    def from_package_request(cls, req: NodeBuildScriptPackageFieldSet) -> NodeBuildScriptRequest:
1✔
UNCOV
170
        return cls(
×
171
            address=req.address,
172
            output_files=req.output_files.value or (),
173
            output_directories=req.output_directories.value or (),
174
            script_name=req.script_name.value,
175
            extra_caches=req.extra_caches.value or (),
176
            extra_env_vars=req.extra_env_vars.value or (),
177
        )
178

179
    def get_paths(self) -> Iterable[str]:
1✔
UNCOV
180
        yield from self.output_directories
×
UNCOV
181
        yield from self.output_files
×
182

183

184
@rule
1✔
185
async def run_node_build_script(req: NodeBuildScriptRequest) -> NodeBuildScriptResult:
1✔
186
    installation = await add_sources_to_installed_node_package(
1✔
187
        InstalledNodePackageRequest(req.address)
188
    )
189
    output_files = req.output_files
1✔
190
    output_dirs = req.output_directories
1✔
191
    script_name = req.script_name
1✔
192
    extra_caches = req.extra_caches
1✔
193
    extra_env_vars = req.extra_env_vars
1✔
194

195
    def cache_name(cache_path: str) -> str:
1✔
UNCOV
196
        parts = (installation.project_env.package_dir(), script_name, cache_path)
×
UNCOV
197
        return "_".join(_NOT_ALPHANUMERIC.sub("_", part) for part in parts if part)
×
198

199
    args = ("run", script_name)
1✔
200
    target_env_vars = await environment_vars_subset(
1✔
201
        EnvironmentVarsRequest(extra_env_vars), **implicitly()
202
    )
203
    result = await fallible_to_exec_result_or_raise(
1✔
204
        **implicitly(
205
            NodeJsProjectEnvironmentProcess(
206
                installation.project_env,
207
                args=filter(None, args),
208
                description=f"Running node build script '{script_name}'.",
209
                input_digest=installation.digest,
210
                output_files=tuple(
211
                    installation.join_relative_workspace_directory(file)
212
                    for file in output_files or ()
213
                ),
214
                output_directories=tuple(
215
                    installation.join_relative_workspace_directory(directory)
216
                    for directory in output_dirs or ()
217
                ),
218
                level=LogLevel.INFO,
219
                per_package_caches=FrozenDict(
220
                    {cache_name(extra_cache): extra_cache for extra_cache in extra_caches or ()}
221
                ),
222
                extra_env=target_env_vars,
223
            )
224
        )
225
    )
226

227
    return NodeBuildScriptResult(result, installation.project_dir)
1✔
228

229

230
@rule
1✔
231
async def generate_resources_from_node_build_script(
1✔
232
    req: GenerateResourcesFromNodeBuildScriptRequest,
233
) -> GeneratedSources:
234
    result = await run_node_build_script(NodeBuildScriptRequest.from_generate_request(req))
1✔
235
    return GeneratedSources(
1✔
236
        await digest_to_snapshot(
237
            **implicitly(AddPrefix(result.process.output_digest, result.project_directory))
238
        )
239
    )
240

241

242
@rule
1✔
243
async def generate_package_artifact_from_node_build_script(
1✔
244
    req: NodeBuildScriptPackageFieldSet,
245
) -> BuiltPackage:
UNCOV
246
    request = NodeBuildScriptRequest.from_package_request(req)
×
UNCOV
247
    result = await run_node_build_script(request)
×
UNCOV
248
    if req.output_path.value:
×
UNCOV
249
        output_path = req.output_path.value_or_default(file_ending=None)
×
UNCOV
250
        digest = await add_prefix(AddPrefix(result.process.output_digest, output_path))
×
251
    else:
252
        digest = result.process.output_digest
×
UNCOV
253
    artifacts = tuple(BuiltPackageArtifact(path) for path in request.get_paths())
×
UNCOV
254
    return BuiltPackage(digest, artifacts)
×
255

256

257
def rules() -> Iterable[Rule | UnionRule]:
1✔
258
    return [
1✔
259
        *collect_rules(),
260
        *install_node_package.rules(),
261
        UnionRule(PackageFieldSet, NodePackageTarFieldSet),
262
        UnionRule(PackageFieldSet, NodeBuildScriptPackageFieldSet),
263
        UnionRule(GenerateSourcesRequest, GenerateResourcesFromNodeBuildScriptRequest),
264
    ]
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

© 2026 Coveralls, Inc