Skip to content
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
39 changes: 26 additions & 13 deletions docs/TypeScript.md
Original file line number Diff line number Diff line change
Expand Up @@ -744,19 +744,9 @@ Defaults to `False`

<h4 id="ts_project-transpiler">transpiler</h4>

What tool to run that produces the JavaScript outputs.
By default, this is the string `tsc`. With that value, `ts_project` expects `.js` outputs
to be written in the same action that does the type-checking to produce `.d.ts` outputs.
This is the simplest configuration, however `tsc` is slower than alternatives.
It also means developers must wait for the type-checking in the developer loop.

In theory, Persistent Workers (via the `supports_workers` attribute) remedies the
slow compilation time, however it adds additional complexity because the worker process
can only see one set of dependencies, and so it cannot be shared between different
`ts_project` rules. That attribute is documented as experimental, and may never graduate
to a better support contract.
A custom transpiler tool to run that produces the JavaScript outputs instead of `tsc`.

Instead of the string `tsc`, this attribute also accepts a rule or macro with this signature:
This attribute accepts a rule or macro with this signature:
`name, srcs, js_outs, map_outs, **kwargs`
where the `**kwargs` attribute propagates the tags, visibility, and testonly attributes from `ts_project`.

Expand All @@ -766,7 +756,30 @@ to bind those arguments at the "make site", then pass that partial to this attri
will be called with the remaining arguments.
See the packages/typescript/test/ts_project/swc directory for an example.

Defaults to `"tsc"`
When a custom transpiler is used, then the `ts_project` macro expands to these targets:

- `[name]` - the default target is a `js_library` which can be included in the `deps` of downstream rules.
Note that it will successfully build *even if there are typecheck failures* because the `tsc` binary
is not needed to produce the default outputs.
This is considered a feature, as it allows you to have a faster development mode where type-checking
is not on the critical path.
- `[name]_typecheck` - this target will fail to build if the type-checking fails, useful for CI.
- `[name]_typings` - internal target which runs the binary from the `tsc` attribute
- Any additional target(s) the custom transpiler rule/macro produces.
Some rules produce one target per TypeScript input file.

By default, `ts_project` expects `.js` outputs to be written in the same action
that does the type-checking to produce `.d.ts` outputs.
This is the simplest configuration, however `tsc` is slower than alternatives.
It also means developers must wait for the type-checking in the developer loop.

In theory, Persistent Workers (via the `supports_workers` attribute) remedies the
slow compilation time, however it adds additional complexity because the worker process
can only see one set of dependencies, and so it cannot be shared between different
`ts_project` rules. That attribute is documented as experimental, and may never graduate
to a better support contract.

Defaults to `None`

<h4 id="ts_project-ts_build_info_file">ts_build_info_file</h4>

Expand Down
106 changes: 77 additions & 29 deletions packages/typescript/internal/ts_project.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ load("@build_bazel_rules_nodejs//:providers.bzl", "DeclarationInfo", "ExternalNp
load("@build_bazel_rules_nodejs//internal/linker:link_node_modules.bzl", "module_mappings_aspect")
load("@build_bazel_rules_nodejs//internal/node:node.bzl", "nodejs_binary")
load("@build_bazel_rules_nodejs//third_party/github.com/bazelbuild/bazel-skylib:lib/partial.bzl", "partial")
load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
load(":ts_config.bzl", "TsConfigInfo", "write_tsconfig")

_ValidOptionsInfo = provider()
Expand Down Expand Up @@ -45,6 +46,7 @@ _ATTRS = {
"srcs": attr.label_list(allow_files = True, mandatory = True),
"supports_workers": attr.bool(default = False),
"tsc": attr.label(default = Label(_DEFAULT_TSC), executable = True, cfg = "host"),
"transpile": attr.bool(doc = "whether tsc should be used to produce .js outputs"),
"tsconfig": attr.label(mandatory = True, allow_single_file = [".json"]),
}

Expand Down Expand Up @@ -180,10 +182,6 @@ def _ts_project_impl(ctx):
else:
json_outs = []

# If there are no js_outs, that implies we are producing declarations only.
# We must avoid tsc writing any JS files in this case, as it was only run for typings.
arguments.add("--emitDeclarationOnly")

outputs = json_outs + ctx.outputs.js_outs + ctx.outputs.map_outs + ctx.outputs.typings_outs + ctx.outputs.typing_maps_outs
if ctx.outputs.buildinfo_out:
arguments.add_all([
Expand All @@ -193,7 +191,17 @@ def _ts_project_impl(ctx):
outputs.append(ctx.outputs.buildinfo_out)
runtime_outputs = json_outs + ctx.outputs.js_outs + ctx.outputs.map_outs
typings_outputs = ctx.outputs.typings_outs + ctx.outputs.typing_maps_outs + [s for s in ctx.files.srcs if s.path.endswith(".d.ts")]
default_outputs_depset = depset(runtime_outputs) if len(runtime_outputs) else depset(typings_outputs)

if ctx.attr.transpile:
default_outputs_depset = depset(runtime_outputs) if len(runtime_outputs) else depset(typings_outputs)
else:
# We must avoid tsc writing any JS files in this case, as tsc was only run for typings, and some other
# action will try to write the JS files. We must avoid collisions where two actions write the same file.
arguments.add("--emitDeclarationOnly")

# We don't produce any DefaultInfo outputs in this case, because we avoid running the tsc action
# unless the DeclarationInfo is requested.
default_outputs_depset = depset([])

if len(outputs) > 0:
run_node(
Expand Down Expand Up @@ -245,7 +253,7 @@ def _ts_project_impl(ctx):
]

# Only provide DeclarationInfo if there are some typings.
# Improves error messaging if a ts_project needs declaration = True
# Improves error messaging if a ts_project is missing declaration = True
typings_in_deps = [d for d in ctx.attr.deps if DeclarationInfo in d]
if len(typings_outputs) or len(typings_in_deps):
providers.append(declaration_info(depset(typings_outputs), typings_in_deps))
Expand Down Expand Up @@ -372,7 +380,7 @@ def ts_project_macro(
composite = False,
incremental = False,
emit_declaration_only = False,
transpiler = "tsc",
transpiler = None,
ts_build_info_file = None,
tsc = None,
typescript_package = _DEFAULT_TYPESCRIPT_PACKAGE,
Expand Down Expand Up @@ -546,19 +554,9 @@ def ts_project_macro(

args: List of strings of additional command-line arguments to pass to tsc.

transpiler: What tool to run that produces the JavaScript outputs.
By default, this is the string `tsc`. With that value, `ts_project` expects `.js` outputs
to be written in the same action that does the type-checking to produce `.d.ts` outputs.
This is the simplest configuration, however `tsc` is slower than alternatives.
It also means developers must wait for the type-checking in the developer loop.
transpiler: A custom transpiler tool to run that produces the JavaScript outputs instead of `tsc`.

In theory, Persistent Workers (via the `supports_workers` attribute) remedies the
slow compilation time, however it adds additional complexity because the worker process
can only see one set of dependencies, and so it cannot be shared between different
`ts_project` rules. That attribute is documented as experimental, and may never graduate
to a better support contract.

Instead of the string `tsc`, this attribute also accepts a rule or macro with this signature:
This attribute accepts a rule or macro with this signature:
`name, srcs, js_outs, map_outs, **kwargs`
where the `**kwargs` attribute propagates the tags, visibility, and testonly attributes from `ts_project`.

Expand All @@ -568,6 +566,29 @@ def ts_project_macro(
will be called with the remaining arguments.
See the packages/typescript/test/ts_project/swc directory for an example.

When a custom transpiler is used, then the `ts_project` macro expands to these targets:

- `[name]` - the default target is a `js_library` which can be included in the `deps` of downstream rules.
Note that it will successfully build *even if there are typecheck failures* because the `tsc` binary
is not needed to produce the default outputs.
This is considered a feature, as it allows you to have a faster development mode where type-checking
is not on the critical path.
- `[name]_typecheck` - this target will fail to build if the type-checking fails, useful for CI.
- `[name]_typings` - internal target which runs the binary from the `tsc` attribute
- Any additional target(s) the custom transpiler rule/macro produces.
Some rules produce one target per TypeScript input file.

By default, `ts_project` expects `.js` outputs to be written in the same action
that does the type-checking to produce `.d.ts` outputs.
This is the simplest configuration, however `tsc` is slower than alternatives.
It also means developers must wait for the type-checking in the developer loop.

In theory, Persistent Workers (via the `supports_workers` attribute) remedies the
slow compilation time, however it adds additional complexity because the worker process
can only see one set of dependencies, and so it cannot be shared between different
`ts_project` rules. That attribute is documented as experimental, and may never graduate
to a better support contract.

tsc: Label of the TypeScript compiler binary to run.

For example, `tsc = "@my_deps//typescript/bin:tsc"`
Expand Down Expand Up @@ -666,7 +687,7 @@ def ts_project_macro(
include.append("**/*.json")
exclude.extend(["**/package.json", "**/package-lock.json", "**/tsconfig*.json"])
srcs = native.glob(include, exclude)
extra_deps = []
tsc_deps = deps

if type(extends) == type([]):
fail("As of rules_nodejs 3.0, extends should have a single value, not a list.\n" +
Expand Down Expand Up @@ -727,7 +748,7 @@ def ts_project_macro(
tsconfig = tsconfig,
extends = extends,
)
extra_deps.append("_validate_%s_options" % name)
tsc_deps = tsc_deps + ["_validate_%s_options" % name]

if supports_workers:
tsc_worker = "%s_worker" % name
Expand Down Expand Up @@ -786,35 +807,61 @@ def ts_project_macro(

tsc_js_outs = []
tsc_map_outs = []
if transpiler == "tsc":
if not transpiler:
tsc_js_outs = js_outs
tsc_map_outs = map_outs
tsc_target_name = name
else:
transpiler_kwargs = {
# To stitch together a tree of ts_project where transpiler is a separate rule,
# we have to produce a few targets
tsc_target_name = "%s_typings" % name
transpile_target_name = "%s_transpile" % name
typecheck_target_name = "%s_typecheck" % name

common_kwargs = {
"tags": kwargs.get("tags", []),
"visibility": kwargs.get("visibility", None),
"testonly": kwargs.get("testonly", None),
}
if type(transpiler) == "function" or type(transpiler) == "rule":
transpiler(
name = name + "_transpile",
name = transpile_target_name,
srcs = srcs,
js_outs = js_outs,
map_outs = map_outs,
**transpiler_kwargs
**common_kwargs
)
elif partial.is_instance(transpiler):
partial.call(
transpiler,
name = name + "_transpile",
name = transpile_target_name,
srcs = srcs,
js_outs = js_outs,
map_outs = map_outs,
**transpiler_kwargs
**common_kwargs
)
else:
fail("transpiler attribute should be a rule/macro, a skylib partial, or the string 'tsc'. Got " + type(transpiler))

# Users should build this target to get a failed build when typechecking fails
native.filegroup(
name = typecheck_target_name,
srcs = [tsc_target_name],
# This causes the DeclarationInfo to be produced, which in turn triggers the tsc action to typecheck
output_group = "types",
**common_kwargs
)

# Default target produced by the macro gives the js and map outs, with the transitive dependencies.
js_library(
name = name,
srcs = js_outs + map_outs,
# Include the tsc target so that this js_library can be a valid dep for downstream ts_project
# or other DeclarationInfo-aware rules.
deps = deps + [tsc_target_name],
**common_kwargs
)

if not len(tsc_js_outs) and not len(typings_outs):
fail("""ts_project target "//{}:{}" is configured to produce no outputs.

Expand All @@ -823,10 +870,10 @@ Check the srcs attribute to see that some .ts files are present (or .js files wi
""".format(native.package_name(), name))

ts_project(
name = name,
name = tsc_target_name,
srcs = srcs,
args = args,
deps = deps + extra_deps,
deps = tsc_deps,
tsconfig = tsconfig,
extends = extends,
declaration_dir = declaration_dir,
Expand All @@ -840,5 +887,6 @@ Check the srcs attribute to see that some .ts files are present (or .js files wi
tsc = tsc,
link_workspace_root = link_workspace_root,
supports_workers = supports_workers,
transpile = not transpiler,
**kwargs
)
40 changes: 28 additions & 12 deletions packages/typescript/test/ts_project/swc/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
load("@bazel_skylib//rules:write_file.bzl", "write_file")
load("//packages/typescript:index.bzl", "ts_project")
load(":swc.bzl", "swc", "swc_macro")
load(":tests.bzl", "test_suite")

_TSCONFIG = {
"compilerOptions": {
"declaration": True,
"sourceMap": True,
},
}

write_file(
name = "gen_ts",
Expand All @@ -11,16 +19,17 @@ write_file(
],
)

write_file(
name = "gen_typeerror",
out = "typeerror.ts",
content = ["export const a: string = 1"],
)

ts_project(
name = "transpile_with_swc",
srcs = ["big.ts"],
transpiler = swc_macro,
tsconfig = {
"compilerOptions": {
"declaration": True,
"sourceMap": True,
},
},
tsconfig = _TSCONFIG,
)

ts_project(
Expand All @@ -31,10 +40,17 @@ ts_project(
args = ["--env-name=test"],
swcrc = "//:.swcrc",
),
tsconfig = {
"compilerOptions": {
"declaration": True,
"sourceMap": True,
},
},
tsconfig = _TSCONFIG,
)

ts_project(
name = "transpile_with_typeerror",
srcs = ["typeerror.ts"],
# The transpile_with_typeerror.check target will have a build failure

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

How does the build failure get ignored on CI with this one?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

the manual tag gets propagated to the build targets, so we don't run them. The test explicitly asks for the js_library which passes because typecheck action doesn't run

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤦 I was starring right at it and didn't see the "manual" tag

# But the default transpile_with_typeerror target should still produce JS outs
tags = ["manual"],
transpiler = swc_macro,
tsconfig = _TSCONFIG,
)

test_suite()
38 changes: 38 additions & 0 deletions packages/typescript/test/ts_project/swc/tests.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"Unit tests for starlark API of ts_project with custom transpiler"

load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest")
load("//:providers.bzl", "DeclarationInfo", "JSModuleInfo")

def _impl0(ctx):
env = unittest.begin(ctx)

decls = []
for decl in ctx.attr.lib[DeclarationInfo].declarations.to_list():
decls.append(decl.basename)
asserts.equals(env, ctx.attr.expected_declarations, sorted(decls))

return unittest.end(env)

transitive_declarations_test = unittest.make(_impl0, attrs = {
"lib": attr.label(default = "transpile_with_swc"),
"expected_declarations": attr.string_list(default = ["big.d.ts"]),
})

def _impl1(ctx):
env = unittest.begin(ctx)

js_files = []
for js in ctx.attr.lib[JSModuleInfo].sources.to_list():
js_files.append(js.basename)
asserts.equals(env, ctx.attr.expected_js, sorted(js_files))

return unittest.end(env)

transpile_with_failing_typecheck_test = unittest.make(_impl1, attrs = {
"lib": attr.label(default = "transpile_with_typeerror"),
"expected_js": attr.string_list(default = ["typeerror.js", "typeerror.js.map"]),
})

def test_suite():
unittest.suite("t0", transitive_declarations_test)
unittest.suite("t1", transpile_with_failing_typecheck_test)