Skip to content

Commit

Permalink
fix: pass all srcs to ts_project transpilers
Browse files Browse the repository at this point in the history
Fixes #219
  • Loading branch information
jbedard committed Nov 11, 2022
1 parent 20697ec commit d489fd7
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 53 deletions.
9 changes: 6 additions & 3 deletions docs/transpiler.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,16 @@ Read more: https://blog.aspect.dev/typescript-speedup
The `transpiler` attribute of `ts_project` lets you select which tool produces the JavaScript outputs. The default value of `None` means that `tsc` should do transpiling along with type-checking, as this is the simplest configuration without additional dependencies. However as noted above, it's also the slowest.

The `transpiler` attribute accepts a rule or macro with this signature:
`name, srcs, js_outs, map_outs, **kwargs`
`name, srcs, **kwargs`
where the `**kwargs` attribute propagates the tags, visibility, and testonly attributes from `ts_project`.

If you need to pass additional attributes to the transpiler rule, you can use a
If you need to pass additional attributes to the transpiler rule such as `out_dir`, you can use a
[partial](https://github.com/bazelbuild/bazel-skylib/blob/main/lib/partial.bzl)
to bind those arguments at the "make site", then pass that partial to this attribute where it will be called with the remaining arguments.

The transpiler rule or macro is responsible for predicting and declaring outputs.
If you want to pre-declare the outputs (so that they can be addressed by a Bazel label, like `//path/to:index.js`)
then you should use a macro which calculates the predicted outputs and supplies them to a `ctx.attr.outputs` attribute
on the rule.
See the examples/transpiler directory for a simple example using Babel, or
<https://github.com/aspect-build/bazel-examples/tree/main/ts_project_transpiler>
for a more complete example that also shows usage of SWC.
Expand Down
29 changes: 21 additions & 8 deletions examples/transpiler/babel.bzl
Original file line number Diff line number Diff line change
@@ -1,32 +1,37 @@
"Adapter from the Babel CLI to the ts_project#transpiler interface"

load("@aspect_rules_js//js:defs.bzl", "js_library")
load("@npm//examples:@babel/cli/package_json.bzl", "bin")

# buildifier: disable=function-docstring
def babel(name, srcs, js_outs, map_outs, **kwargs):
def babel(name, srcs, **kwargs):
# rules_js runs under the output tree in bazel-out/[arch]/bin
# and we additionally chdir to the examples/ folder beneath that.
execroot = "../../../.."

outs = []

# In this example we compile each file individually.
# You might instead use a single babel_cli call to compile
# a directory of sources into an output directory,
# but you'll need to:
# - make sure the input directory only contains files listed in srcs
# - make sure the js_outs are actually created in the expected path
for idx, src in enumerate(srcs):
if not src.endswith(".ts"):
fail("babel example transpiler only supports source .ts files")

js_out = src.replace(".ts", ".js")
map_out = src.replace(".ts", ".js.map")

# see https://babeljs.io/docs/en/babel-cli
args = [
"{}/$(location {})".format(execroot, src),
"--presets=@babel/preset-typescript",
"--out-file",
"{}/$(location {})".format(execroot, js_outs[idx]),
"{}/$(location {})".format(execroot, js_out),
"--source-maps",
]
outs = [js_outs[idx]]

if len(map_outs) > 0:
args.append("--source-maps")
outs.append(map_outs[idx])

bin.babel(
name = "{}_{}".format(name, idx),
Expand All @@ -35,7 +40,15 @@ def babel(name, srcs, js_outs, map_outs, **kwargs):
"//examples:node_modules/@babel/preset-typescript",
],
chdir = "examples",
outs = outs,
outs = [js_out, map_out],
args = args,
**kwargs
)

outs.append(js_out)
outs.append(map_out)

js_library(
name = name,
srcs = outs,
)
31 changes: 9 additions & 22 deletions ts/defs.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -298,16 +298,14 @@ def ts_project(
typings_out_dir = declaration_dir if declaration_dir else out_dir
tsbuildinfo_path = ts_build_info_file if ts_build_info_file else name + ".tsbuildinfo"

js_outs = _lib.calculate_js_outs(srcs, out_dir, root_dir, allow_js, preserve_jsx, emit_declaration_only)
map_outs = _lib.calculate_map_outs(srcs, out_dir, root_dir, source_map, preserve_jsx, emit_declaration_only)
typings_outs = _lib.calculate_typings_outs(srcs, typings_out_dir, root_dir, declaration, composite, allow_js)
typing_maps_outs = _lib.calculate_typing_maps_outs(srcs, typings_out_dir, root_dir, declaration_map, allow_js)
tsc_typings_outs = _lib.calculate_typings_outs(srcs, typings_out_dir, root_dir, declaration, composite, allow_js)
tsc_typing_maps_outs = _lib.calculate_typing_maps_outs(srcs, typings_out_dir, root_dir, declaration_map, allow_js)

tsc_js_outs = []
tsc_map_outs = []
if not transpiler:
tsc_js_outs = js_outs
tsc_map_outs = map_outs
tsc_js_outs = _lib.calculate_js_outs(srcs, out_dir, root_dir, allow_js, preserve_jsx, emit_declaration_only)
tsc_map_outs = _lib.calculate_map_outs(srcs, out_dir, root_dir, source_map, preserve_jsx, emit_declaration_only)
tsc_target_name = name
else:
# To stitch together a tree of ts_project where transpiler is a separate rule,
Expand All @@ -317,28 +315,17 @@ def ts_project(
typecheck_target_name = "%s_typecheck" % name
test_target_name = "%s_typecheck_test" % name

transpile_srcs = [s for s in srcs if _lib.is_ts_src(s, allow_js)]
if (len(transpile_srcs) != len(js_outs)):
fail("ERROR: illegal state: transpile_srcs has length {} but js_outs has length {}".format(
len(transpile_srcs),
len(js_outs),
))

if type(transpiler) == "function" or type(transpiler) == "rule":
transpiler(
name = transpile_target_name,
srcs = transpile_srcs,
js_outs = js_outs,
map_outs = map_outs,
srcs = srcs,
**common_kwargs
)
elif partial.is_instance(transpiler):
partial.call(
transpiler,
name = transpile_target_name,
srcs = transpile_srcs,
js_outs = js_outs,
map_outs = map_outs,
srcs = srcs,
**common_kwargs
)
else:
Expand All @@ -365,7 +352,7 @@ def ts_project(
name = name,
# Include the tsc target in srcs to pick-up both the direct & transitive declaration outputs so
# that this js_library can be a valid dep for downstream ts_project or other rules_js derivative rules.
srcs = js_outs + map_outs + [tsc_target_name],
srcs = [transpile_target_name, tsc_target_name],
deps = deps,
**common_kwargs
)
Expand All @@ -390,8 +377,8 @@ def ts_project(
root_dir = root_dir,
js_outs = tsc_js_outs,
map_outs = tsc_map_outs,
typings_outs = typings_outs,
typing_maps_outs = typing_maps_outs,
typings_outs = tsc_typings_outs,
typing_maps_outs = tsc_typing_maps_outs,
buildinfo_out = tsbuildinfo_path if composite or incremental else None,
emit_declaration_only = emit_declaration_only,
tsc = tsc,
Expand Down
14 changes: 11 additions & 3 deletions ts/test/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
load("@bazel_skylib//rules:build_test.bzl", "build_test")
load("@bazel_skylib//rules:write_file.bzl", "write_file")

load(":mock_transpiler.bzl", "mock")
load(":transpiler_tests.bzl", "transpiler_test_suite")
load("//ts:defs.bzl", "ts_project")
Expand Down Expand Up @@ -52,6 +51,15 @@ ts_project(
tsconfig = _TSCONFIG,
)

# Ensure the output files are predeclared
build_test(
name = "out_refs_test",
targets = [
"big.js",
"big.d.ts",
],
)

# This target proves that transpilation doesn't require typechecking:
#
# $ bazel build examples/swc:transpile_with_typeerror
Expand Down Expand Up @@ -98,7 +106,7 @@ transpiler_test_suite()
ts_project(
name = "rootdir_works_with_repeated_directory",
srcs = ["root/deep/root/deep_src.ts"],
root_dir = "root",
transpiler = mock,
tsconfig = _TSCONFIG,
root_dir = "root",
)
)
76 changes: 59 additions & 17 deletions ts/test/mock_transpiler.bzl
Original file line number Diff line number Diff line change
@@ -1,26 +1,68 @@
"Fixture to demonstrate a custom transpiler for ts_project"

load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
load("@bazel_skylib//rules:write_file.bzl", "write_file")

_DUMMY_SOURCEMAP = """{"version":3,"sources":["%s"],"mappings":"AAAO,KAAK,CAAC","file":"in.js","sourcesContent":["fake"]}"""

def mock(name, srcs, js_outs, map_outs, **kwargs):
"""Mock transpiler macro.
def _mock_impl(ctx):
src_files = [src for src in ctx.files.srcs if src.short_path.endswith(".ts") and not src.short_path.endswith(".d.ts")]
out_files = []

for src in src_files:
out_path = src.short_path[len(ctx.attr.pkg_dir) + 1:] if src.short_path.startswith(ctx.attr.pkg_dir) else src.short_path

js_file = ctx.actions.declare_file(out_path.replace(".ts", ".js"))
map_file = ctx.actions.declare_file(out_path.replace(".ts", ".js.map"))

In real usage you would wrap a rule like
https://github.com/aspect-build/rules_swc/blob/main/docs/swc.md
"""
out_files.append(js_file)
out_files.append(map_file)

for i, s in enumerate(srcs):
copy_file(
name = "_{}_{}_js".format(name, s),
src = s,
out = js_outs[i],
ctx.actions.run_shell(
inputs = [src],
outputs = [js_file],
command = "cp $@",
arguments = [src.path, js_file.path.replace(".ts", ".js")],
)

write_file(
name = "_{}_{}_map".format(name, s),
out = map_outs[i],
content = [_DUMMY_SOURCEMAP % s],
ctx.actions.write(
output = map_file,
content = _DUMMY_SOURCEMAP % src.short_path,
)

return DefaultInfo(files = depset(out_files))

mock_impl = rule(
attrs = {
"srcs": attr.label_list(
doc = "TypeScript source files",
allow_files = True,
mandatory = True,
),

# Used only to predeclare outputs
"outs": attr.output_list(),

# TODO: why is this needed?
"pkg_dir": attr.string(),
},
implementation = _mock_impl,
)

def mock(name, srcs, source_map = False, **kwargs):
# Calculate pre-declared outputs so they can be referenced as targets.
# This is an optional transpiler feature aligning with the default tsc transpiler.
js_outs = [
src.replace(".ts", ".js")
for src in srcs
if (src.endswith(".ts") and not src.endswith(".d.ts"))
]
map_outs = [js.replace(".js", ".js.map") for js in js_outs] if source_map else []

# Run the rule producing those pre-declared outputs as well as any other outputs
# which can not be determined ahead of time such as within directories, goruped
# within a filegroup() etc.
mock_impl(
name = name,
srcs = srcs,
outs = js_outs + map_outs,
pkg_dir = native.package_name(),
**kwargs
)

0 comments on commit d489fd7

Please sign in to comment.