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

pantsbuild / pants / 26342152999

23 May 2026 07:59PM UTC coverage: 91.165% (-1.6%) from 92.792%
26342152999

push

github

web-flow
Run Linux ARM CI on Depot runners (#23363)

RunsOn is deprecating their v2 stack, and rather than migrate
to v3 we should use the resources kindly donated by Depot.

GitHub also now has Linux ARM runners, should we need them.

87305 of 95766 relevant lines covered (91.16%)

3.87 hits per line

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

88.07
/src/python/pants/backend/typescript/tsconfig.py
1
# Copyright 2024 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3
"""tsconfig.json is primarily used by the typescript compiler in order to resolve types during
4
compilation. The format is also used by IDE:s to provide intellisense. The format is used for
5
projects that use only javascript, and is then named jsconfig.json.
6

7
See https://code.visualstudio.com/docs/languages/jsconfig
8
"""
9

10
from __future__ import annotations
11✔
11

12
import json
11✔
13
import logging
11✔
14
import os
11✔
15
import re
11✔
16
from collections.abc import Iterable
11✔
17
from dataclasses import dataclass
11✔
18
from pathlib import PurePath
11✔
19

20
from pants.engine.collection import Collection
11✔
21
from pants.engine.fs import DigestContents, FileContent, PathGlobs
11✔
22
from pants.engine.internals.selectors import concurrently
11✔
23
from pants.engine.intrinsics import get_digest_contents, path_globs_to_digest
11✔
24
from pants.engine.rules import Rule, collect_rules, rule
11✔
25
from pants.util.frozendict import FrozenDict
11✔
26

27
logger = logging.getLogger(__name__)
11✔
28

29

30
@dataclass(frozen=True)
11✔
31
class TSConfig:
11✔
32
    """Parsed tsconfig.json fields with `extends` substitution applied."""
33

34
    path: str
11✔
35
    module_resolution: str | None = None
11✔
36
    paths: FrozenDict[str, tuple[str, ...]] | None = None
11✔
37
    base_url: str | None = None
11✔
38
    out_dir: str | None = None
11✔
39
    allow_js: bool | None = None
11✔
40
    check_js: bool | None = None
11✔
41

42
    @property
11✔
43
    def resolution_root_dir(self) -> str:
11✔
44
        directory = os.path.dirname(self.path)
2✔
45
        return os.path.join(directory, self.base_url) if self.base_url else directory
2✔
46

47
    def validate_outdir(self) -> None:
11✔
48
        if not self.out_dir:
1✔
49
            raise ValueError(
1✔
50
                f"TypeScript configuration at '{self.path}' is missing required 'outDir' setting. "
51
                f"TypeScript type-checking requires an explicit outDir in compilerOptions to work properly. "
52
                f'Add \'"outDir": "./dist"\' (or your preferred output directory) to the compilerOptions '
53
                f"in {self.path}."
54
            )
55

56
        if ".." in self.out_dir:
1✔
57
            raise ValueError(
×
58
                f"TypeScript configuration at '{self.path}' has outDir '{self.out_dir}' "
59
                f"that uses '..' path components. Each package should use its own output directory "
60
                f"within its package boundary (e.g., './dist', './build'). Cross-package output "
61
                f"directories can cause build conflicts where packages overwrite each other's artifacts."
62
            )
63

64
        if os.path.isabs(self.out_dir):
1✔
65
            raise ValueError(
×
66
                f"TypeScript configuration at '{self.path}' has absolute outDir '{self.out_dir}'. "
67
                f"Use a relative path within the package directory instead (e.g., './dist', './build')."
68
            )
69

70

71
class AllTSConfigs(Collection[TSConfig]):
11✔
72
    pass
11✔
73

74

75
@dataclass(frozen=True)
11✔
76
class ParseTSConfigRequest:
11✔
77
    content: FileContent
11✔
78
    others: DigestContents
11✔
79

80

81
def _get_parent_config_content(
11✔
82
    child_path: str,
83
    extends_path: str,
84
    others: DigestContents,
85
) -> FileContent | None:
86
    if child_path.endswith(".json"):
1✔
87
        relative = os.path.dirname(child_path)
1✔
88
    else:
89
        relative = child_path
×
90
    relative = os.path.normpath(os.path.join(relative, extends_path))
1✔
91
    for target_file in ("tsconfig.json", "jsconfig.json"):
1✔
92
        if not extends_path.endswith(".json"):
1✔
93
            relative = os.path.join(relative, target_file)
×
94
        parent = next((other for other in others if other.path == relative), None)
1✔
95
        if parent:
1✔
96
            break
1✔
97
    if not parent:
1✔
98
        logger.warning(
×
99
            f"pants could not locate {child_path}'s 'extends' at {relative}. Found: {[other.path for other in others]}."
100
        )
101
        return None
×
102
    return parent
1✔
103

104

105
def _clean_tsconfig_contents(content: str) -> str:
11✔
106
    """The tsconfig.json uses a format similar to JSON ("JSON with comments"), but there are some
107
    important differences:
108

109
    * tsconfig.json allows both single-line (`// comment`) and multi-line comments (`/* comment */`) to be added
110
    anywhere in the file.
111
    * Trailing commas in arrays and objects are permitted.
112

113
    TypeScript uses its own parser to read the file; in standard JSON, trailing commas or comments are not allowed.
114
    """
115
    # This pattern matches:
116
    # 1. Strings: "..." or '...'
117
    # 2. Single-line comments: //...
118
    # 3. Multi-line comments: /*...*/
119
    # 4. Everything else (including potential trailing commas)
120
    pattern = r'("(?:\\.|[^"\\])*")|(\'(?:\\.|[^\'\\])*\')|(//.*?$)|(/\*.*?\*/)|,(\s*[\]}])'
2✔
121

122
    def replace(match):
2✔
123
        if match.group(1) or match.group(2):  # It's a string
2✔
124
            return match.group(0)  # Return the string as is
2✔
125
        elif match.group(3) or match.group(4):  # It's a comment
×
126
            return ""  # Remove the comment
×
127
        elif match.group(5):  # It's a trailing comma
×
128
            return match.group(5)  # Remove the comma keeping the closing brace/bracket
×
129
        return match.group(0)
×
130

131
    cleaned_content = re.sub(pattern, replace, content, flags=re.DOTALL | re.MULTILINE)
2✔
132
    return cleaned_content
2✔
133

134

135
def _parse_config_from_content(content: FileContent) -> tuple[TSConfig, str | None]:
11✔
136
    cleaned_tsconfig_contents = _clean_tsconfig_contents(content.content.decode("utf-8"))
2✔
137
    parsed_ts_config_json = FrozenDict.deep_freeze(json.loads(cleaned_tsconfig_contents))
2✔
138

139
    compiler_options = parsed_ts_config_json.get("compilerOptions", FrozenDict())
2✔
140
    return TSConfig(
2✔
141
        content.path,
142
        module_resolution=compiler_options.get("moduleResolution"),
143
        paths=compiler_options.get("paths"),
144
        base_url=compiler_options.get("baseUrl"),
145
        out_dir=compiler_options.get("outDir"),
146
        allow_js=compiler_options.get("allowJs"),
147
        check_js=compiler_options.get("checkJs"),
148
    ), parsed_ts_config_json.get("extends")
149

150

151
@rule
11✔
152
async def parse_extended_ts_config(request: ParseTSConfigRequest) -> TSConfig:
11✔
153
    ts_config, extends = _parse_config_from_content(request.content)
2✔
154
    if not extends:
2✔
155
        return ts_config
2✔
156

157
    parent_content = _get_parent_config_content(ts_config.path, extends, request.others)
1✔
158
    if parent_content:
1✔
159
        extended_parent = await parse_extended_ts_config(
1✔
160
            ParseTSConfigRequest(parent_content, request.others)
161
        )
162
    else:
163
        extended_parent = None
×
164

165
    if not extended_parent:
1✔
166
        return ts_config
×
167
    return TSConfig(
1✔
168
        ts_config.path,
169
        module_resolution=ts_config.module_resolution or extended_parent.module_resolution,
170
        paths=ts_config.paths or extended_parent.paths,
171
        base_url=ts_config.base_url or extended_parent.base_url,
172
        # Do NOT inherit outDir - paths in extended configs are resolved relative to where they're defined,
173
        # not where they're used, making inherited outDir values incorrect for child projects
174
        out_dir=ts_config.out_dir,
175
        allow_js=ts_config.allow_js if ts_config.allow_js is not None else extended_parent.allow_js,
176
        check_js=ts_config.check_js if ts_config.check_js is not None else extended_parent.check_js,
177
    )
178

179

180
@dataclass(frozen=True)
11✔
181
class TSConfigsRequest:
11✔
182
    target_file: str
11✔
183

184

185
@rule
11✔
186
async def construct_effective_ts_configs() -> AllTSConfigs:
11✔
187
    all_files = await path_globs_to_digest(PathGlobs(["**/tsconfig*.json", "**/jsconfig*.json"]))
4✔
188
    digest_contents = await get_digest_contents(all_files)
4✔
189

190
    return AllTSConfigs(
4✔
191
        await concurrently(
192
            parse_extended_ts_config(ParseTSConfigRequest(digest_content, digest_contents))
193
            for digest_content in digest_contents
194
        )
195
    )
196

197

198
@dataclass(frozen=True)
11✔
199
class ClosestTSConfig:
11✔
200
    ts_config: TSConfig | None
11✔
201

202

203
@dataclass(frozen=True)
11✔
204
class ParentTSConfigRequest:
11✔
205
    file: str
11✔
206

207

208
@rule(desc="Finding parent tsconfig.json")
11✔
209
async def find_parent_ts_config(req: ParentTSConfigRequest) -> ClosestTSConfig:
11✔
210
    all_configs = await construct_effective_ts_configs()
4✔
211
    configs_by_longest_path = sorted(all_configs, key=lambda config: len(config.path), reverse=True)
4✔
212
    for config in configs_by_longest_path:
4✔
213
        if PurePath(req.file).is_relative_to(os.path.dirname(config.path)):
2✔
214
            return ClosestTSConfig(config)
2✔
215
    return ClosestTSConfig(None)
3✔
216

217

218
def rules() -> Iterable[Rule]:
11✔
219
    return collect_rules()
11✔
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