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

fix: support non-file transpiler srcs #236

Merged
merged 3 commits into from
Nov 13, 2022
Merged
Show file tree
Hide file tree
Changes from 2 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
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
35 changes: 26 additions & 9 deletions examples/transpiler/babel.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,38 @@
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 = "../../../.."

# In this example we compile each file individually.
outs = []

# In this example we compile each file individually on .ts src files.
# The src files must be .ts files known at the loading phase in order
# to setup the babel compilation for each .ts file.
#
# 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):
jbedard marked this conversation as resolved.
Show resolved Hide resolved
if not src.endswith(".ts"):
fail("babel example transpiler only supports source .ts files")

# Predict the output paths where babel will write
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 +43,16 @@ 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)

# The target whose default outputs are the js files which ts_project() will reference
native.filegroup(
jbedard marked this conversation as resolved.
Show resolved Hide resolved
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
43 changes: 40 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 = [
alexeagle marked this conversation as resolved.
Show resolved Hide resolved
"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 @@ -92,13 +100,42 @@ ts_project(
tsconfig = _TSCONFIG,
)

# ts_project srcs containing a filegroup()
write_file(
name = "src_filegroup_a",
out = "src_fg_a.ts",
content = ["export const a: string = \"1\";"],
)

write_file(
name = "src_filegroup_b",
out = "src_fg_b.ts",
content = ["export const b: string = \"2\";"],
)

filegroup(
name = "src_filegroup",
srcs = [
"src_fg_a.ts",
"src_fg_b.ts",
],
)

ts_project(
name = "transpile_filegroup",
srcs = [":src_filegroup"],
tags = ["manual"],
transpiler = mock,
tsconfig = _TSCONFIG,
)

transpiler_test_suite()

# Ensure that when determining output location, the `root_dir` attribute is only removed once.
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
jbedard marked this conversation as resolved.
Show resolved Hide resolved

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
)
22 changes: 22 additions & 0 deletions ts/test/transpiler_tests.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,29 @@ transpile_with_dts_test = unittest.make(_impl2, attrs = {
"expected_js": attr.string_list(default = ["index.js", "index.js.map"]),
})

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

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

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

return unittest.end(env)

transitive_filegroup_test = unittest.make(_impl3, attrs = {
"lib": attr.label(default = "transpile_filegroup"),
"expected_js": attr.string_list(default = ["src_fg_a.js", "src_fg_a.js.map", "src_fg_b.js", "src_fg_b.js.map"]),
"expected_declarations": attr.string_list(default = ["src_fg_a.d.ts", "src_fg_b.d.ts"]),
})

def transpiler_test_suite():
unittest.suite("t0", transitive_declarations_test)
unittest.suite("t1", transpile_with_failing_typecheck_test)
unittest.suite("t2", transpile_with_dts_test)
unittest.suite("t3", transitive_filegroup_test)