From c47e00eba1fc24ab89eea8361153c02f9b92c0f3 Mon Sep 17 00:00:00 2001 From: ericrafalovsky <88681631+ericrafalovsky@users.noreply.github.com> Date: Wed, 22 Jan 2025 16:34:17 -0500 Subject: [PATCH] feat: add generate_trace option to run TypeScript with `--generateTrace` (#765) --- docs/rules.md | 16 +++++---- docs/troubleshooting.md | 4 +++ examples/generate_trace/BUILD.bazel | 6 ++++ examples/generate_trace/a.ts | 1 + examples/generate_trace/tsconfig.json | 1 + ts/BUILD.bazel | 17 ++++++++++ ts/defs.bzl | 5 +++ ts/private/options.bzl | 4 ++- ts/private/ts_lib.bzl | 3 ++ ts/private/ts_project.bzl | 17 +++++++++- ts/test/flags_test.bzl | 48 ++++++++++++++++++++++++++- 11 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 examples/generate_trace/BUILD.bazel create mode 100644 examples/generate_trace/a.ts create mode 100644 examples/generate_trace/tsconfig.json diff --git a/docs/rules.md b/docs/rules.md index 7dbfa0a9..5a9b0a6f 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -41,11 +41,11 @@ load("@aspect_rules_ts//ts:defs.bzl", "ts_project_rule") ts_project_rule(name, deps, srcs, data, allow_js, args, assets, buildinfo_out, composite, declaration, declaration_dir, declaration_map, declaration_transpile, - emit_declaration_only, extends, incremental, is_typescript_5_or_greater, - isolated_typecheck, js_outs, map_outs, no_emit, out_dir, preserve_jsx, - pretranspiled_dts, pretranspiled_js, resolve_json_module, resource_set, root_dir, - source_map, supports_workers, transpile, ts_build_info_file, tsc, tsc_worker, - tsconfig, typing_maps_outs, typings_outs, validate, validator) + emit_declaration_only, extends, generate_trace, incremental, + is_typescript_5_or_greater, isolated_typecheck, js_outs, map_outs, no_emit, out_dir, + preserve_jsx, pretranspiled_dts, pretranspiled_js, resolve_json_module, resource_set, + root_dir, source_map, supports_workers, transpile, ts_build_info_file, tsc, + tsc_worker, tsconfig, typing_maps_outs, typings_outs, validate, validator) Implementation rule behind the ts_project macro. @@ -74,6 +74,7 @@ for srcs and tsconfig, and pre-declaring output files. | declaration_transpile | Whether tsc should be used to produce .d.ts outputs | Boolean | optional | `False` | | emit_declaration_only | https://www.typescriptlang.org/tsconfig#emitDeclarationOnly | Boolean | optional | `False` | | extends | https://www.typescriptlang.org/tsconfig#extends | Label | optional | `None` | +| generate_trace | https://www.typescriptlang.org/tsconfig/#generateTrace | Boolean | optional | `False` | | incremental | https://www.typescriptlang.org/tsconfig#incremental | Boolean | optional | `False` | | is_typescript_5_or_greater | Whether TypeScript version is >= 5.0.0 | Boolean | optional | `False` | | isolated_typecheck | Whether type-checking should be a separate action.

This allows the transpilation action to run without waiting for typings from dependencies.

Requires a minimum version of typescript 5.6 for the [noCheck](https://www.typescriptlang.org/tsconfig#noCheck) flag which is automatically set on the transpilation action when the typecheck action is isolated.

Requires [isolatedDeclarations](https://www.typescriptlang.org/tsconfig#isolatedDeclarations) to be set so that declarations can be emitted without dependencies. The use of `isolatedDeclarations` may require significant changes to your codebase and should be done as a pre-requisite to enabling `isolated_typecheck`. | Boolean | optional | `False` | @@ -131,8 +132,8 @@ load("@aspect_rules_ts//ts:defs.bzl", "ts_project") ts_project(name, tsconfig, srcs, args, data, deps, assets, extends, allow_js, isolated_typecheck, declaration, source_map, declaration_map, resolve_json_module, preserve_jsx, composite, incremental, no_emit, emit_declaration_only, transpiler, declaration_transpiler, - ts_build_info_file, tsc, tsc_worker, validate, validator, declaration_dir, out_dir, - root_dir, supports_workers, kwargs) + ts_build_info_file, generate_trace, tsc, tsc_worker, validate, validator, declaration_dir, + out_dir, root_dir, supports_workers, kwargs) Compiles one TypeScript project using `tsc --project`. @@ -184,6 +185,7 @@ If you have problems getting your `ts_project` to work correctly, read the dedic | transpiler | A custom transpiler tool to run that produces the JavaScript outputs instead of `tsc`.

Under `--@aspect_rules_ts//ts:default_to_tsc_transpiler`, the default is to use `tsc` to produce `.js` outputs 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.

Without `--@aspect_rules_ts//ts:default_to_tsc_transpiler`, an explicit value must be set. This may be the string `"tsc"` to explicitly choose `tsc`, just like the default above.

It may also be any rule or macro with this signature: `(name, srcs, **kwargs)`

If JavaScript outputs are configured to not be emitted the custom transpiler will not be used, such as when `no_emit = True` or `emit_declaration_only = True`.

See [docs/transpiler.md](/docs/transpiler.md) for more details. | `None` | | declaration_transpiler | A custom transpiler tool to run that produces the TypeScript declaration outputs instead of `tsc`.

It may be any rule or macro with this signature: `(name, srcs, **kwargs)`

If TypeScript declaration outputs are configured to not be emitted the custom declaration transpiler will not be used, such as when `no_emit = True` or `declaration = False`.

See [docs/transpiler.md](/docs/transpiler.md) for more details. | `None` | | ts_build_info_file | The user-specified value of `tsBuildInfoFile` from the tsconfig. Helps Bazel to predict the path where the .tsbuildinfo output is written. | `None` | +| generate_trace | Whether to generate a trace file for TypeScript compiler performance analysis. When enabled, creates a trace directory containing performance tracing information that can be loaded in chrome://tracing. Use the `--@aspect_rules_ts//ts:generate_tsc_trace` flag to enable this by default. | `None` | | tsc | Label of the TypeScript compiler binary to run. This allows you to use a custom API-compatible compiler in place of the regular `tsc` such as a custom `js_binary` or Angular's `ngc`. compatible with it such as Angular's `ngc`.

See examples of use in [examples/custom_compiler](https://github.com/aspect-build/rules_ts/blob/main/examples/custom_compiler/BUILD.bazel) | `"@npm_typescript//:tsc"` | | tsc_worker | Label of a custom TypeScript compiler binary which understands Bazel's persistent worker protocol. | `"@npm_typescript//:tsc_worker"` | | validate | Whether to check that the dependencies are valid and the tsconfig JSON settings match the attributes on this target. Set this to `False` to skip running our validator, in case you have a legitimate reason for these to differ, e.g. you have a setting enabled just for the editor but you want different behavior when Bazel runs `tsc`. | `True` | diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 25191747..dd1c7f8d 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -177,3 +177,7 @@ Possible solutions: ``` * Use pnpm workspaces and `npm_package`/`npm_link_package` in between the `ts_project` rule and the `webpack` rule, so that the loader finds files under the `node_modules` tree like it would with third-party npm packages. + + +# Troubleshooting performance issues +Running your build with `--@aspect_rules_ts//ts:generate_tsc_trace` causes the `ts_project` rule to run with the `generate_trace` option enabled. This generates a profile that can be analyzed to understand TypeScript compilation performance. The trace files will be written in `bazel-bin//_trace/`. To analyze it use ChromeDevTools or [@typescript/analyze-trace](https://www.npmjs.com/package/@typescript/analyze-trace). See the [TypeScript documentation](https://github.com/microsoft/TypeScript-wiki/blob/main/Performance-Tracing.md) for more information. \ No newline at end of file diff --git a/examples/generate_trace/BUILD.bazel b/examples/generate_trace/BUILD.bazel new file mode 100644 index 00000000..f2c0cf62 --- /dev/null +++ b/examples/generate_trace/BUILD.bazel @@ -0,0 +1,6 @@ +load("@aspect_rules_ts//ts:defs.bzl", "ts_project") + +ts_project( + name = "lib", + generate_trace = True, +) diff --git a/examples/generate_trace/a.ts b/examples/generate_trace/a.ts new file mode 100644 index 00000000..2d0a38f9 --- /dev/null +++ b/examples/generate_trace/a.ts @@ -0,0 +1 @@ +export const a: string = 'a' diff --git a/examples/generate_trace/tsconfig.json b/examples/generate_trace/tsconfig.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/examples/generate_trace/tsconfig.json @@ -0,0 +1 @@ +{} diff --git a/ts/BUILD.bazel b/ts/BUILD.bazel index c1324d8f..12be7484 100644 --- a/ts/BUILD.bazel +++ b/ts/BUILD.bazel @@ -87,6 +87,12 @@ bool_flag( visibility = ["//visibility:public"], ) +bool_flag( + name = "generate_tsc_trace", + build_setting_default = False, + visibility = ["//visibility:public"], +) + # Note, users could use a Transition to make a subgraph of their depgraph opt-in to skipLibCheck. config_setting( name = "skip_lib_check.always", @@ -119,12 +125,23 @@ config_setting( }, ) +config_setting( + name = "generate_tsc_trace_flag", + flag_values = { + ":generate_tsc_trace": "true", + }, +) + options( name = "options", default_to_tsc_transpiler = select({ ":default_to_tsc_transpiler_flag": True, "//conditions:default": False, }), + generate_tsc_trace = select({ + ":generate_tsc_trace_flag": True, + "//conditions:default": False, + }), skip_lib_check = select( { "@aspect_rules_ts//ts:skip_lib_check.always": True, diff --git a/ts/defs.bzl b/ts/defs.bzl index 44a8f2fa..7d560d1f 100644 --- a/ts/defs.bzl +++ b/ts/defs.bzl @@ -52,6 +52,7 @@ def ts_project( transpiler = None, declaration_transpiler = None, ts_build_info_file = None, + generate_trace = None, tsc = _tsc, tsc_worker = _tsc_worker, validate = True, @@ -239,6 +240,9 @@ def ts_project( Instructs Bazel *not* to expect `.js` or `.js.map` outputs for `.ts` sources. ts_build_info_file: The user-specified value of `tsBuildInfoFile` from the tsconfig. Helps Bazel to predict the path where the .tsbuildinfo output is written. + generate_trace: Whether to generate a trace file for TypeScript compiler performance analysis. + When enabled, creates a trace directory containing performance tracing information that can be + loaded in chrome://tracing. Use the `--@aspect_rules_ts//ts:generate_tsc_trace` flag to enable this by default. supports_workers: Whether the "Persistent Worker" protocol is enabled. This uses a custom `tsc` compiler to make rebuilds faster. @@ -468,6 +472,7 @@ def ts_project( source_map = source_map, declaration_map = declaration_map, ts_build_info_file = ts_build_info_file, + generate_trace = generate_trace, out_dir = out_dir, root_dir = root_dir, js_outs = tsc_js_outs, diff --git a/ts/private/options.bzl b/ts/private/options.bzl index 1dfb42f2..c3e74333 100644 --- a/ts/private/options.bzl +++ b/ts/private/options.bzl @@ -13,7 +13,7 @@ Please read https://docs.aspect.build/rules/aspect_rules_ts/docs/transpiler OptionsInfo = provider( doc = "Internal: Provider that carries verbosity and global worker support information.", - fields = ["args", "default_to_tsc_transpiler", "verbose", "supports_workers"], + fields = ["args", "default_to_tsc_transpiler", "verbose", "supports_workers", "generate_tsc_trace"], ) def _options_impl(ctx): @@ -47,6 +47,7 @@ def _options_impl(ctx): args = args, supports_workers = ctx.attr.supports_workers, default_to_tsc_transpiler = ctx.attr.default_to_tsc_transpiler, + generate_tsc_trace = ctx.attr.generate_tsc_trace, ) options = rule( @@ -56,5 +57,6 @@ options = rule( "verbose": attr.bool(), "supports_workers": attr.bool(), "skip_lib_check": attr.bool(), + "generate_tsc_trace": attr.bool(), }, ) diff --git a/ts/private/ts_lib.bzl b/ts/private/ts_lib.bzl index 2da88f4d..9ca955e6 100644 --- a/ts/private/ts_lib.bzl +++ b/ts/private/ts_lib.bzl @@ -162,6 +162,9 @@ COMPILER_OPTION_ATTRS = { "ts_build_info_file": attr.string( doc = "https://www.typescriptlang.org/tsconfig#tsBuildInfoFile", ), + "generate_trace": attr.bool( + doc = "https://www.typescriptlang.org/tsconfig/#generateTrace", + ), } # tsc knows how to produce the following kinds of output files. diff --git a/ts/private/ts_project.bzl b/ts/private/ts_project.bzl index 1a016666..9f55498a 100644 --- a/ts/private/ts_project.bzl +++ b/ts/private/ts_project.bzl @@ -196,6 +196,8 @@ See https://github.com/aspect-build/rules_ts/issues/361 for more details. common_args.extend(["--tsBuildInfoFile", to_output_relative_path(ctx.outputs.buildinfo_out)]) outputs.append(ctx.outputs.buildinfo_out) + should_generate_tsc_trace = options.generate_tsc_trace or ctx.attr.generate_trace + output_sources = js_outs + map_outs + assets_outs + ctx.files.pretranspiled_js output_types = typings_outs + typing_maps_outs + ctx.files.pretranspiled_dts @@ -241,16 +243,24 @@ See https://github.com/aspect-build/rules_ts/issues/361 for more details. # or # - not invoking tsc for output files at all if ctx.attr.isolated_typecheck or not (use_tsc_for_js or use_tsc_for_dts): + typecheck_outputs = [] + # The type-checking action still need to produce some output, so we output the stdout # to a .typecheck file that ends up in the typecheck output group. typecheck_output = ctx.actions.declare_file(ctx.attr.name + ".typecheck") typecheck_outs.append(typecheck_output) + typecheck_outputs.append(typecheck_output) typecheck_arguments = ctx.actions.args() typecheck_arguments.add_all(common_args) typecheck_arguments.add("--noEmit") + if should_generate_tsc_trace: + tsc_trace_dir = ctx.actions.declare_directory(ctx.attr.name + "_trace") + typecheck_outputs.append(tsc_trace_dir) + typecheck_arguments.add_all(["--generateTrace", to_output_relative_path(tsc_trace_dir)]) + env = { "BAZEL_BINDIR": ctx.bin_dir.path, } @@ -271,7 +281,7 @@ See https://github.com/aspect-build/rules_ts/issues/361 for more details. executable = executable, inputs = transitive_inputs_depset, arguments = [typecheck_arguments], - outputs = [typecheck_output], + outputs = typecheck_outputs, mnemonic = "TsProjectCheck", execution_requirements = execution_requirements, resource_set = resource_set(ctx.attr), @@ -302,6 +312,11 @@ See https://github.com/aspect-build/rules_ts/issues/361 for more details. # Not emitting declarations tsc_emit_arguments.add("--declaration", "false") + if should_generate_tsc_trace and not ctx.attr.isolated_typecheck: + tsc_trace_dir = ctx.actions.declare_directory(ctx.attr.name + "_trace") + outputs.append(tsc_trace_dir) + tsc_emit_arguments.add_all(["--generateTrace", to_output_relative_path(tsc_trace_dir)]) + inputs_depset = inputs if ctx.attr.isolated_typecheck else transitive_inputs_depset if supports_workers: diff --git a/ts/test/flags_test.bzl b/ts/test/flags_test.bzl index be504503..0fe8a0f6 100644 --- a/ts/test/flags_test.bzl +++ b/ts/test/flags_test.bzl @@ -11,6 +11,7 @@ def _transition_impl(_settings, attr): "@aspect_rules_ts//ts:supports_workers": attr.supports_workers, "@aspect_rules_ts//ts:verbose": attr.verbose, "@aspect_rules_ts//ts:skipLibCheck": attr.skip_lib_check, + "@aspect_rules_ts//ts:generate_tsc_trace": attr.generate_tsc_trace, } configuration_transition = transition( @@ -20,6 +21,7 @@ configuration_transition = transition( "@aspect_rules_ts//ts:supports_workers", "@aspect_rules_ts//ts:verbose", "@aspect_rules_ts//ts:skipLibCheck", + "@aspect_rules_ts//ts:generate_tsc_trace", ], ) @@ -37,6 +39,7 @@ _transition_rule = rule( "supports_workers": attr.bool(default = True), "verbose": attr.bool(default = False), "skip_lib_check": attr.string(default = "honor_tsconfig"), + "generate_tsc_trace": attr.bool(default = False), "_allowlist_function_transition": attr.label( default = "@bazel_tools//tools/allowlists/function_transition_allowlist", ), @@ -103,7 +106,27 @@ def _skip_lib_check_honor_tsconfig_test_impl(ctx): _skip_lib_check_honor_tsconfig_test = analysistest.make(_skip_lib_check_honor_tsconfig_test_impl) -def _ts_project_with_flags(name, supports_workers = None, supports_workers_flag = None, verbose_flag = None, skip_lib_check_flag = None, **kwargs): +# generate_tsc_trace flag = true test +def _generate_tsc_trace_true_test_impl(ctx): + env = analysistest.begin(ctx) + target_under_test = analysistest.target_under_test(env) + action = _find_tsc_action(env, target_under_test) + asserts.true(env, "--generateTrace" in action.argv, "expected --generateTrace to be set") + return analysistest.end(env) + +_generate_tsc_trace_true_test = analysistest.make(_generate_tsc_trace_true_test_impl) + +# generate_tsc_trace flag = false test +def _generate_tsc_trace_false_test_impl(ctx): + env = analysistest.begin(ctx) + target_under_test = analysistest.target_under_test(env) + action = _find_tsc_action(env, target_under_test) + asserts.false(env, "--generateTrace" in action.argv, "expected --generateTrace to not be set") + return analysistest.end(env) + +_generate_tsc_trace_false_test = analysistest.make(_generate_tsc_trace_false_test_impl) + +def _ts_project_with_flags(name, supports_workers = None, supports_workers_flag = None, verbose_flag = None, skip_lib_check_flag = None, generate_tsc_trace_flag = None, **kwargs): write_file( name = "{}_write".format(name), out = "{}.ts".format(name), @@ -123,6 +146,7 @@ def _ts_project_with_flags(name, supports_workers = None, supports_workers_flag supports_workers = supports_workers_flag, verbose = verbose_flag, skip_lib_check = skip_lib_check_flag, + generate_tsc_trace = generate_tsc_trace_flag, ) def ts_project_flags_test_suite(name): @@ -179,6 +203,26 @@ def ts_project_flags_test_suite(name): target_under_test = ":skip_lib_check_honor_tsconfig", ) + _ts_project_with_flags( + name = "generate_tsc_trace_true", + tsconfig = _TSCONFIG, + generate_tsc_trace_flag = True, + ) + _generate_tsc_trace_true_test( + name = "generate_tsc_trace_true_test", + target_under_test = ":generate_tsc_trace_true", + ) + + _ts_project_with_flags( + name = "generate_tsc_trace_false", + tsconfig = _TSCONFIG, + generate_tsc_trace_flag = False, + ) + _generate_tsc_trace_false_test( + name = "generate_tsc_trace_false_test", + target_under_test = ":generate_tsc_trace_false", + ) + native.test_suite( name = name, tests = [ @@ -186,5 +230,7 @@ def ts_project_flags_test_suite(name): ":verbose_false_test", ":skip_lib_check_always_test", ":skip_lib_check_honor_tsconfig_test", + ":generate_tsc_trace_true_test", + ":generate_tsc_trace_false_test", ], )