Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add JSX support built on the JS backend #21206

Merged
merged 12 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,4 @@ GTAGS
/.venv
.tool-versions
TAGS
node_modules
3 changes: 3 additions & 0 deletions docs/notes/2.23.x.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ Pants now applies dependency inference according to the most permissive "bundler
[jsconfig.json](https://code.visualstudio.com/docs/languages/jsconfig), when a jsconfig.json is
part of your javascript workspace.

Pants now ships with experimental JSX support, including Prettier formatting and JS testing as part of the
JS backend.

#### Shell

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).
Expand Down
12 changes: 12 additions & 0 deletions src/python/pants/backend/experimental/javascript/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@
JSTestsGeneratorTarget,
JSTestTarget,
)
from pants.backend.jsx.goals import tailor as jsx_tailor
from pants.backend.jsx.target_types import (
JSXSourcesGeneratorTarget,
JSXSourceTarget,
JSXTestsGeneratorTarget,
JSXTestTarget,
)
from pants.build_graph.build_file_aliases import BuildFileAliases
from pants.engine.rules import Rule
from pants.engine.target import Target
Expand All @@ -31,6 +38,7 @@ def rules() -> Iterable[Rule | UnionRule]:
*run_rules(),
*test.rules(),
*export.rules(),
*jsx_tailor.rules(),
)


Expand All @@ -40,6 +48,10 @@ def target_types() -> Iterable[type[Target]]:
JSSourcesGeneratorTarget,
JSTestTarget,
JSTestsGeneratorTarget,
JSXSourceTarget,
JSXSourcesGeneratorTarget,
JSXTestTarget,
JSXTestsGeneratorTarget,
*package_json.target_types(),
)

Expand Down
22 changes: 13 additions & 9 deletions src/python/pants/backend/javascript/dependency_inference/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@
from pants.backend.javascript.subsystems.nodejs_infer import NodeJSInfer
from pants.backend.javascript.target_types import (
JS_FILE_EXTENSIONS,
JSDependenciesField,
JSSourceField,
JSRuntimeDependenciesField,
JSRuntimeSourceField,
)
from pants.backend.jsx.target_types import JSX_FILE_EXTENSIONS
from pants.backend.typescript import tsconfig
from pants.backend.typescript.tsconfig import ParentTSConfigRequest, TSConfig, find_parent_ts_config
from pants.build_graph.address import Address
Expand Down Expand Up @@ -74,10 +75,10 @@ class InferNodePackageDependenciesRequest(InferDependenciesRequest):

@dataclass(frozen=True)
class JSSourceInferenceFieldSet(FieldSet):
required_fields = (JSSourceField, JSDependenciesField)
required_fields = (JSRuntimeSourceField, JSRuntimeDependenciesField)

source: JSSourceField
dependencies: JSDependenciesField
source: JSRuntimeSourceField
dependencies: JSRuntimeDependenciesField


class InferJSDependenciesRequest(InferDependenciesRequest):
Expand All @@ -96,7 +97,9 @@ async def infer_node_package_dependencies(
)
candidate_js_files = await Get(Owners, OwnersRequest(tuple(entry_points.globs_from_root())))
js_targets = await Get(Targets, Addresses(candidate_js_files))
return InferredDependencies(tgt.address for tgt in js_targets if tgt.has_field(JSSourceField))
return InferredDependencies(
tgt.address for tgt in js_targets if tgt.has_field(JSRuntimeSourceField)
)


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


def _add_extensions(file_imports: frozenset[str]) -> PathGlobs:
extensions = JS_FILE_EXTENSIONS + tuple(f"/index{ext}" for ext in JS_FILE_EXTENSIONS)
file_extensions = (*JS_FILE_EXTENSIONS, *JSX_FILE_EXTENSIONS)
extensions = file_extensions + tuple(f"/index{ext}" for ext in file_extensions)
return PathGlobs(
string
for file_import in file_imports
Expand Down Expand Up @@ -227,12 +231,12 @@ async def infer_js_source_dependencies(
request: InferJSDependenciesRequest,
nodejs_infer: NodeJSInfer,
) -> InferredDependencies:
source: JSSourceField = request.field_set.source
source: JSRuntimeSourceField = request.field_set.source
if not nodejs_infer.imports:
return InferredDependencies(())

sources = await Get(
HydratedSources, HydrateSourcesRequest(source, for_sources_types=[JSSourceField])
HydratedSources, HydrateSourcesRequest(source, for_sources_types=[JSRuntimeSourceField])
)
metadata = await _prepare_inference_metadata(request.field_set.address, source.file_path)

Expand Down
23 changes: 10 additions & 13 deletions src/python/pants/backend/javascript/goals/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,7 @@
OwningNodePackageRequest,
)
from pants.backend.javascript.subsystems.nodejstest import NodeJSTest
from pants.backend.javascript.target_types import (
JSSourceField,
JSTestBatchCompatibilityTagField,
JSTestExtraEnvVarsField,
JSTestSourceField,
JSTestTimeoutField,
)
from pants.backend.javascript.target_types import JSRuntimeSourceField, JSTestRuntimeSourceField
from pants.base.glob_match_error_behavior import GlobMatchErrorBehavior
from pants.build_graph.address import Address
from pants.core.goals.test import (
Expand All @@ -37,10 +31,13 @@
CoverageReports,
FilesystemCoverageReport,
TestExtraEnv,
TestExtraEnvVarsField,
TestFieldSet,
TestRequest,
TestResult,
TestsBatchCompatibilityTagField,
TestSubsystem,
TestTimeoutField,
)
from pants.core.target_types import AssetSourceField
from pants.core.util_rules import source_files
Expand Down Expand Up @@ -88,13 +85,13 @@ class JSCoverageDataCollection(CoverageDataCollection[JSCoverageData]):

@dataclass(frozen=True)
class JSTestFieldSet(TestFieldSet):
required_fields = (JSTestSourceField,)
required_fields = (JSTestRuntimeSourceField,)

batch_compatibility_tag: JSTestBatchCompatibilityTagField
source: JSTestSourceField
batch_compatibility_tag: TestsBatchCompatibilityTagField
source: JSTestRuntimeSourceField
dependencies: Dependencies
timeout: JSTestTimeoutField
extra_env_vars: JSTestExtraEnvVarsField
timeout: TestTimeoutField
extra_env_vars: TestExtraEnvVarsField


class JSTestRequest(TestRequest):
Expand Down Expand Up @@ -174,7 +171,7 @@ async def run_javascript_tests(
SourceFilesRequest(
(tgt.get(SourcesField) for tgt in transitive_tgts.closure),
enable_codegen=True,
for_sources_types=[JSSourceField, AssetSourceField],
for_sources_types=[JSRuntimeSourceField, AssetSourceField],
),
)
merged_digest = await Get(Digest, MergeDigests([sources.snapshot.digest, installation.digest]))
Expand Down
4 changes: 2 additions & 2 deletions src/python/pants/backend/javascript/install_node_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
)
from pants.backend.javascript.package_manager import PackageManager
from pants.backend.javascript.subsystems import nodejs
from pants.backend.javascript.target_types import JSSourceField
from pants.backend.javascript.target_types import JSRuntimeSourceField
from pants.build_graph.address import Address
from pants.core.target_types import FileSourceField, ResourceSourceField
from pants.core.util_rules.source_files import SourceFiles, SourceFilesRequest
Expand Down Expand Up @@ -71,7 +71,7 @@ async def _get_relevant_source_files(
SourceFilesRequest(
sources,
for_sources_types=(PackageJsonSourceField, FileSourceField)
+ ((ResourceSourceField, JSSourceField) if with_js else ()),
+ ((ResourceSourceField, JSRuntimeSourceField) if with_js else ()),
enable_codegen=True,
),
)
Expand Down
6 changes: 3 additions & 3 deletions src/python/pants/backend/javascript/lint/prettier/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from pants.backend.javascript.lint.prettier.subsystem import Prettier
from pants.backend.javascript.subsystems import nodejs_tool
from pants.backend.javascript.subsystems.nodejs_tool import NodeJSToolRequest
from pants.backend.javascript.target_types import JSSourceField
from pants.backend.javascript.target_types import JSRuntimeSourceField
from pants.core.goals.fmt import FmtResult, FmtTargetsRequest
from pants.core.util_rules.config_files import ConfigFiles, ConfigFilesRequest
from pants.core.util_rules.partitions import PartitionerType
Expand All @@ -28,9 +28,9 @@

@dataclass(frozen=True)
class PrettierFmtFieldSet(FieldSet):
required_fields = (JSSourceField,)
required_fields = (JSRuntimeSourceField,)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the target discussion, for prettier specifically, it supports things that are very non-JS-shaped, e.g. HTML, GraphQL, Markdown, JSON, Yaml.

Two questions:

  1. Specifically for prettier: do we currently have any thoughts on how to make Pants able to use Prettier to (optionally!) format non-JS files too?
  2. Generalising/another clarifying example: are there other tools in the JS ecosystem that might behave similarly and work with more file types? Do we need to think about how to handle them?

Copy link
Contributor Author

@tobni tobni Jul 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. No. I have no use case myself but I suppose a contributor could open up the prettier plugin to more languages later by adding a union-layer? Alternatively prettier seems like the kind of tool one might want to run "target-less" and just glob?
  2. The typescript compiler can check other files than .ts. It can be run on .js, .jsx and .tsx as well Test runners have the same capability, so the test goal also applies. Those are the usecases I'm primarily intrested in, and I think the "JS-ish source" model works well there.

Copy link
Contributor Author

@tobni tobni Jul 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think it is fun that you implied json is non-js shaped 😏 I get what you're saying though, you do not run a json file.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively prettier seems like the kind of tool one might want to run "target-less" and just glob?

Hm, maybe, yeah.

The typescript compiler can check other files than .ts. It can be run on .js, .jsx and .tsx as well Test runners have the same capability, so the test goal also applies. Those are the usecases I'm primarily intrested in, and I think the "JS-ish source" model works well there.

👍

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I wrote the Prettier plugin a while back, and we did have the discussion about how it could be used target-less, but that (d)evolved into a much larger discussion about the needs for targets and target-less and so on and so forth.

There are a few tools where I think we just want to defer to whatever the tool can handle - and I don't know how we do that in the most pants-esque way


sources: JSSourceField
sources: JSRuntimeSourceField


class PrettierFmtRequest(FmtTargetsRequest):
Expand Down
18 changes: 15 additions & 3 deletions src/python/pants/backend/javascript/target_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,23 @@
JS_TEST_FILE_EXTENSIONS = tuple(f"*.test{ext}" for ext in JS_FILE_EXTENSIONS)


class JSDependenciesField(Dependencies):
class JSRuntimeDependenciesField(Dependencies):
"""Dependencies of a target that is javascript at runtime."""


class JSDependenciesField(JSRuntimeDependenciesField):
pass


class JSSourceField(SingleSourceField):
class JSRuntimeSourceField(SingleSourceField):
"""A source that is javascript at runtime."""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feeding into the PR description. Thinking about this framing: AIUI, there's some tools that can run TypeScript natively (e.g. Bun; Node (experimentally) as of nodejs/node#53725), so a .ts file can be run without being/becoming JS.

Does that play into the broader consideration, or maybe it's not a big-picture issue (e.g. maybe just this doc string is (very slightly) inaccurate)?

Copy link
Contributor Author

@tobni tobni Jul 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose it is a point of semantics, but bun and node do not run typescript. They strip (node) or compile-down the (bun) types and run the resulting javascript.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think my key point is that there's tools for which the file run is the .ts one, without a bundling/packaging/compilation step to turn it into real JS first, so it's becomes harder to say that it "is Javascript" at runtime.

Brainstorming: I wonder if we could instead flip this around to be tool-focused using a union somehow (I'm not fully familiar with whether that's possible, though). E.g. register various source types as members of "can be passed to tsc ...", "can be passed to node ...", "prettier ...", "biome ...", etc.

Copy link
Contributor Author

@tobni tobni Jul 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For specific tools were we can have a field set and union to target the tool, yes.

Not sure how that works out for node_test_script. The TestFieldSet is already a union that requires a SourcesField unique to each union member.

In other words, I think at some point we are required to "boil down" to a unique source field type to "end up" with a JSTestRequest. If it was generic on e.g SourceField I think the union is not specialized enough? But idk 🤷‍♂️

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In other words, I think at some point we are required to "boil down" to a unique source field type to "end up" with a JSTestRequest. If it was generic on e.g SourceField I think the union is not specialized enough? But idk 🤷‍♂️

IDK either 🤷‍♂️



class JSTestRuntimeSourceField(SingleSourceField):
"""A source that is runnable by javascript test-runners at runtime."""


class JSSourceField(JSRuntimeSourceField):
expected_file_extensions = JS_FILE_EXTENSIONS


Expand Down Expand Up @@ -86,7 +98,7 @@ class JSTestDependenciesField(JSDependenciesField):
pass


class JSTestSourceField(JSSourceField):
class JSTestSourceField(JSSourceField, JSTestRuntimeSourceField):
expected_file_extensions = JS_FILE_EXTENSIONS


Expand Down
11 changes: 11 additions & 0 deletions src/python/pants/backend/jsx/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Copyright 2024 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

# NOTE: Sources restricted from the default for python_sources due to conflict with
# - //:all-__init__.py-files
# - //src/python/pants/backend/jsx/__init__.py:../../../../../all-__init__.py-files
python_sources(
sources=[
"target_types.py",
],
)
Empty file.
6 changes: 6 additions & 0 deletions src/python/pants/backend/jsx/goals/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Copyright 2024 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

python_sources()

python_tests(name="tests")
Empty file.
62 changes: 62 additions & 0 deletions src/python/pants/backend/jsx/goals/tailor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Copyright 2024 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

from __future__ import annotations

import dataclasses
from dataclasses import dataclass
from typing import Iterable

from pants.backend.jsx.target_types import (
JSX_FILE_EXTENSIONS,
JSXSourcesGeneratorTarget,
JSXTestsGeneratorSourcesField,
JSXTestsGeneratorTarget,
)
from pants.core.goals.tailor import (
AllOwnedSources,
PutativeTarget,
PutativeTargets,
PutativeTargetsRequest,
)
from pants.core.util_rules.ownership import get_unowned_files_for_globs
from pants.core.util_rules.source_files import classify_files_for_sources_and_tests
from pants.engine.rules import Rule, collect_rules, rule
from pants.engine.unions import UnionRule
from pants.util.dirutil import group_by_dir
from pants.util.logging import LogLevel


@dataclass(frozen=True)
class PutativeJSXTargetsRequest(PutativeTargetsRequest):
pass


@rule(level=LogLevel.DEBUG, desc="Determine candidate JSX targets to create")
async def find_putative_jsx_targets(
req: PutativeJSXTargetsRequest, all_owned_sources: AllOwnedSources
) -> PutativeTargets:
unowned_jsx_files = await get_unowned_files_for_globs(
req, all_owned_sources, (f"*{ext}" for ext in JSX_FILE_EXTENSIONS)
)
classified_unowned_js_files = classify_files_for_sources_and_tests(
paths=unowned_jsx_files,
test_file_glob=JSXTestsGeneratorSourcesField.default,
sources_generator=JSXSourcesGeneratorTarget,
tests_generator=JSXTestsGeneratorTarget,
)

return PutativeTargets(
PutativeTarget.for_target_type(
tgt_type, path=dirname, name=name, triggering_sources=sorted(filenames)
)
for tgt_type, paths, name in (dataclasses.astuple(f) for f in classified_unowned_js_files)
for dirname, filenames in group_by_dir(paths).items()
)


def rules() -> Iterable[Rule | UnionRule]:
return (
*collect_rules(),
UnionRule(PutativeTargetsRequest, PutativeJSXTargetsRequest),
)
Loading
Loading