diff --git a/docs/swc.md b/docs/swc.md index 4fefde17..fe1f6702 100644 --- a/docs/swc.md +++ b/docs/swc.md @@ -18,8 +18,8 @@ swc( ## swc_compile
-swc_compile(name, srcs, data, args, js_outs, map_outs, out_dir, output_dir, plugins, root_dir,
-            source_maps, source_root, swcrc)
+swc_compile(name, srcs, data, args, dts_outs, emit_isolated_dts, js_outs, map_outs, out_dir,
+            output_dir, plugins, root_dir, source_maps, source_root, swcrc)
 
Underlying rule for the `swc` macro. @@ -39,6 +39,8 @@ for example to set your own output labels for `js_outs`. | srcs | source files, typically .ts files in the source tree | List of labels | required | | | data | Runtime dependencies to include in binaries/tests that depend on this target.

Follows the same semantics as `js_library` `data` attribute. See https://docs.aspect.build/rulesets/aspect_rules_js/docs/js_library#data for more info. | List of labels | optional | `[]` | | args | Additional arguments to pass to swcx cli (NOT swc!).

NB: this is not the same as the CLI arguments for @swc/cli npm package. For performance, rules_swc does not call a Node.js program wrapping the swc rust binding. Instead, we directly spawn the (somewhat experimental) native Rust binary shipped inside the @swc/core npm package, which the swc project calls "swcx" Tracking issue for feature parity: https://github.com/swc-project/swc/issues/4017 | List of strings | optional | `[]` | +| dts_outs | list of expected TypeScript declaration files.

Can be empty, meaning no dts files should be produced. If non-empty, there should be one for each entry in srcs. | List of labels | optional | `[]` | +| emit_isolated_dts | Emit .d.ts files instead of .js for TypeScript sources

EXPERIMENTAL: this API is undocumented, experimental and may change without notice | Boolean | optional | `False` | | js_outs | list of expected JavaScript output files.

There should be one for each entry in srcs. | List of labels | optional | `[]` | | map_outs | list of expected source map output files.

Can be empty, meaning no source maps should be produced. If non-empty, there should be one for each entry in srcs. | List of labels | optional | `[]` | | out_dir | With output_dir=False, output files will have this directory prefix.

With output_dir=True, this is the name of the output directory. | String | optional | `""` | diff --git a/examples/emit_types/.bazelrc b/examples/emit_types/.bazelrc new file mode 100644 index 00000000..413c9950 --- /dev/null +++ b/examples/emit_types/.bazelrc @@ -0,0 +1,15 @@ +# Import Aspect bazelrc presets +try-import %workspace%/../../.aspect/bazelrc/bazel7.bazelrc +import %workspace%/../../.aspect/bazelrc/convenience.bazelrc +import %workspace%/../../.aspect/bazelrc/correctness.bazelrc +import %workspace%/../../.aspect/bazelrc/debug.bazelrc +import %workspace%/../../.aspect/bazelrc/javascript.bazelrc +import %workspace%/../../.aspect/bazelrc/performance.bazelrc + +### YOUR PROJECT SPECIFIC OPTIONS GO HERE ### + +# Load any settings & overrides specific to the current user from `.aspect/bazelrc/user.bazelrc`. +# This file should appear in `.gitignore` so that settings are not shared with team members. This +# should be last statement in this config so the user configuration is able to overwrite flags from +# this file. See https://bazel.build/configure/best-practices#bazelrc-file. +try-import %workspace%/../../.aspect/bazelrc/user.bazelrc diff --git a/examples/emit_types/.bazelversion b/examples/emit_types/.bazelversion new file mode 120000 index 00000000..96cf9496 --- /dev/null +++ b/examples/emit_types/.bazelversion @@ -0,0 +1 @@ +../../.bazelversion \ No newline at end of file diff --git a/examples/emit_types/BUILD.bazel b/examples/emit_types/BUILD.bazel new file mode 100644 index 00000000..f36e099a --- /dev/null +++ b/examples/emit_types/BUILD.bazel @@ -0,0 +1,86 @@ +load("@aspect_rules_swc//swc:defs.bzl", "swc") +load("@bazel_skylib//rules:build_test.bzl", "build_test") + +# No root/out +swc( + name = "emit_dts", + srcs = [ + "src/a.ts", + "src/b.ts", + ], + emit_isolated_dts = True, +) + +build_test( + name = "emit_dts-test", + targets = [ + "src/a.js", + "src/a.d.ts", + "src/b.js", + "src/b.d.ts", + ], +) + +# With out_dir +swc( + name = "emit_dts_outdir", + srcs = [ + "src/a.ts", + "src/b.ts", + ], + emit_isolated_dts = True, + out_dir = "out", +) + +build_test( + name = "emit_dts_outdir-test", + targets = [ + "out/src/a.js", + "out/src/a.d.ts", + "out/src/b.js", + "out/src/b.d.ts", + ], +) + +# With root_dir +swc( + name = "emit_dts_rootdir", + srcs = [ + "src/a.ts", + "src/b.ts", + ], + emit_isolated_dts = True, + root_dir = "src", +) + +build_test( + name = "emit_dts_rootdir-test", + targets = [ + "a.js", + "a.d.ts", + "b.js", + "b.d.ts", + ], +) + +# With out_dir and root_dir +swc( + name = "emit_dts_outdir_rootdir", + srcs = [ + "src/a.ts", + "src/b.ts", + ], + emit_isolated_dts = True, + out_dir = "out_root", + root_dir = "src", +) + +build_test( + name = "emit_dts_outdir_rootdir-test", + targets = [ + "out_root/a.js", + "out_root/a.d.ts", + "out_root/b.js", + "out_root/b.d.ts", + ], +) diff --git a/examples/emit_types/src/a.ts b/examples/emit_types/src/a.ts new file mode 100644 index 00000000..f0cf0135 --- /dev/null +++ b/examples/emit_types/src/a.ts @@ -0,0 +1,8 @@ +export interface Foo { + name: string; +} + +export const A = 1; +export const AF: Foo = { + name: "bar", +}; diff --git a/examples/emit_types/src/b.ts b/examples/emit_types/src/b.ts new file mode 100644 index 00000000..cf59416c --- /dev/null +++ b/examples/emit_types/src/b.ts @@ -0,0 +1,6 @@ +import { A, Foo } from "./a"; + +export const B: typeof A = 1; +export const BF: Foo = { + name: "baz", +}; diff --git a/swc/defs.bzl b/swc/defs.bzl index a15e7ad3..4ca76569 100644 --- a/swc/defs.bzl +++ b/swc/defs.bzl @@ -100,10 +100,12 @@ def swc(name, srcs, args = [], data = [], plugins = [], output_dir = False, swcr # Determine js & map outputs js_outs = [] map_outs = [] + dts_outs = [] if not output_dir: js_outs = _swc_lib.calculate_js_outs(srcs, out_dir, root_dir) map_outs = _swc_lib.calculate_map_outs(srcs, source_maps, out_dir, root_dir) + dts_outs = _swc_lib.calculate_dts_outs(srcs, kwargs.get("emit_isolated_dts", False), out_dir, root_dir) swc_compile( name = name, @@ -111,6 +113,7 @@ def swc(name, srcs, args = [], data = [], plugins = [], output_dir = False, swcr plugins = plugins, js_outs = js_outs, map_outs = map_outs, + dts_outs = dts_outs, output_dir = output_dir, source_maps = source_maps, args = args, diff --git a/swc/private/swc.bzl b/swc/private/swc.bzl index 4479d815..de9591d3 100644 --- a/swc/private/swc.bzl +++ b/swc/private/swc.bzl @@ -66,6 +66,13 @@ https://docs.aspect.build/rulesets/aspect_rules_js/docs/js_library#data for more "root_dir": attr.string( doc = "a subdirectory under the input package which should be consider the root directory of all the input files", ), + "emit_isolated_dts": attr.bool( + doc = """Emit .d.ts files instead of .js for TypeScript sources + +EXPERIMENTAL: this API is undocumented, experimental and may change without notice +""", + default = False, + ), } _outputs = { @@ -75,12 +82,19 @@ There should be one for each entry in srcs."""), "map_outs": attr.output_list(doc = """list of expected source map output files. Can be empty, meaning no source maps should be produced. +If non-empty, there should be one for each entry in srcs."""), + "dts_outs": attr.output_list(doc = """list of expected TypeScript declaration files. + +Can be empty, meaning no dts files should be produced. If non-empty, there should be one for each entry in srcs."""), } def _is_ts_src(src): return src.endswith(".ts") or src.endswith(".mts") or src.endswith(".cts") or src.endswith(".tsx") or src.endswith(".jsx") +def _is_typings_src(src): + return src.endswith(".d.ts") or src.endswith(".d.mts") or src.endswith(".d.cts") + def _is_js_src(src): return src.endswith(".mjs") or src.endswith(".cjs") or src.endswith(".js") @@ -112,7 +126,7 @@ def _remove_extension(f): return f if i <= 0 else f[:-(len(f) - i)] def _to_js_out(src, out_dir, root_dir, js_outs = []): - if not _is_supported_src(src): + if not _is_supported_src(src) or _is_typings_src(src): return None exts = { @@ -153,7 +167,7 @@ def _calculate_js_outs(srcs, out_dir, root_dir): def _to_map_out(src, source_maps, out_dir, root_dir): if source_maps == "false" or source_maps == "inline": return None - if not _is_supported_src(src): + if not _is_supported_src(src) or _is_typings_src(src): return None exts = { ".mts": ".mjs.map", @@ -177,6 +191,23 @@ def _calculate_map_outs(srcs, source_maps, out_dir, root_dir): out.append(map_out) return out +def _to_dts_out(src, emit_isolated_dts, out_dir, root_dir): + if not emit_isolated_dts: + return None + if not _is_supported_src(src) or _is_typings_src(src): + return None + dts_out = src[:src.rindex(".")] + ".d.ts" + dts_out = _to_out_path(dts_out, out_dir, root_dir) + return dts_out + +def _calculate_dts_outs(srcs, emit_isolated_dts, out_dir, root_dir): + out = [] + for f in srcs: + dts_out = _to_dts_out(f, emit_isolated_dts, out_dir, root_dir) + if dts_out: + out.append(dts_out) + return out + def _calculate_source_file(ctx, src): if not (ctx.attr.out_dir or ctx.attr.root_dir): return src.basename @@ -252,6 +283,15 @@ def _swc_impl(ctx): inputs.extend(ctx.files.plugins) args.add_all(plugin_args) + if ctx.attr.emit_isolated_dts: + args.add_all(["--config-json", json.encode({ + "jsc": { + "experimental": { + "emitIsolatedDts": True, + }, + }, + })]) + if ctx.attr.output_dir: if len(ctx.attr.srcs) != 1: fail("Under output_dir, there must be a single entry in srcs") @@ -296,19 +336,29 @@ def _swc_impl(ctx): src_path = _relative_to_package(src.path, ctx) + # This source file is a typings file and not transpiled + if _is_typings_src(src_path): + # Copy to the output directory if emitting dts files is enabled + if ctx.attr.emit_isolated_dts: + output_sources.append(src) + continue + js_out_path = _to_js_out(src_path, ctx.attr.out_dir, ctx.attr.root_dir, js_outs_relative) if not js_out_path: # This source file is not a supported src continue js_out = ctx.actions.declare_file(js_out_path) outputs = [js_out] - map_out_path = _to_map_out(src_path, ctx.attr.source_maps, ctx.attr.out_dir, ctx.attr.root_dir) + map_out_path = _to_map_out(src_path, ctx.attr.source_maps, ctx.attr.out_dir, ctx.attr.root_dir) if map_out_path: js_map_out = ctx.actions.declare_file(map_out_path) outputs.append(js_map_out) - src_inputs = [src] + inputs + dts_out_path = _to_dts_out(src_path, ctx.attr.emit_isolated_dts, ctx.attr.out_dir, ctx.attr.root_dir) + if dts_out_path: + dts_out = ctx.actions.declare_file(dts_out_path) + outputs.append(dts_out) src_args.add("--out-file", js_out) @@ -317,7 +367,7 @@ def _swc_impl(ctx): _swc_action( ctx, swc_toolchain.swcinfo.swc_binary, - inputs = src_inputs, + inputs = [src] + inputs, arguments = [ args, src_args, @@ -377,4 +427,5 @@ swc = struct( toolchains = ["@aspect_rules_swc//swc:toolchain_type"], calculate_js_outs = _calculate_js_outs, calculate_map_outs = _calculate_map_outs, + calculate_dts_outs = _calculate_dts_outs, )