From 6458179c67630192ec8149e4223f7e590692791e Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Wed, 9 Nov 2022 11:16:40 -0800 Subject: [PATCH] fix: pass all srcs to ts_project transpilers The js_outs and map_outs attributes have been removed and all sources are passed including non .ts files. Transpilers should produce .js files for each inputted .ts file. Additional features such as generation of source maps (.js.map) may be implemented and should respect the the ts_project(source_map) attribute. Transpilers may optionally pre-declare output files similar to ts_project(). Fixes #219 --- docs/transpiler.md | 9 +++-- examples/transpiler/babel.bzl | 35 +++++++++++----- ts/defs.bzl | 31 +++++--------- ts/test/BUILD.bazel | 14 +++++-- ts/test/mock_transpiler.bzl | 76 +++++++++++++++++++++++++++-------- 5 files changed, 111 insertions(+), 54 deletions(-) diff --git a/docs/transpiler.md b/docs/transpiler.md index 333a57f2..bef88094 100644 --- a/docs/transpiler.md +++ b/docs/transpiler.md @@ -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 for a more complete example that also shows usage of SWC. diff --git a/examples/transpiler/babel.bzl b/examples/transpiler/babel.bzl index 9a5029aa..8f078acc 100644 --- a/examples/transpiler/babel.bzl +++ b/examples/transpiler/babel.bzl @@ -1,32 +1,40 @@ "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 = "../../../.." - # 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): + 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), @@ -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 producing the js files which ts_project() will reference + js_library( + name = name, + srcs = outs, + ) diff --git a/ts/defs.bzl b/ts/defs.bzl index ce14320f..fe03cb8d 100644 --- a/ts/defs.bzl +++ b/ts/defs.bzl @@ -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, @@ -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: @@ -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 ) @@ -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, diff --git a/ts/test/BUILD.bazel b/ts/test/BUILD.bazel index feb856d6..7edda778 100644 --- a/ts/test/BUILD.bazel +++ b/ts/test/BUILD.bazel @@ -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") @@ -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 @@ -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", -) \ No newline at end of file +) diff --git a/ts/test/mock_transpiler.bzl b/ts/test/mock_transpiler.bzl index 81305a3a..fc5f1894 100644 --- a/ts/test/mock_transpiler.bzl +++ b/ts/test/mock_transpiler.bzl @@ -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 + )