diff --git a/docs/rules.md b/docs/rules.md index 4856f702..c3e28ed1 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -35,9 +35,9 @@ extended configuration file as well, to pass them both to the TypeScript compile ## ts_project_rule
-ts_project_rule(name, deps, srcs, data, allow_js, args, assets, buildinfo_out, composite, - declaration, declaration_dir, declaration_map, emit_declaration_only, extends, - incremental, is_typescript_5_or_greater, js_outs, map_outs, no_emit, out_dir, +ts_project_rule(name, deps, srcs, data, allow_js, args, assets, async_typecheck, buildinfo_out, + composite, declaration, declaration_dir, declaration_map, emit_declaration_only, + extends, incremental, is_typescript_5_or_greater, js_outs, map_outs, no_emit, out_dir, preserve_jsx, 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) @@ -61,6 +61,7 @@ for srcs and tsconfig, and pre-declaring output files. | allow_js | https://www.typescriptlang.org/tsconfig#allowJs | Boolean | optional | `False` | | args | https://www.typescriptlang.org/docs/handbook/compiler-options.html | List of strings | optional | `[]` | | assets | Files which are needed by a downstream build step such as a bundler.
See more details on the `assets` parameter of the `ts_project` macro. | List of labels | optional | `[]` | +| async_typecheck | Whether type-checking should be a separate action.
This allows the transpilation action to run without waiting for typings from dependencies.
Requires the typescript 5.6 [noCheck](https://www.typescriptlang.org/tsconfig#noCheck).
Requires tsconfig [isolatedDeclarations](https://www.typescriptlang.org/tsconfig#isolatedDeclarations) to allow declarations to be emitted without dependencies. | Boolean | optional | `False` | | buildinfo_out | Location in bazel-out where tsc will write a `.tsbuildinfo` file | Label | optional | `None` | | composite | https://www.typescriptlang.org/tsconfig#composite | Boolean | optional | `False` | | declaration | https://www.typescriptlang.org/tsconfig#declaration | Boolean | optional | `False` | @@ -116,10 +117,11 @@ along with any transitively referenced tsconfig.json files chained by the ## ts_project-ts_project(name, tsconfig, srcs, args, data, deps, assets, extends, allow_js, declaration, - source_map, declaration_map, resolve_json_module, preserve_jsx, composite, incremental, - no_emit, emit_declaration_only, transpiler, ts_build_info_file, tsc, tsc_worker, validate, - validator, declaration_dir, out_dir, root_dir, supports_workers, kwargs) +ts_project(name, tsconfig, srcs, args, data, deps, assets, extends, allow_js, async_typecheck, + declaration, source_map, declaration_map, resolve_json_module, preserve_jsx, composite, + incremental, no_emit, emit_declaration_only, transpiler, ts_build_info_file, tsc, + tsc_worker, validate, validator, declaration_dir, out_dir, root_dir, supports_workers, + kwargs)Compiles one TypeScript project using `tsc --project`. @@ -158,6 +160,7 @@ If you have problems getting your `ts_project` to work correctly, read the dedic | assets | Files which are needed by a downstream build step such as a bundler.
These files are **not** included as inputs to any actions spawned by `ts_project`. They are not transpiled, and are not visible to the type-checker. Instead, these files appear among the *outputs* of this target.
A typical use is when your TypeScript code has an import that TS itself doesn't understand such as
`import './my.scss'`
and the type-checker allows this because you have an "ambient" global type declaration like
`declare module '*.scss' { ... }`
A bundler like webpack will expect to be able to resolve the `./my.scss` import to a file and doesn't care about the typing declaration. A bundler runs as a build step, so it does not see files included in the `data` attribute.
Note that `data` is used for files that are resolved by some binary, including a test target. Behind the scenes, `data` populates Bazel's Runfiles object in `DefaultInfo`, while this attribute populates the `transitive_sources` of the `JsInfo`. | `[]` | | extends | Label of the tsconfig file referenced in the `extends` section of tsconfig To support "chaining" of more than one extended config, this label could be a target that provdes `TsConfigInfo` such as `ts_config`. | `None` | | allow_js | Whether TypeScript will read .js and .jsx files. When used with `declaration`, TypeScript will generate `.d.ts` files from `.js` files. | `False` | +| async_typecheck | Whether to type-check asynchronously as a separate bazel action. Requires https://devblogs.microsoft.com/typescript/announcing-typescript-5-6/#the---nocheck-option6 Requires https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-5.html#isolated-declarations | `False` | | declaration | Whether the `declaration` bit is set in the tsconfig. Instructs Bazel to expect a `.d.ts` output for each `.ts` source. | `False` | | source_map | Whether the `sourceMap` bit is set in the tsconfig. Instructs Bazel to expect a `.js.map` output for each `.ts` source. | `False` | | declaration_map | Whether the `declarationMap` bit is set in the tsconfig. Instructs Bazel to expect a `.d.ts.map` output for each `.ts` source. | `False` | diff --git a/examples/async_typecheck/BUILD.bazel b/examples/async_typecheck/BUILD.bazel new file mode 100644 index 00000000..ab1b80bf --- /dev/null +++ b/examples/async_typecheck/BUILD.bazel @@ -0,0 +1,7 @@ +load("@aspect_rules_ts//ts:defs.bzl", "ts_config") + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + visibility = [":__subpackages__"], +) diff --git a/examples/async_typecheck/backend/BUILD.bazel b/examples/async_typecheck/backend/BUILD.bazel new file mode 100644 index 00000000..40ddcd65 --- /dev/null +++ b/examples/async_typecheck/backend/BUILD.bazel @@ -0,0 +1,16 @@ +load("@aspect_bazel_lib//lib:testing.bzl", "assert_outputs") +load("@aspect_rules_ts//ts:defs.bzl", "ts_project") + +ts_project( + name = "backend", + async_typecheck = True, + declaration = True, + tsconfig = "//examples/async_typecheck:tsconfig", + deps = ["//examples/async_typecheck/core"], +) + +assert_outputs( + name = "test_backend_default_outputs", + actual = "backend", + expected = ["examples/async_typecheck/backend/index.js"], +) diff --git a/examples/async_typecheck/backend/index.ts b/examples/async_typecheck/backend/index.ts new file mode 100644 index 00000000..e9cd9074 --- /dev/null +++ b/examples/async_typecheck/backend/index.ts @@ -0,0 +1,10 @@ +import type { IntersectionType } from '../core' + +// Example object of IntersectionType +const myObject: IntersectionType = { + a: 42, + b: 'backend', + c: true, +} + +console.log(myObject) diff --git a/examples/async_typecheck/core/BUILD.bazel b/examples/async_typecheck/core/BUILD.bazel new file mode 100644 index 00000000..cda51eac --- /dev/null +++ b/examples/async_typecheck/core/BUILD.bazel @@ -0,0 +1,9 @@ +load("@aspect_rules_ts//ts:defs.bzl", "ts_project") + +ts_project( + name = "core", + async_typecheck = True, + declaration = True, + tsconfig = "//examples/async_typecheck:tsconfig", + visibility = ["//examples/async_typecheck:__subpackages__"], +) diff --git a/examples/async_typecheck/core/index.ts b/examples/async_typecheck/core/index.ts new file mode 100644 index 00000000..7870ee3c --- /dev/null +++ b/examples/async_typecheck/core/index.ts @@ -0,0 +1,20 @@ +/** + * A file with some non-trivial types, so type-checking it may take some time. + * This helps to motivate the example: we'd like to be able to type-check the frontend and backend in parallel with this file. + */ +type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( + k: infer I +) => void + ? I + : never + +// Example usage +type UnionType = { a: number } | { b: string } | { c: boolean } + +export type IntersectionType = UnionToIntersection+ +export const MyIntersectingValue: IntersectionType = { + a: 1, + b: '2', + c: true, +} diff --git a/examples/async_typecheck/frontend/BUILD.bazel b/examples/async_typecheck/frontend/BUILD.bazel new file mode 100644 index 00000000..81fc94c3 --- /dev/null +++ b/examples/async_typecheck/frontend/BUILD.bazel @@ -0,0 +1,13 @@ +load("@aspect_rules_ts//ts:defs.bzl", "ts_project") + +ts_project( + name = "frontend", + async_typecheck = True, + tsconfig = { + "compilerOptions": { + "declaration": True, + "isolatedDeclarations": True, + }, + }, + deps = ["//examples/async_typecheck/core"], +) diff --git a/examples/async_typecheck/frontend/index.ts b/examples/async_typecheck/frontend/index.ts new file mode 100644 index 00000000..4f68e360 --- /dev/null +++ b/examples/async_typecheck/frontend/index.ts @@ -0,0 +1,13 @@ +import type { IntersectionType } from '../core' +import { MyIntersectingValue } from '../core' + +// Example object of IntersectionType +const myObject: IntersectionType = { + a: 42, + b: 'frontend', + c: true, +} + +const otherObject = MyIntersectingValue + +console.log(myObject, otherObject, myObject === otherObject) diff --git a/examples/async_typecheck/tsconfig.json b/examples/async_typecheck/tsconfig.json new file mode 100644 index 00000000..281da336 --- /dev/null +++ b/examples/async_typecheck/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "isolatedDeclarations": true, + "declaration": true + }, + // Workaround https://github.com/microsoft/TypeScript/issues/59036 + "exclude": [] +} diff --git a/ts/defs.bzl b/ts/defs.bzl index c130cbff..68d5b8ae 100644 --- a/ts/defs.bzl +++ b/ts/defs.bzl @@ -40,6 +40,7 @@ def ts_project( assets = [], extends = None, allow_js = False, + async_typecheck = False, declaration = False, source_map = False, declaration_map = False, @@ -153,6 +154,10 @@ def ts_project( See https://www.typescriptlang.org/docs/handbook/compiler-options.html#compiler-options Typically useful arguments for debugging are `--listFiles` and `--listEmittedFiles`. + async_typecheck: Whether to type-check asynchronously as a separate bazel action. + Requires https://devblogs.microsoft.com/typescript/announcing-typescript-5-6/#the---nocheck-option6 + Requires https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-5.html#isolated-declarations + 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 @@ -217,7 +222,7 @@ def ts_project( incremental: Whether the `incremental` bit is set in the tsconfig. Instructs Bazel to expect a `.tsbuildinfo` output. no_emit: Whether the `noEmit` bit is set in the tsconfig. - Instructs Bazel *not* to expect any outputs. Only a validation action is used. + Instructs Bazel *not* to expect any outputs. emit_declaration_only: Whether the `emitDeclarationOnly` bit is set in the tsconfig. 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. @@ -416,6 +421,7 @@ def ts_project( incremental = incremental, preserve_jsx = preserve_jsx, composite = composite, + async_typecheck = async_typecheck, declaration = declaration, declaration_dir = declaration_dir, source_map = source_map, diff --git a/ts/private/ts_lib.bzl b/ts/private/ts_lib.bzl index de940fdb..99cab366 100644 --- a/ts/private/ts_lib.bzl +++ b/ts/private/ts_lib.bzl @@ -87,6 +87,18 @@ https://docs.aspect.build/rulesets/aspect_rules_js/docs/js_library#deps for more mandatory = True, allow_single_file = [".json"], ), + "async_typecheck": attr.bool( + doc = """\ + Whether type-checking should be a separate action. + + This allows the transpilation action to run without waiting for typings from dependencies. + + Requires the typescript 5.6 [noCheck](https://www.typescriptlang.org/tsconfig#noCheck). + + Requires tsconfig [isolatedDeclarations](https://www.typescriptlang.org/tsconfig#isolatedDeclarations) + to allow declarations to be emitted without dependencies. + """, + ), "validate": attr.bool( doc = """whether to add a Validation Action to verify the other attributes match settings in the tsconfig.json file""", diff --git a/ts/private/ts_project.bzl b/ts/private/ts_project.bzl index efba3982..de9708df 100644 --- a/ts/private/ts_project.bzl +++ b/ts/private/ts_project.bzl @@ -182,75 +182,101 @@ See https://github.com/aspect-build/rules_ts/issues/361 for more details. fail(transpiler_selection_required) output_types = typings_outs + typing_maps_outs + typings_srcs - use_tsc_noemit = ctx.attr.no_emit or (ctx.attr.transpile == 0 and not ctx.attr.declaration) + + # What tsc will be emitting + use_tsc_for_js = len(js_outs) > 0 + use_tsc_for_dts = len(typings_outs) > 0 + + # Use a separate non-emitting action for type-checking when: + # - a separate action was explicitly requested + # or + # - not invoking tsc for output files at all + use_tsc_noemit = ctx.attr.async_typecheck or not (use_tsc_for_js or use_tsc_for_dts) + + # We don't produce any DefaultInfo outputs in this case, because we avoid running the tsc action + # unless the output_types are requested. + default_outputs = [] # Default outputs (DefaultInfo files) is what you see on the command-line for a built # library, and determines what files are used by a simple non-provider-aware downstream # library. Only the JavaScript outputs are intended for use in non-TS-aware dependents. - if ctx.attr.transpile != 0: + if use_tsc_for_js: # Special case case where there are no source outputs and we don't have a custom # transpiler so we add output_types to the default outputs default_outputs = output_sources if len(output_sources) else output_types - else: - # We must avoid tsc writing any JS files in this case, as tsc was only run for typings, and some other - # action will try to write the JS files. We must avoid collisions where two actions write the same file. - if not use_tsc_noemit: - arguments.add("--emitDeclarationOnly") - - # We don't produce any DefaultInfo outputs in this case, because we avoid running the tsc action - # unless the output_types are requested. - default_outputs = [] srcs_tsconfig_deps = ctx.attr.srcs + [ctx.attr.tsconfig] + ctx.attr.deps - stdout_file = "" + inputs = copy_files_to_bin_actions(ctx, inputs) + transitive_inputs.append(_gather_types_from_js_infos(srcs_tsconfig_deps)) + transitive_inputs_depset = depset( + inputs, + transitive = transitive_inputs, + ) + + # tsc action for type-checking if use_tsc_noemit: # 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) - outputs.append(typecheck_output) - stdout_file = typecheck_output.path - + typecheck_arguments = ctx.actions.args() if supports_workers: - arguments.add("--bazelValidationFile", typecheck_output.short_path) + typecheck_arguments.add("--bazelValidationFile", typecheck_output.short_path) - arguments.add("--noEmit") + typecheck_arguments.add("--noEmit") + + ctx.actions.run( + executable = executable, + inputs = transitive_inputs_depset, + arguments = [arguments, typecheck_arguments], + outputs = [typecheck_output], + mnemonic = "TsProjectCheck", + execution_requirements = execution_requirements, + resource_set = resource_set(ctx.attr), + progress_message = "Type-checking TypeScript project %s [tsc -p %s]" % ( + ctx.label, + tsconfig_path, + ), + env = { + "BAZEL_BINDIR": ctx.bin_dir.path, + "JS_BINARY__STDOUT_OUTPUT_FILE": typecheck_output.path, + }, + ) else: typecheck_outs.extend(output_types) - inputs_depset = depset() - if len(outputs) > 0: - transitive_inputs.append(_gather_types_from_js_infos(srcs_tsconfig_deps)) + if use_tsc_for_js or use_tsc_for_dts: + tsc_emit_arguments = ctx.actions.args() - inputs_depset = depset( - copy_files_to_bin_actions(ctx, inputs), - transitive = transitive_inputs, - ) + # Type-checking is done async as a separate action and can be skipped. + if ctx.attr.async_typecheck: + tsc_emit_arguments.add("--noCheck") + tsc_emit_arguments.add("--skipLibCheck") + tsc_emit_arguments.add("--noResolve") + + if not use_tsc_for_js: + # Not emitting js + tsc_emit_arguments.add("--emitDeclarationOnly") - if ctx.attr.transpile != 0 and not ctx.attr.emit_declaration_only and not ctx.attr.no_emit: - if ctx.attr.declaration: - verb = "Transpiling & type-checking" - else: - verb = "Transpiling" - else: - verb = "Type-checking" + elif not use_tsc_for_dts: + # Not emitting declarations + # TODO: why doesn't this work with workers? + if not supports_workers: + tsc_emit_arguments.add("--declaration", "false") - env = { - "BAZEL_BINDIR": ctx.bin_dir.path, - } + verb = "Transpiling" if ctx.attr.async_typecheck else "Transpiling & type-checking" - if stdout_file != "": - env["JS_BINARY__STDOUT_OUTPUT_FILE"] = stdout_file + inputs_depset = inputs if ctx.attr.async_typecheck else transitive_inputs_depset ctx.actions.run( executable = executable, inputs = inputs_depset, - arguments = [arguments], + arguments = [arguments, tsc_emit_arguments], outputs = outputs, - mnemonic = "TsProject", + mnemonic = "TsProjectEmit" if ctx.attr.async_typecheck else "TsProject", execution_requirements = execution_requirements, resource_set = resource_set(ctx.attr), progress_message = "%s TypeScript project %s [tsc -p %s]" % ( @@ -258,7 +284,9 @@ See https://github.com/aspect-build/rules_ts/issues/361 for more details. ctx.label, tsconfig_path, ), - env = env, + env = { + "BAZEL_BINDIR": ctx.bin_dir.path, + }, ) transitive_sources = js_lib_helpers.gather_transitive_sources(output_sources, srcs_tsconfig_deps) @@ -303,7 +331,7 @@ See https://github.com/aspect-build/rules_ts/issues/361 for more details. types = output_types_depset, typecheck = depset(typecheck_outs), # make the inputs to the tsc action available for analysis testing - _action_inputs = inputs_depset, + _action_inputs = transitive_inputs_depset, # https://bazel.build/extending/rules#validations_output_group # "hold the otherwise unused outputs of validation actions" _validation = validation_outs, diff --git a/ts/private/ts_project_options_validator.js b/ts/private/ts_project_options_validator.js index 9a67c69a..a01a09a5 100755 --- a/ts/private/ts_project_options_validator.js +++ b/ts/private/ts_project_options_validator.js @@ -131,6 +131,17 @@ function main(_a) { ) } } + function check_nocheck() { + if (attrs.async_typecheck) { + var optionVal = getTsOption('isolatedDeclarations') + if (!optionVal) { + failures.push( + 'attribute async_typecheck=True requires compilerOptions.isolatedDeclarations=true' + ) + buildozerCmds.push('set async_typecheck False') + } + } + } if (options.preserveSymlinks) { console.error( 'ERROR: ts_project rule ' + @@ -152,6 +163,7 @@ function main(_a) { check('declaration') check('incremental') check('tsBuildInfoFile', 'ts_build_info_file') + check_nocheck() check_preserve_jsx() if (failures.length > 0) { console.error( diff --git a/ts/private/ts_validate_options.bzl b/ts/private/ts_validate_options.bzl index 793ea490..240147c3 100644 --- a/ts/private/ts_validate_options.bzl +++ b/ts/private/ts_validate_options.bzl @@ -36,6 +36,7 @@ def _validate_action(ctx, tsconfig_inputs): source_map = ctx.attr.source_map, incremental = ctx.attr.incremental, ts_build_info_file = ctx.attr.ts_build_info_file, + async_typecheck = ctx.attr.async_typecheck, ) arguments.add_all([ to_output_relative_path(tsconfig),