Skip to content

Commit 6c41074

Browse files
tobniriisi
andauthored
Add JSX support built on the JS backend (#21206)
This is a small change for the JS backend, as the tree-sitter parser already supports JSX. This is a fairly big decision w.r.t how the js backend will evolve with typescript and other javascript transpile formats. Since many tools that support js also supports other kinds of files, the `JSRuntime...` will be used to refer to the files that can be treated as a source code file for some node-ish runtime who either processes the AST or executes actual programmes. Clarifiying examples: 1. A linter that can process any js-esque file should accept JSRuntimeSourceField 2. All test sources should use JSRuntimeDependencies 3. Files that can cross import (post bundling) different transpile formats should use JSRuntimeDependencies and JSRuntimeSourceField. Think typescript, JSX. 4. A checker that can only process typed vue-templates should not accept JSRuntimeSourceField. This PR together with #21176 essentially gives pants "react" support. --------- Co-authored-by: riisi <[email protected]>
1 parent 0baf008 commit 6c41074

File tree

26 files changed

+4856
-31
lines changed

26 files changed

+4856
-31
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,4 @@ GTAGS
6868
/.venv
6969
.tool-versions
7070
TAGS
71+
node_modules

docs/notes/2.23.x.md

+3
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,9 @@ Pants now applies dependency inference according to the most permissive "bundler
195195
[jsconfig.json](https://code.visualstudio.com/docs/languages/jsconfig), when a jsconfig.json is
196196
part of your javascript workspace.
197197

198+
Pants now ships with experimental JSX support, including Prettier formatting and JS testing as part of the
199+
JS backend.
200+
198201
#### Shell
199202

200203
The `tailor` goal now has independent options for tailoring `shell_sources` and `shunit2_tests` targets. The option was split from `tailor` into [`tailor_sources`](https://www.pantsbuild.org/2.23/reference/subsystems/shell-setup#tailor_sources) and [`tailor_shunit2_tests`](https://www.pantsbuild.org/2.23/reference/subsystems/shell-setup#tailor_shunit2_tests).

src/python/pants/backend/experimental/javascript/register.py

+12
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@
1616
JSTestsGeneratorTarget,
1717
JSTestTarget,
1818
)
19+
from pants.backend.jsx.goals import tailor as jsx_tailor
20+
from pants.backend.jsx.target_types import (
21+
JSXSourcesGeneratorTarget,
22+
JSXSourceTarget,
23+
JSXTestsGeneratorTarget,
24+
JSXTestTarget,
25+
)
1926
from pants.build_graph.build_file_aliases import BuildFileAliases
2027
from pants.engine.rules import Rule
2128
from pants.engine.target import Target
@@ -31,6 +38,7 @@ def rules() -> Iterable[Rule | UnionRule]:
3138
*run_rules(),
3239
*test.rules(),
3340
*export.rules(),
41+
*jsx_tailor.rules(),
3442
)
3543

3644

@@ -40,6 +48,10 @@ def target_types() -> Iterable[type[Target]]:
4048
JSSourcesGeneratorTarget,
4149
JSTestTarget,
4250
JSTestsGeneratorTarget,
51+
JSXSourceTarget,
52+
JSXSourcesGeneratorTarget,
53+
JSXTestTarget,
54+
JSXTestsGeneratorTarget,
4355
*package_json.target_types(),
4456
)
4557

src/python/pants/backend/javascript/dependency_inference/rules.py

+13-9
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,10 @@
2525
from pants.backend.javascript.subsystems.nodejs_infer import NodeJSInfer
2626
from pants.backend.javascript.target_types import (
2727
JS_FILE_EXTENSIONS,
28-
JSDependenciesField,
29-
JSSourceField,
28+
JSRuntimeDependenciesField,
29+
JSRuntimeSourceField,
3030
)
31+
from pants.backend.jsx.target_types import JSX_FILE_EXTENSIONS
3132
from pants.backend.typescript import tsconfig
3233
from pants.backend.typescript.tsconfig import ParentTSConfigRequest, TSConfig, find_parent_ts_config
3334
from pants.build_graph.address import Address
@@ -74,10 +75,10 @@ class InferNodePackageDependenciesRequest(InferDependenciesRequest):
7475

7576
@dataclass(frozen=True)
7677
class JSSourceInferenceFieldSet(FieldSet):
77-
required_fields = (JSSourceField, JSDependenciesField)
78+
required_fields = (JSRuntimeSourceField, JSRuntimeDependenciesField)
7879

79-
source: JSSourceField
80-
dependencies: JSDependenciesField
80+
source: JSRuntimeSourceField
81+
dependencies: JSRuntimeDependenciesField
8182

8283

8384
class InferJSDependenciesRequest(InferDependenciesRequest):
@@ -96,7 +97,9 @@ async def infer_node_package_dependencies(
9697
)
9798
candidate_js_files = await Get(Owners, OwnersRequest(tuple(entry_points.globs_from_root())))
9899
js_targets = await Get(Targets, Addresses(candidate_js_files))
99-
return InferredDependencies(tgt.address for tgt in js_targets if tgt.has_field(JSSourceField))
100+
return InferredDependencies(
101+
tgt.address for tgt in js_targets if tgt.has_field(JSRuntimeSourceField)
102+
)
100103

101104

102105
class NodePackageCandidateMap(FrozenDict[str, Address]):
@@ -155,7 +158,8 @@ async def _prepare_inference_metadata(address: Address, file_path: str) -> Infer
155158

156159

157160
def _add_extensions(file_imports: frozenset[str]) -> PathGlobs:
158-
extensions = JS_FILE_EXTENSIONS + tuple(f"/index{ext}" for ext in JS_FILE_EXTENSIONS)
161+
file_extensions = (*JS_FILE_EXTENSIONS, *JSX_FILE_EXTENSIONS)
162+
extensions = file_extensions + tuple(f"/index{ext}" for ext in file_extensions)
159163
return PathGlobs(
160164
string
161165
for file_import in file_imports
@@ -227,12 +231,12 @@ async def infer_js_source_dependencies(
227231
request: InferJSDependenciesRequest,
228232
nodejs_infer: NodeJSInfer,
229233
) -> InferredDependencies:
230-
source: JSSourceField = request.field_set.source
234+
source: JSRuntimeSourceField = request.field_set.source
231235
if not nodejs_infer.imports:
232236
return InferredDependencies(())
233237

234238
sources = await Get(
235-
HydratedSources, HydrateSourcesRequest(source, for_sources_types=[JSSourceField])
239+
HydratedSources, HydrateSourcesRequest(source, for_sources_types=[JSRuntimeSourceField])
236240
)
237241
metadata = await _prepare_inference_metadata(request.field_set.address, source.file_path)
238242

src/python/pants/backend/javascript/goals/test.py

+10-13
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,7 @@
2222
OwningNodePackageRequest,
2323
)
2424
from pants.backend.javascript.subsystems.nodejstest import NodeJSTest
25-
from pants.backend.javascript.target_types import (
26-
JSSourceField,
27-
JSTestBatchCompatibilityTagField,
28-
JSTestExtraEnvVarsField,
29-
JSTestSourceField,
30-
JSTestTimeoutField,
31-
)
25+
from pants.backend.javascript.target_types import JSRuntimeSourceField, JSTestRuntimeSourceField
3226
from pants.base.glob_match_error_behavior import GlobMatchErrorBehavior
3327
from pants.build_graph.address import Address
3428
from pants.core.goals.test import (
@@ -37,10 +31,13 @@
3731
CoverageReports,
3832
FilesystemCoverageReport,
3933
TestExtraEnv,
34+
TestExtraEnvVarsField,
4035
TestFieldSet,
4136
TestRequest,
4237
TestResult,
38+
TestsBatchCompatibilityTagField,
4339
TestSubsystem,
40+
TestTimeoutField,
4441
)
4542
from pants.core.target_types import AssetSourceField
4643
from pants.core.util_rules import source_files
@@ -88,13 +85,13 @@ class JSCoverageDataCollection(CoverageDataCollection[JSCoverageData]):
8885

8986
@dataclass(frozen=True)
9087
class JSTestFieldSet(TestFieldSet):
91-
required_fields = (JSTestSourceField,)
88+
required_fields = (JSTestRuntimeSourceField,)
9289

93-
batch_compatibility_tag: JSTestBatchCompatibilityTagField
94-
source: JSTestSourceField
90+
batch_compatibility_tag: TestsBatchCompatibilityTagField
91+
source: JSTestRuntimeSourceField
9592
dependencies: Dependencies
96-
timeout: JSTestTimeoutField
97-
extra_env_vars: JSTestExtraEnvVarsField
93+
timeout: TestTimeoutField
94+
extra_env_vars: TestExtraEnvVarsField
9895

9996

10097
class JSTestRequest(TestRequest):
@@ -174,7 +171,7 @@ async def run_javascript_tests(
174171
SourceFilesRequest(
175172
(tgt.get(SourcesField) for tgt in transitive_tgts.closure),
176173
enable_codegen=True,
177-
for_sources_types=[JSSourceField, AssetSourceField],
174+
for_sources_types=[JSRuntimeSourceField, AssetSourceField],
178175
),
179176
)
180177
merged_digest = await Get(Digest, MergeDigests([sources.snapshot.digest, installation.digest]))

src/python/pants/backend/javascript/install_node_package.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
)
2121
from pants.backend.javascript.package_manager import PackageManager
2222
from pants.backend.javascript.subsystems import nodejs
23-
from pants.backend.javascript.target_types import JSSourceField
23+
from pants.backend.javascript.target_types import JSRuntimeSourceField
2424
from pants.build_graph.address import Address
2525
from pants.core.target_types import FileSourceField, ResourceSourceField
2626
from pants.core.util_rules.source_files import SourceFiles, SourceFilesRequest
@@ -71,7 +71,7 @@ async def _get_relevant_source_files(
7171
SourceFilesRequest(
7272
sources,
7373
for_sources_types=(PackageJsonSourceField, FileSourceField)
74-
+ ((ResourceSourceField, JSSourceField) if with_js else ()),
74+
+ ((ResourceSourceField, JSRuntimeSourceField) if with_js else ()),
7575
enable_codegen=True,
7676
),
7777
)

src/python/pants/backend/javascript/lint/prettier/rules.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from pants.backend.javascript.lint.prettier.subsystem import Prettier
1212
from pants.backend.javascript.subsystems import nodejs_tool
1313
from pants.backend.javascript.subsystems.nodejs_tool import NodeJSToolRequest
14-
from pants.backend.javascript.target_types import JSSourceField
14+
from pants.backend.javascript.target_types import JSRuntimeSourceField
1515
from pants.core.goals.fmt import FmtResult, FmtTargetsRequest
1616
from pants.core.util_rules.config_files import ConfigFiles, ConfigFilesRequest
1717
from pants.core.util_rules.partitions import PartitionerType
@@ -28,9 +28,9 @@
2828

2929
@dataclass(frozen=True)
3030
class PrettierFmtFieldSet(FieldSet):
31-
required_fields = (JSSourceField,)
31+
required_fields = (JSRuntimeSourceField,)
3232

33-
sources: JSSourceField
33+
sources: JSRuntimeSourceField
3434

3535

3636
class PrettierFmtRequest(FmtTargetsRequest):

src/python/pants/backend/javascript/target_types.py

+15-3
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,23 @@
2525
JS_TEST_FILE_EXTENSIONS = tuple(f"*.test{ext}" for ext in JS_FILE_EXTENSIONS)
2626

2727

28-
class JSDependenciesField(Dependencies):
28+
class JSRuntimeDependenciesField(Dependencies):
29+
"""Dependencies of a target that is javascript at runtime."""
30+
31+
32+
class JSDependenciesField(JSRuntimeDependenciesField):
2933
pass
3034

3135

32-
class JSSourceField(SingleSourceField):
36+
class JSRuntimeSourceField(SingleSourceField):
37+
"""A source that is javascript at runtime."""
38+
39+
40+
class JSTestRuntimeSourceField(SingleSourceField):
41+
"""A source that is runnable by javascript test-runners at runtime."""
42+
43+
44+
class JSSourceField(JSRuntimeSourceField):
3345
expected_file_extensions = JS_FILE_EXTENSIONS
3446

3547

@@ -86,7 +98,7 @@ class JSTestDependenciesField(JSDependenciesField):
8698
pass
8799

88100

89-
class JSTestSourceField(JSSourceField):
101+
class JSTestSourceField(JSSourceField, JSTestRuntimeSourceField):
90102
expected_file_extensions = JS_FILE_EXTENSIONS
91103

92104

src/python/pants/backend/jsx/BUILD

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Copyright 2024 Pants project contributors (see CONTRIBUTORS.md).
2+
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3+
4+
# NOTE: Sources restricted from the default for python_sources due to conflict with
5+
# - //:all-__init__.py-files
6+
# - //src/python/pants/backend/jsx/__init__.py:../../../../../all-__init__.py-files
7+
python_sources(
8+
sources=[
9+
"target_types.py",
10+
],
11+
)

src/python/pants/backend/jsx/__init__.py

Whitespace-only changes.
+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Copyright 2024 Pants project contributors (see CONTRIBUTORS.md).
2+
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3+
4+
python_sources()
5+
6+
python_tests(name="tests")

src/python/pants/backend/jsx/goals/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Copyright 2024 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 dataclasses
7+
from dataclasses import dataclass
8+
from typing import Iterable
9+
10+
from pants.backend.jsx.target_types import (
11+
JSX_FILE_EXTENSIONS,
12+
JSXSourcesGeneratorTarget,
13+
JSXTestsGeneratorSourcesField,
14+
JSXTestsGeneratorTarget,
15+
)
16+
from pants.core.goals.tailor import (
17+
AllOwnedSources,
18+
PutativeTarget,
19+
PutativeTargets,
20+
PutativeTargetsRequest,
21+
)
22+
from pants.core.util_rules.ownership import get_unowned_files_for_globs
23+
from pants.core.util_rules.source_files import classify_files_for_sources_and_tests
24+
from pants.engine.rules import Rule, collect_rules, rule
25+
from pants.engine.unions import UnionRule
26+
from pants.util.dirutil import group_by_dir
27+
from pants.util.logging import LogLevel
28+
29+
30+
@dataclass(frozen=True)
31+
class PutativeJSXTargetsRequest(PutativeTargetsRequest):
32+
pass
33+
34+
35+
@rule(level=LogLevel.DEBUG, desc="Determine candidate JSX targets to create")
36+
async def find_putative_jsx_targets(
37+
req: PutativeJSXTargetsRequest, all_owned_sources: AllOwnedSources
38+
) -> PutativeTargets:
39+
unowned_jsx_files = await get_unowned_files_for_globs(
40+
req, all_owned_sources, (f"*{ext}" for ext in JSX_FILE_EXTENSIONS)
41+
)
42+
classified_unowned_js_files = classify_files_for_sources_and_tests(
43+
paths=unowned_jsx_files,
44+
test_file_glob=JSXTestsGeneratorSourcesField.default,
45+
sources_generator=JSXSourcesGeneratorTarget,
46+
tests_generator=JSXTestsGeneratorTarget,
47+
)
48+
49+
return PutativeTargets(
50+
PutativeTarget.for_target_type(
51+
tgt_type, path=dirname, name=name, triggering_sources=sorted(filenames)
52+
)
53+
for tgt_type, paths, name in (dataclasses.astuple(f) for f in classified_unowned_js_files)
54+
for dirname, filenames in group_by_dir(paths).items()
55+
)
56+
57+
58+
def rules() -> Iterable[Rule | UnionRule]:
59+
return (
60+
*collect_rules(),
61+
UnionRule(PutativeTargetsRequest, PutativeJSXTargetsRequest),
62+
)

0 commit comments

Comments
 (0)