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

pantsbuild / pants / 19015773527

02 Nov 2025 05:33PM UTC coverage: 17.872% (-62.4%) from 80.3%
19015773527

Pull #22816

github

web-flow
Merge a12d75757 into 6c024e162
Pull Request #22816: Update Pants internal Python to 3.14

4 of 5 new or added lines in 3 files covered. (80.0%)

28452 existing lines in 683 files now uncovered.

9831 of 55007 relevant lines covered (17.87%)

0.18 hits per line

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

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

UNCOV
5
import itertools
×
UNCOV
6
import json
×
UNCOV
7
import logging
×
UNCOV
8
import os.path
×
UNCOV
9
from abc import ABC
×
UNCOV
10
from collections.abc import Iterable, Mapping
×
UNCOV
11
from dataclasses import dataclass, field
×
UNCOV
12
from typing import Any, ClassVar, Literal
×
13

UNCOV
14
import yaml
×
15

UNCOV
16
from pants.backend.project_info import dependencies
×
UNCOV
17
from pants.base.glob_match_error_behavior import GlobMatchErrorBehavior
×
UNCOV
18
from pants.base.specs import AncestorGlobSpec, RawSpecs
×
UNCOV
19
from pants.build_graph.address import Address
×
UNCOV
20
from pants.build_graph.build_file_aliases import BuildFileAliases
×
UNCOV
21
from pants.core.goals.package import OutputPathField
×
UNCOV
22
from pants.core.target_types import (
×
23
    TargetGeneratorSourcesHelperSourcesField,
24
    TargetGeneratorSourcesHelperTarget,
25
)
UNCOV
26
from pants.core.util_rules import stripped_source_files
×
UNCOV
27
from pants.engine import fs
×
UNCOV
28
from pants.engine.collection import Collection, DeduplicatedCollection
×
UNCOV
29
from pants.engine.env_vars import EXTRA_ENV_VARS_USAGE_HELP
×
UNCOV
30
from pants.engine.fs import CreateDigest, FileContent, GlobExpansionConjunction, PathGlobs
×
UNCOV
31
from pants.engine.internals import graph
×
UNCOV
32
from pants.engine.internals.graph import (
×
33
    ResolveAllTargetGeneratorRequests,
34
    resolve_all_generator_target_requests,
35
    resolve_targets,
36
)
UNCOV
37
from pants.engine.internals.native_engine import Digest, Snapshot
×
UNCOV
38
from pants.engine.internals.selectors import concurrently
×
UNCOV
39
from pants.engine.intrinsics import create_digest, digest_to_snapshot, get_digest_contents
×
UNCOV
40
from pants.engine.rules import Rule, collect_rules, implicitly, rule
×
UNCOV
41
from pants.engine.target import (
×
42
    COMMON_TARGET_FIELDS,
43
    AllTargets,
44
    Dependencies,
45
    DependenciesRequest,
46
    DescriptionField,
47
    GeneratedTargets,
48
    GenerateTargetsRequest,
49
    InvalidFieldException,
50
    ScalarField,
51
    SequenceField,
52
    SingleSourceField,
53
    SourcesField,
54
    StringField,
55
    StringSequenceField,
56
    Tags,
57
    Target,
58
    TargetGenerator,
59
    Targets,
60
)
UNCOV
61
from pants.engine.unions import UnionMembership, UnionRule
×
UNCOV
62
from pants.option.bootstrap_options import UnmatchedBuildFileGlobs
×
UNCOV
63
from pants.util.frozendict import FrozenDict
×
UNCOV
64
from pants.util.strutil import help_text, softwrap
×
65

UNCOV
66
_logger = logging.getLogger(__name__)
×
67

68

UNCOV
69
class NodePackageDependenciesField(Dependencies):
×
UNCOV
70
    pass
×
71

72

UNCOV
73
class PackageJsonSourceField(SingleSourceField):
×
UNCOV
74
    default = "package.json"
×
UNCOV
75
    required = False
×
76

77

UNCOV
78
class NodeScript(ABC):
×
UNCOV
79
    entry_point: str
×
UNCOV
80
    alias: ClassVar[str]
×
81

82

UNCOV
83
@dataclass(frozen=True)
×
UNCOV
84
class NodeRunScript(NodeScript):
×
UNCOV
85
    entry_point: str
×
UNCOV
86
    extra_env_vars: tuple[str, ...] = ()
×
87

UNCOV
88
    alias: ClassVar[str] = "node_run_script"
×
89

UNCOV
90
    def __str__(self) -> str:
×
91
        return f'{self.alias}(entry_point="{self.entry_point}", ...)'
×
92

UNCOV
93
    @classmethod
×
UNCOV
94
    def create(
×
95
        cls,
96
        entry_point: str,
97
        extra_env_vars: Iterable[str] = (),
98
    ) -> NodeRunScript:
99
        """A script that can be run directly via the run goal, mapped from the `scripts` section of
100
        a package.json file.
101

102
        This allows running any script defined in package.json directly through pants run.
103
        """
104
        return cls(
×
105
            entry_point=entry_point,
106
            extra_env_vars=tuple(extra_env_vars),
107
        )
108

109

UNCOV
110
@dataclass(frozen=True)
×
UNCOV
111
class NodeBuildScript(NodeScript):
×
UNCOV
112
    entry_point: str
×
UNCOV
113
    output_directories: tuple[str, ...] = ()
×
UNCOV
114
    output_files: tuple[str, ...] = ()
×
UNCOV
115
    extra_caches: tuple[str, ...] = ()
×
UNCOV
116
    extra_env_vars: tuple[str, ...] = ()
×
UNCOV
117
    description: str | None = None
×
UNCOV
118
    tags: tuple[str, ...] = ()
×
119

UNCOV
120
    alias: ClassVar[str] = "node_build_script"
×
121

UNCOV
122
    @classmethod
×
UNCOV
123
    def create(
×
124
        cls,
125
        entry_point: str,
126
        output_directories: Iterable[str] = (),
127
        output_files: Iterable[str] = (),
128
        extra_caches: Iterable[str] = (),
129
        extra_env_vars: Iterable[str] = (),
130
        description: str | None = None,
131
        tags: Iterable[str] = (),
132
    ) -> NodeBuildScript:
133
        """A build script, mapped from the `scripts` section of a package.json file.
134

135
        Either the `output_directories` or the `output_files` argument has to be set to capture the
136
        output artifacts of the build.
137
        """
138

139
        return cls(
×
140
            entry_point=entry_point,
141
            output_directories=tuple(output_directories),
142
            output_files=tuple(output_files),
143
            extra_caches=tuple(extra_caches),
144
            extra_env_vars=tuple(extra_env_vars),
145
            description=description,
146
            tags=tuple(tags),
147
        )
148

149

UNCOV
150
@dataclass(frozen=True)
×
UNCOV
151
class NodeTestScript(NodeScript):
×
UNCOV
152
    entry_point: str = "test"
×
UNCOV
153
    report_args: tuple[str, ...] = ()
×
UNCOV
154
    report_output_files: tuple[str, ...] = ()
×
UNCOV
155
    report_output_directories: tuple[str, ...] = ()
×
UNCOV
156
    coverage_args: tuple[str, ...] = ()
×
UNCOV
157
    coverage_output_files: tuple[str, ...] = ()
×
UNCOV
158
    coverage_output_directories: tuple[str, ...] = ()
×
UNCOV
159
    coverage_entry_point: str | None = None
×
UNCOV
160
    extra_caches: tuple[str, ...] = ()
×
161

UNCOV
162
    alias: ClassVar[str] = "node_test_script"
×
163

UNCOV
164
    def __str__(self) -> str:
×
165
        return f'{self.alias}(entry_point="{self.entry_point}", ...)'
×
166

UNCOV
167
    @classmethod
×
UNCOV
168
    def create(
×
169
        cls,
170
        entry_point: str = "test",
171
        report_args: Iterable[str] = (),
172
        report_output_files: Iterable[str] = (),
173
        report_output_directories: Iterable[str] = (),
174
        coverage_args: Iterable[str] = (),
175
        coverage_output_files: Iterable[str] = (),
176
        coverage_output_directories: Iterable[str] = (),
177
        coverage_entry_point: str | None = None,
178
    ) -> NodeTestScript:
179
        """The test script for this package, mapped from the `scripts` section of a package.json
180
        file. The pointed to script should accept a variadic number of ([ARG]...) path arguments.
181

182
        This entry point is the "test" script, by default.
183
        """
184
        return cls(
×
185
            entry_point=entry_point,
186
            report_args=tuple(report_args),
187
            report_output_files=tuple(report_output_files),
188
            report_output_directories=tuple(report_output_directories),
189
            coverage_args=tuple(coverage_args),
190
            coverage_output_files=tuple(coverage_output_files),
191
            coverage_output_directories=tuple(coverage_output_directories),
192
            coverage_entry_point=coverage_entry_point,
193
        )
194

UNCOV
195
    def supports_coverage(self) -> bool:
×
196
        return bool(self.coverage_entry_point) or bool(self.coverage_args)
×
197

UNCOV
198
    def coverage_globs(self, working_directory: str) -> PathGlobs:
×
199
        return self.coverage_globs_for(
×
200
            working_directory,
201
            self.coverage_output_files,
202
            self.coverage_output_directories,
203
            GlobMatchErrorBehavior.ignore,
204
        )
205

UNCOV
206
    @classmethod
×
UNCOV
207
    def coverage_globs_for(
×
208
        cls,
209
        working_directory: str,
210
        files: tuple[str, ...],
211
        directories: tuple[str, ...],
212
        error_behaviour: GlobMatchErrorBehavior,
213
        conjunction: GlobExpansionConjunction = GlobExpansionConjunction.any_match,
214
        description_of_origin: str | None = None,
215
    ) -> PathGlobs:
216
        dir_globs = (os.path.join(directory, "*") for directory in directories)
×
217
        return PathGlobs(
×
218
            (os.path.join(working_directory, glob) for glob in itertools.chain(files, dir_globs)),
219
            conjunction=conjunction,
220
            glob_match_error_behavior=error_behaviour,
221
            description_of_origin=description_of_origin,
222
        )
223

224

UNCOV
225
class NodePackageScriptsField(SequenceField[NodeScript]):
×
UNCOV
226
    alias = "scripts"
×
UNCOV
227
    expected_element_type = NodeScript
×
228

UNCOV
229
    help = help_text(
×
230
        """
231
        Custom node package manager scripts that should be known
232
        and ran as part of relevant goals.
233

234
        Maps the package.json#scripts section to a cacheable pants invocation.
235
        """
236
    )
UNCOV
237
    expected_type_description = (
×
238
        '[node_build_script(entry_point="build", output_directories=["./dist/"], ...])'
239
    )
UNCOV
240
    default = ()
×
241

UNCOV
242
    @classmethod
×
UNCOV
243
    def compute_value(
×
244
        cls, raw_value: Iterable[Any] | None, address: Address
245
    ) -> tuple[NodeScript, ...] | None:
246
        values = super().compute_value(raw_value, address)
×
247
        test_scripts = [value for value in values or () if isinstance(value, NodeTestScript)]
×
248
        if len(test_scripts) > 1:
×
249
            entry_points = ", ".join(str(script) for script in test_scripts)
×
250
            raise InvalidFieldException(
×
251
                softwrap(
252
                    f"""
253
                    You can only specify one `{NodeTestScript.alias}` per `{PackageJsonTarget.alias}`,
254
                    but the {cls.alias} contains {entry_points}.
255
                    """
256
                )
257
            )
258
        return values
×
259

UNCOV
260
    def build_scripts(self) -> Iterable[NodeBuildScript]:
×
261
        for script in self.value or ():
×
262
            if isinstance(script, NodeBuildScript):
×
263
                yield script
×
264

UNCOV
265
    def get_test_script(self) -> NodeTestScript:
×
266
        for script in self.value or ():
×
267
            if isinstance(script, NodeTestScript):
×
268
                return script
×
269
        return NodeTestScript()
×
270

271

UNCOV
272
class NodePackageTestScriptField(ScalarField[NodeTestScript]):
×
UNCOV
273
    alias = "_node_test_script"
×
UNCOV
274
    expected_type = NodeTestScript
×
UNCOV
275
    expected_type_description = (
×
276
        'node_test_script(entry_point="test", coverage_args="--coverage=true")'
277
    )
UNCOV
278
    default = NodeTestScript()
×
UNCOV
279
    value: NodeTestScript
×
280

281

UNCOV
282
class NodePackageVersionField(StringField):
×
UNCOV
283
    alias = "version"
×
UNCOV
284
    help = help_text(
×
285
        """
286
        Version of the Node package, as specified in the package.json.
287

288
        This field should not be overridden; use the value from target generation.
289
        """
290
    )
UNCOV
291
    required = False
×
UNCOV
292
    value: str | None
×
293

294

UNCOV
295
class NodeThirdPartyPackageVersionField(NodePackageVersionField):
×
UNCOV
296
    alias = "version"
×
UNCOV
297
    help = help_text(
×
298
        """
299
        Version of the Node package, as specified in the package.json.
300

301
        This field should not be overridden; use the value from target generation.
302
        """
303
    )
UNCOV
304
    required = True
×
UNCOV
305
    value: str
×
306

307

UNCOV
308
class NodePackageNameField(StringField):
×
UNCOV
309
    alias = "package"
×
UNCOV
310
    help = help_text(
×
311
        """
312
        Name of the Node package, as specified in the package.json.
313

314
        This field should not be overridden; use the value from target generation.
315
        """
316
    )
UNCOV
317
    required = True
×
UNCOV
318
    value: str
×
319

320

UNCOV
321
class NodeThirdPartyPackageNameField(NodePackageNameField):
×
UNCOV
322
    pass
×
323

324

UNCOV
325
class NodeThirdPartyPackageDependenciesField(Dependencies):
×
UNCOV
326
    pass
×
327

328

UNCOV
329
class NodeThirdPartyPackageTarget(Target):
×
UNCOV
330
    alias = "node_third_party_package"
×
331

UNCOV
332
    help = "A third party node package."
×
333

UNCOV
334
    core_fields = (
×
335
        *COMMON_TARGET_FIELDS,
336
        NodeThirdPartyPackageNameField,
337
        NodeThirdPartyPackageVersionField,
338
        NodeThirdPartyPackageDependenciesField,
339
    )
340

341

UNCOV
342
class NodePackageExtraEnvVarsField(StringSequenceField):
×
UNCOV
343
    alias = "extra_env_vars"
×
UNCOV
344
    help = help_text(
×
345
        f"""
346
        Environment variables to set when running package manager operations.
347

348
        {EXTRA_ENV_VARS_USAGE_HELP}
349
        """
350
    )
UNCOV
351
    required = False
×
352

353

UNCOV
354
class NodePackageTarget(Target):
×
UNCOV
355
    alias = "node_package"
×
356

UNCOV
357
    help = "A first party node package."
×
358

UNCOV
359
    core_fields = (
×
360
        *COMMON_TARGET_FIELDS,
361
        PackageJsonSourceField,
362
        NodePackageNameField,
363
        NodePackageVersionField,
364
        NodePackageDependenciesField,
365
        NodePackageTestScriptField,
366
        NodePackageExtraEnvVarsField,
367
    )
368

369

UNCOV
370
class NPMDistributionTarget(Target):
×
UNCOV
371
    alias = "npm_distribution"
×
372

UNCOV
373
    help = help_text(
×
374
        """
375
        A publishable npm registry distribution, typically a gzipped tarball
376
        of the sources and any resources, but omitting the lockfile.
377

378
        Generated using the projects package manager `pack` implementation.
379
        """
380
    )
381

UNCOV
382
    core_fields = (
×
383
        *COMMON_TARGET_FIELDS,
384
        PackageJsonSourceField,
385
        OutputPathField,
386
    )
387

388

UNCOV
389
class PackageJsonTarget(TargetGenerator):
×
UNCOV
390
    alias = "package_json"
×
UNCOV
391
    core_fields = (
×
392
        *COMMON_TARGET_FIELDS,
393
        PackageJsonSourceField,
394
        NodePackageScriptsField,
395
        NodePackageExtraEnvVarsField,
396
    )
UNCOV
397
    help = help_text(
×
398
        f"""
399
        A package.json file describing a nodejs package. (https://nodejs.org/api/packages.html#introduction)
400

401
        Generates a `{NodePackageTarget.alias}` target for the package.
402

403
        Generates `{NodeThirdPartyPackageTarget.alias}` targets for each specified
404
        3rd party dependency (e.g. in the package.json#devDependencies field).
405
        """
406
    )
407

UNCOV
408
    copied_fields = COMMON_TARGET_FIELDS
×
UNCOV
409
    moved_fields = (NodePackageDependenciesField,)
×
410

411

UNCOV
412
class NodeBuildScriptEntryPointField(StringField):
×
UNCOV
413
    alias = "entry_point"
×
UNCOV
414
    required = True
×
UNCOV
415
    value: str
×
416

UNCOV
417
    help = help_text(
×
418
        """
419
        The name of the script from the package.json#scripts section to execute for the build.
420

421
        This script should produce the output files/directories specified in the build script configuration.
422
        """
423
    )
424

425

UNCOV
426
class NodeBuildScriptSourcesField(SourcesField):
×
UNCOV
427
    alias = "_sources"
×
UNCOV
428
    required = False
×
UNCOV
429
    default = None
×
430

UNCOV
431
    help = "Marker field for node_build_scripts used in export-codegen."
×
432

433

UNCOV
434
class NodeBuildScriptOutputFilesField(StringSequenceField):
×
UNCOV
435
    alias = "output_files"
×
UNCOV
436
    required = False
×
UNCOV
437
    default = ()
×
UNCOV
438
    help = help_text(
×
439
        """
440
        Specify the build script's output files to capture, relative to the package.json.
441

442
        For directories, use `output_directories`. At least one of `output_files` and
443
        `output_directories` must be specified.
444

445
        Relative paths (including `..`) may be used, as long as the path does not ascend further
446
        than the package.json parent directory.
447
        """
448
    )
449

450

UNCOV
451
class NodeBuildScriptOutputDirectoriesField(StringSequenceField):
×
UNCOV
452
    alias = "output_directories"
×
UNCOV
453
    required = False
×
UNCOV
454
    default = ()
×
UNCOV
455
    help = help_text(
×
456
        """
457
        Specify full directories (including recursive descendants) of output to capture from the
458
        build script, relative to the package.json.
459

460
        For individual files, use `output_files`. At least one of `output_files` and
461
        `output_directories` must be specified.
462

463
        Relative paths (including `..`) may be used, as long as the path does not ascend further
464
        than the package.json parent directory.
465
        """
466
    )
467

468

UNCOV
469
class NodeBuildScriptExtraEnvVarsField(StringSequenceField):
×
UNCOV
470
    alias = "extra_env_vars"
×
UNCOV
471
    required = False
×
UNCOV
472
    default = ()
×
UNCOV
473
    help = help_text(
×
474
        f"""
475
        Additional environment variables to include in environment when running a build script process.
476

477
        {EXTRA_ENV_VARS_USAGE_HELP}
478
        """
479
    )
480

481

UNCOV
482
class NodeBuildScriptExtraCaches(StringSequenceField):
×
UNCOV
483
    alias = "extra_caches"
×
UNCOV
484
    required = False
×
UNCOV
485
    default = ()
×
486
    help = help_text(
487
        f"""
488
        Specify directories that pants should treat as caches for the build script.
489

490
        These directories will not be available as sources, but are available to
491
        subsequent executions of the build script.
492

493
        Example usage:
494
        # BUILD
495
        {PackageJsonTarget.alias}(
496
            scripts={NodeBuildScript.alias}(
497
                entry_point="build",
498
                output_directories=["dist"],
499
                extra_caches=[".parcel-cache"],
500
            )
501
        )
502

503
        # package.json
504
        {{
505
            ...
506
            "scripts": {{
507
                "build": "parcel build --dist-dir=dist --cache-dir=.parcel-cache"
508
                ...
509
            }}
510
            ...
511
        }}
512
        """
513
    )
514

515

UNCOV
516
class NodeBuildScriptTarget(Target):
×
UNCOV
517
    core_fields = (
×
518
        *COMMON_TARGET_FIELDS,
519
        NodeBuildScriptEntryPointField,
520
        NodeBuildScriptOutputDirectoriesField,
521
        NodeBuildScriptOutputFilesField,
522
        NodeBuildScriptSourcesField,
523
        NodeBuildScriptExtraCaches,
524
        NodeBuildScriptExtraEnvVarsField,
525
        NodePackageDependenciesField,
526
        OutputPathField,
527
    )
528

UNCOV
529
    alias = "_node_build_script"
×
530

UNCOV
531
    help = help_text(
×
532
        """
533
        A package.json script that is invoked by the configured package manager
534
        to produce `resource` targets or a packaged artifact.
535
        """
536
    )
537

538

UNCOV
539
class NodeRunScriptEntryPointField(StringField):
×
UNCOV
540
    alias = "entry_point"
×
UNCOV
541
    required = True
×
UNCOV
542
    value: str
×
543

544

UNCOV
545
class NodeRunScriptExtraEnvVarsField(StringSequenceField):
×
UNCOV
546
    alias = "extra_env_vars"
×
UNCOV
547
    required = False
×
UNCOV
548
    default = ()
×
UNCOV
549
    help = help_text(
×
550
        f"""
551
        Additional environment variables to include in environment when running a script process.
552

553
        {EXTRA_ENV_VARS_USAGE_HELP}
554
        """
555
    )
556

557

UNCOV
558
class NodeRunScriptTarget(Target):
×
UNCOV
559
    core_fields = (
×
560
        *COMMON_TARGET_FIELDS,
561
        NodeRunScriptEntryPointField,
562
        NodeRunScriptExtraEnvVarsField,
563
        NodePackageDependenciesField,
564
    )
565

UNCOV
566
    alias = "_node_run_script"
×
567

UNCOV
568
    help = help_text(
×
569
        """
570
        A package.json script that can be invoked directly via the run goal.
571
        """
572
    )
573

574

UNCOV
575
@dataclass(frozen=True)
×
UNCOV
576
class PackageJsonImports:
×
577
    """https://nodejs.org/api/packages.html#subpath-imports."""
578

UNCOV
579
    imports: FrozenDict[str, tuple[str, ...]]
×
UNCOV
580
    root_dir: str
×
581

UNCOV
582
    @classmethod
×
UNCOV
583
    def from_package_json(cls, pkg_json: PackageJson) -> PackageJsonImports:
×
584
        return cls(
×
585
            imports=cls._import_from_package_json(pkg_json),
586
            root_dir=pkg_json.root_dir,
587
        )
588

UNCOV
589
    @staticmethod
×
UNCOV
590
    def _import_from_package_json(
×
591
        pkg_json: PackageJson,
592
    ) -> FrozenDict[str, tuple[str, ...]]:
593
        imports: Mapping[str, Any] | None = pkg_json.content.get("imports")
×
594

595
        def get_subpaths(value: str | Mapping[str, Any]) -> Iterable[str]:
×
596
            if isinstance(value, str):
×
597
                yield value
×
598
            elif isinstance(value, Mapping):
×
599
                for v in value.values():
×
600
                    yield from get_subpaths(v)
×
601

602
        if not imports:
×
603
            return FrozenDict()
×
604
        return FrozenDict(
×
605
            {key: tuple(sorted(get_subpaths(subpath))) for key, subpath in imports.items()}
606
        )
607

608

UNCOV
609
@dataclass(frozen=True)
×
UNCOV
610
class PackageJsonEntryPoints:
×
611
    """See https://nodejs.org/api/packages.html#package-entry-points and
612
    https://docs.npmjs.com/cli/v9/configuring-npm/package-json#browser."""
613

UNCOV
614
    exports: FrozenDict[str, str]
×
UNCOV
615
    bin: FrozenDict[str, str]
×
UNCOV
616
    root_dir: str
×
617

UNCOV
618
    @property
×
UNCOV
619
    def globs(self) -> Iterable[str]:
×
620
        for export in self.exports.values():
×
621
            yield export.replace("*", "**/*")
×
622
        yield from self.bin.values()
×
623

UNCOV
624
    def globs_from_root(self) -> Iterable[str]:
×
625
        for path in self.globs:
×
626
            yield os.path.normpath(os.path.join(self.root_dir, path))
×
627

UNCOV
628
    @classmethod
×
UNCOV
629
    def from_package_json(cls, pkg_json: PackageJson) -> PackageJsonEntryPoints:
×
630
        return cls(
×
631
            exports=cls._exports_form_package_json(pkg_json),
632
            bin=cls._binaries_from_package_json(pkg_json),
633
            root_dir=pkg_json.root_dir,
634
        )
635

UNCOV
636
    @staticmethod
×
UNCOV
637
    def _exports_form_package_json(pkg_json: PackageJson) -> FrozenDict[str, str]:
×
638
        content = pkg_json.content
×
639
        exports: str | Mapping[str, str] | None = content.get("exports")
×
640
        main: str | None = content.get("main")
×
641
        browser: str | None = content.get("browser")
×
642
        source: str | None = content.get("source")
×
643
        if exports:
×
644
            if isinstance(exports, str):
×
645
                return FrozenDict({".": exports})
×
646
            else:
647
                return FrozenDict(exports)
×
648
        elif browser:
×
649
            return FrozenDict({".": browser})
×
650
        elif main:
×
651
            return FrozenDict({".": main})
×
652
        elif source:
×
653
            return FrozenDict({".": source})
×
654
        return FrozenDict()
×
655

UNCOV
656
    @staticmethod
×
UNCOV
657
    def _binaries_from_package_json(pkg_json: PackageJson) -> FrozenDict[str, str]:
×
658
        binaries: str | Mapping[str, str] | None = pkg_json.content.get("bin")
×
659
        if binaries:
×
660
            if isinstance(binaries, str):
×
661
                return FrozenDict({pkg_json.name: binaries})
×
662
            else:
663
                return FrozenDict(binaries)
×
664
        return FrozenDict()
×
665

666

UNCOV
667
@dataclass(frozen=True)
×
UNCOV
668
class PackageJsonScripts:
×
UNCOV
669
    scripts: FrozenDict[str, str]
×
670

UNCOV
671
    @classmethod
×
UNCOV
672
    def from_package_json(cls, pkg_json: PackageJson) -> PackageJsonScripts:
×
673
        return cls(FrozenDict.deep_freeze(pkg_json.content.get("scripts", {})))
×
674

675

UNCOV
676
@dataclass(frozen=True)
×
UNCOV
677
class PackageJson:
×
UNCOV
678
    content: FrozenDict[str, Any]
×
UNCOV
679
    name: str
×
UNCOV
680
    version: str | None
×
UNCOV
681
    snapshot: Snapshot
×
UNCOV
682
    workspaces: tuple[str, ...] = ()
×
UNCOV
683
    module: Literal["commonjs", "module"] | None = None
×
UNCOV
684
    dependencies: FrozenDict[str, str] = field(default_factory=FrozenDict)
×
UNCOV
685
    package_manager: str | None = None
×
686

UNCOV
687
    def __post_init__(self) -> None:
×
UNCOV
688
        if self.module not in (None, "commonjs", "module"):
×
689
            raise ValueError(
×
690
                f'package.json "type" can only be one of "commonjs", "module", but was "{self.module}".'
691
            )
692

UNCOV
693
    @property
×
UNCOV
694
    def digest(self) -> Digest:
×
695
        return self.snapshot.digest
×
696

UNCOV
697
    @property
×
UNCOV
698
    def file(self) -> str:
×
699
        return self.snapshot.files[0]
×
700

UNCOV
701
    @property
×
UNCOV
702
    def root_dir(self) -> str:
×
703
        return os.path.dirname(self.file)
×
704

705

UNCOV
706
class FirstPartyNodePackageTargets(Targets):
×
UNCOV
707
    pass
×
708

709

UNCOV
710
class AllPackageJson(Collection[PackageJson]):
×
UNCOV
711
    pass
×
712

713

UNCOV
714
class PackageJsonForGlobs(Collection[PackageJson]):
×
UNCOV
715
    pass
×
716

717

UNCOV
718
@rule
×
UNCOV
719
async def all_first_party_node_package_targets(targets: AllTargets) -> FirstPartyNodePackageTargets:
×
720
    return FirstPartyNodePackageTargets(
×
721
        tgt for tgt in targets if tgt.has_fields((PackageJsonSourceField, NodePackageNameField))
722
    )
723

724

UNCOV
725
@dataclass(frozen=True)
×
UNCOV
726
class OwningNodePackageRequest:
×
UNCOV
727
    address: Address
×
728

729

UNCOV
730
@dataclass(frozen=True)
×
UNCOV
731
class OwningNodePackage:
×
UNCOV
732
    target: Target | None = None
×
UNCOV
733
    third_party: tuple[Target, ...] = ()
×
734

UNCOV
735
    @classmethod
×
UNCOV
736
    def no_owner(cls) -> OwningNodePackage:
×
UNCOV
737
        return cls()
×
738

UNCOV
739
    def ensure_owner(self) -> Target:
×
UNCOV
740
        if self != OwningNodePackage.no_owner():
×
UNCOV
741
            assert self.target
×
UNCOV
742
            return self.target
×
743
        raise ValueError("No owner could be determined.")
×
744

745

UNCOV
746
@rule
×
UNCOV
747
async def find_owning_package(request: OwningNodePackageRequest) -> OwningNodePackage:
×
748
    candidate_targets = await resolve_targets(
×
749
        **implicitly(
750
            RawSpecs(
751
                ancestor_globs=(AncestorGlobSpec(request.address.spec_path),),
752
                description_of_origin=f"the `{OwningNodePackage.__name__}` rule",
753
            )
754
        )
755
    )
756
    package_json_tgts = sorted(
×
757
        (
758
            tgt
759
            for tgt in candidate_targets
760
            if tgt.has_field(PackageJsonSourceField) and tgt.has_field(NodePackageNameField)
761
        ),
762
        key=lambda tgt: tgt.address.spec_path,
763
        reverse=True,
764
    )
765
    tgt = package_json_tgts[0] if package_json_tgts else None
×
766
    if tgt:
×
767
        deps = await resolve_targets(**implicitly(DependenciesRequest(tgt[Dependencies])))
×
768
        return OwningNodePackage(
×
769
            tgt, tuple(dep for dep in deps if dep.has_field(NodeThirdPartyPackageNameField))
770
        )
771
    return OwningNodePackage()
×
772

773

UNCOV
774
@rule
×
UNCOV
775
async def parse_package_json(content: FileContent) -> PackageJson:
×
776
    parsed_package_json = FrozenDict.deep_freeze(json.loads(content.content))
×
777
    package_name = parsed_package_json.get("name")
×
778
    if not package_name:
×
779
        raise ValueError("No package name found in package.json")
×
780

781
    return PackageJson(
×
782
        content=parsed_package_json,
783
        name=package_name,
784
        version=parsed_package_json.get("version"),
785
        snapshot=await digest_to_snapshot(**implicitly(PathGlobs([content.path]))),
786
        module=parsed_package_json.get("type"),
787
        workspaces=tuple(parsed_package_json.get("workspaces", ())),
788
        dependencies=FrozenDict.deep_freeze(
789
            {
790
                **parsed_package_json.get("dependencies", {}),
791
                **parsed_package_json.get("devDependencies", {}),
792
                **parsed_package_json.get("peerDependencies", {}),
793
            }
794
        ),
795
        package_manager=parsed_package_json.get("packageManager"),
796
    )
797

798

UNCOV
799
@rule
×
UNCOV
800
async def read_package_jsons(globs: PathGlobs) -> PackageJsonForGlobs:
×
801
    digest_contents = await get_digest_contents(**implicitly(globs))
×
802
    return PackageJsonForGlobs(
×
803
        await concurrently(parse_package_json(digest_content) for digest_content in digest_contents)
804
    )
805

806

UNCOV
807
@rule
×
UNCOV
808
async def all_package_json() -> AllPackageJson:
×
809
    # Avoids using `AllTargets` due to a circular rule dependency.
810
    # `generate_node_package_targets` requires knowledge of all
811
    # first party package names.
812
    description_of_origin = "The `AllPackageJson` rule"
×
813
    requests = await resolve_all_generator_target_requests(
×
814
        ResolveAllTargetGeneratorRequests(
815
            description_of_origin=description_of_origin, of_type=PackageJsonTarget
816
        )
817
    )
818
    globs = [
×
819
        glob
820
        for req in requests.requests
821
        for glob in req.generator[PackageJsonSourceField]
822
        .path_globs(UnmatchedBuildFileGlobs.error())
823
        .globs
824
    ]
825
    return AllPackageJson(
×
826
        await read_package_jsons(
827
            PathGlobs(
828
                globs, GlobMatchErrorBehavior.error, description_of_origin=description_of_origin
829
            )
830
        )
831
    )
832

833

UNCOV
834
@dataclass(frozen=True)
×
UNCOV
835
class PnpmWorkspaceGlobs:
×
UNCOV
836
    packages: tuple[str, ...]
×
UNCOV
837
    digest: Digest
×
838

839

UNCOV
840
class PnpmWorkspaces(FrozenDict[PackageJson, PnpmWorkspaceGlobs]):
×
UNCOV
841
    def for_root(self, root_dir: str) -> PnpmWorkspaceGlobs | None:
×
842
        for pkg, workspaces in self.items():
×
843
            if pkg.root_dir == root_dir:
×
844
                return workspaces
×
845
        return None
×
846

847

UNCOV
848
@rule
×
UNCOV
849
async def pnpm_workspace_files(pkgs: AllPackageJson) -> PnpmWorkspaces:
×
850
    digest_contents = await get_digest_contents(
×
851
        **implicitly(PathGlobs(os.path.join(pkg.root_dir, "pnpm-workspace.yaml") for pkg in pkgs))
852
    )
853

854
    async def parse_package_globs(content: FileContent) -> PnpmWorkspaceGlobs:
×
855
        parsed = yaml.safe_load(content.content) or {"packages": ("**",)}
×
856
        return PnpmWorkspaceGlobs(
×
857
            tuple(parsed.get("packages", ("**",)) or ("**",)),
858
            await create_digest(CreateDigest([content])),
859
        )
860

861
    globs_per_root = {
×
862
        os.path.dirname(digest_content.path): await parse_package_globs(digest_content)
863
        for digest_content in digest_contents
864
    }
865

866
    return PnpmWorkspaces(
×
867
        {pkg: globs_per_root[pkg.root_dir] for pkg in pkgs if pkg.root_dir in globs_per_root}
868
    )
869

870

UNCOV
871
class AllPackageJsonNames(DeduplicatedCollection[str]):
×
872
    """Used to not invalidate all generated node package targets when any package.json contents are
873
    changed."""
874

875

UNCOV
876
@rule
×
UNCOV
877
async def all_package_json_names(all_pkg_jsons: AllPackageJson) -> AllPackageJsonNames:
×
878
    return AllPackageJsonNames(pkg.name for pkg in all_pkg_jsons)
×
879

880

UNCOV
881
@rule
×
UNCOV
882
async def package_json_for_source(source_field: PackageJsonSourceField) -> PackageJson:
×
883
    [pkg_json] = await read_package_jsons(source_field.path_globs(UnmatchedBuildFileGlobs.error()))
×
884
    return pkg_json
×
885

886

UNCOV
887
@rule
×
UNCOV
888
async def script_entrypoints_for_source(
×
889
    source_field: PackageJsonSourceField,
890
) -> PackageJsonEntryPoints:
891
    return PackageJsonEntryPoints.from_package_json(await package_json_for_source(source_field))
×
892

893

UNCOV
894
@rule
×
UNCOV
895
async def subpath_imports_for_source(
×
896
    source_field: PackageJsonSourceField,
897
) -> PackageJsonImports:
898
    return PackageJsonImports.from_package_json(await package_json_for_source(source_field))
×
899

900

UNCOV
901
class GenerateNodePackageTargets(GenerateTargetsRequest):
×
UNCOV
902
    generate_from = PackageJsonTarget
×
903

904

UNCOV
905
def _script_missing_error(entry_point: str, scripts: Iterable[str], address: Address) -> ValueError:
×
906
    return ValueError(
×
907
        softwrap(
908
            f"""
909
            {entry_point} was not found in package.json#scripts section
910
            of the `{PackageJsonTarget.alias}` target with address {address}.
911

912
            Available scripts are: {", ".join(scripts)}.
913
            """
914
        )
915
    )
916

917

UNCOV
918
@rule
×
UNCOV
919
async def generate_node_package_targets(
×
920
    request: GenerateNodePackageTargets,
921
    union_membership: UnionMembership,
922
    first_party_names: AllPackageJsonNames,
923
) -> GeneratedTargets:
924
    file = request.generator[PackageJsonSourceField].file_path
×
925
    file_tgt = TargetGeneratorSourcesHelperTarget(
×
926
        {TargetGeneratorSourcesHelperSourcesField.alias: os.path.basename(file)},
927
        request.generator.address.create_generated(file),
928
        union_membership,
929
    )
930

931
    pkg_json = await package_json_for_source(request.generator[PackageJsonSourceField])
×
932

933
    third_party_tgts = [
×
934
        NodeThirdPartyPackageTarget(
935
            {
936
                **{
937
                    key: value
938
                    for key, value in request.template.items()
939
                    if key != PackageJsonSourceField.alias
940
                },
941
                NodeThirdPartyPackageNameField.alias: name,
942
                NodeThirdPartyPackageVersionField.alias: version,
943
                NodeThirdPartyPackageDependenciesField.alias: [file_tgt.address.spec],
944
            },
945
            request.generator.address.create_generated(name.replace("@", "__")),
946
            union_membership,
947
        )
948
        for name, version in pkg_json.dependencies.items()
949
        if name not in first_party_names
950
    ]
951

952
    package_target = NodePackageTarget(
×
953
        {
954
            **request.template,
955
            NodePackageNameField.alias: pkg_json.name,
956
            NodePackageVersionField.alias: pkg_json.version,
957
            NodePackageExtraEnvVarsField.alias: request.generator[
958
                NodePackageExtraEnvVarsField
959
            ].value,
960
            NodePackageDependenciesField.alias: [
961
                file_tgt.address.spec,
962
                *(tgt.address.spec for tgt in third_party_tgts),
963
                *request.template.get("dependencies", []),
964
            ],
965
            NodePackageTestScriptField.alias: request.generator[
966
                NodePackageScriptsField
967
            ].get_test_script(),
968
        },
969
        request.generator.address.create_generated(pkg_json.name.replace("@", "__")),
970
        union_membership,
971
    )
972
    scripts = PackageJsonScripts.from_package_json(pkg_json).scripts
×
973
    build_script_tgts = []
×
974
    for build_script in request.generator[NodePackageScriptsField].build_scripts():
×
975
        if build_script.entry_point in scripts:
×
976
            build_script_tgts.append(
×
977
                NodeBuildScriptTarget(
978
                    {
979
                        **request.template,
980
                        NodeBuildScriptEntryPointField.alias: build_script.entry_point,
981
                        NodeBuildScriptOutputDirectoriesField.alias: build_script.output_directories,
982
                        NodeBuildScriptOutputFilesField.alias: build_script.output_files,
983
                        NodeBuildScriptExtraEnvVarsField.alias: build_script.extra_env_vars,
984
                        NodeBuildScriptExtraCaches.alias: build_script.extra_caches,
985
                        NodePackageDependenciesField.alias: [
986
                            file_tgt.address.spec,
987
                            *(tgt.address.spec for tgt in third_party_tgts),
988
                            *request.template.get("dependencies", []),
989
                            package_target.address.spec,
990
                        ],
991
                        DescriptionField.alias: build_script.description,
992
                        Tags.alias: build_script.tags,
993
                    },
994
                    request.generator.address.create_generated(build_script.entry_point),
995
                    union_membership,
996
                )
997
            )
998
        else:
999
            raise _script_missing_error(
×
1000
                build_script.entry_point, scripts, request.generator.address
1001
            )
1002

1003
    run_script_tgts = []
×
1004
    for script in request.generator[NodePackageScriptsField].value or ():
×
1005
        if isinstance(script, NodeRunScript):
×
1006
            if script.entry_point in scripts:
×
1007
                run_script_tgts.append(
×
1008
                    NodeRunScriptTarget(
1009
                        {
1010
                            **request.template,
1011
                            NodeRunScriptEntryPointField.alias: script.entry_point,
1012
                            NodeRunScriptExtraEnvVarsField.alias: script.extra_env_vars,
1013
                            NodePackageDependenciesField.alias: [
1014
                                file_tgt.address.spec,
1015
                                *(tgt.address.spec for tgt in third_party_tgts),
1016
                                *request.template.get("dependencies", []),
1017
                                package_target.address.spec,
1018
                            ],
1019
                        },
1020
                        request.generator.address.create_generated(script.entry_point),
1021
                        union_membership,
1022
                    )
1023
                )
1024
            else:
1025
                raise _script_missing_error(script.entry_point, scripts, request.generator.address)
×
1026

1027
    coverage_script = package_target[NodePackageTestScriptField].value.coverage_entry_point
×
1028
    if coverage_script and coverage_script not in scripts:
×
1029
        raise _script_missing_error(coverage_script, scripts, request.generator.address)
×
1030

1031
    return GeneratedTargets(
×
1032
        request.generator,
1033
        [
1034
            package_target,
1035
            file_tgt,
1036
            *third_party_tgts,
1037
            *build_script_tgts,
1038
            *run_script_tgts,
1039
        ],
1040
    )
1041

1042

UNCOV
1043
def target_types() -> Iterable[type[Target]]:
×
UNCOV
1044
    return [
×
1045
        PackageJsonTarget,
1046
        NodePackageTarget,
1047
        NodeThirdPartyPackageTarget,
1048
        NPMDistributionTarget,
1049
        NodeBuildScriptTarget,
1050
    ]
1051

1052

UNCOV
1053
def rules() -> Iterable[Rule | UnionRule]:
×
UNCOV
1054
    return [
×
1055
        *graph.rules(),
1056
        *dependencies.rules(),
1057
        *stripped_source_files.rules(),
1058
        *fs.rules(),
1059
        *collect_rules(),
1060
        UnionRule(GenerateTargetsRequest, GenerateNodePackageTargets),
1061
    ]
1062

1063

UNCOV
1064
def build_file_aliases() -> BuildFileAliases:
×
UNCOV
1065
    return BuildFileAliases(
×
1066
        objects={
1067
            NodeBuildScript.alias: NodeBuildScript.create,
1068
            NodeTestScript.alias: NodeTestScript.create,
1069
            NodeRunScript.alias: NodeRunScript.create,
1070
        }
1071
    )
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