diff --git a/docs/rules.md b/docs/rules.md
index 11f3c9d3..e94464bc 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -37,10 +37,10 @@ extended configuration file as well, to pass them both to the TypeScript compile
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,
- 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)
+ incremental, is_typescript_5_or_greater, isolated_typecheck, 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)
Implementation rule behind the ts_project macro.
@@ -70,6 +70,7 @@ for srcs and tsconfig, and pre-declaring output files.
| extends | https://www.typescriptlang.org/tsconfig#extends | Label | optional | `None` |
| 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` |
| js_outs | Locations in bazel-out where tsc will write `.js` files | List of labels | optional | `[]` |
| map_outs | Locations in bazel-out where tsc will write `.js.map` files | List of labels | optional | `[]` |
| no_emit | https://www.typescriptlang.org/tsconfig#noEmit | 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, isolated_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` |
+| isolated_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/isolated_typecheck/BUILD.bazel b/examples/isolated_typecheck/BUILD.bazel
new file mode 100644
index 00000000..ab1b80bf
--- /dev/null
+++ b/examples/isolated_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/isolated_typecheck/backend/BUILD.bazel b/examples/isolated_typecheck/backend/BUILD.bazel
new file mode 100644
index 00000000..493ac70b
--- /dev/null
+++ b/examples/isolated_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",
+ declaration = True,
+ isolated_typecheck = True,
+ tsconfig = "//examples/isolated_typecheck:tsconfig",
+ deps = ["//examples/isolated_typecheck/core"],
+)
+
+assert_outputs(
+ name = "test_backend_default_outputs",
+ actual = "backend",
+ expected = ["examples/isolated_typecheck/backend/index.js"],
+)
diff --git a/examples/isolated_typecheck/backend/index.ts b/examples/isolated_typecheck/backend/index.ts
new file mode 100644
index 00000000..e9cd9074
--- /dev/null
+++ b/examples/isolated_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/isolated_typecheck/core/BUILD.bazel b/examples/isolated_typecheck/core/BUILD.bazel
new file mode 100644
index 00000000..78ceb548
--- /dev/null
+++ b/examples/isolated_typecheck/core/BUILD.bazel
@@ -0,0 +1,9 @@
+load("@aspect_rules_ts//ts:defs.bzl", "ts_project")
+
+ts_project(
+ name = "core",
+ declaration = True,
+ isolated_typecheck = True,
+ tsconfig = "//examples/isolated_typecheck:tsconfig",
+ visibility = ["//examples/isolated_typecheck:__subpackages__"],
+)
diff --git a/examples/isolated_typecheck/core/index.ts b/examples/isolated_typecheck/core/index.ts
new file mode 100644
index 00000000..7870ee3c
--- /dev/null
+++ b/examples/isolated_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/isolated_typecheck/frontend/BUILD.bazel b/examples/isolated_typecheck/frontend/BUILD.bazel
new file mode 100644
index 00000000..1d0c6f45
--- /dev/null
+++ b/examples/isolated_typecheck/frontend/BUILD.bazel
@@ -0,0 +1,13 @@
+load("@aspect_rules_ts//ts:defs.bzl", "ts_project")
+
+ts_project(
+ name = "frontend",
+ isolated_typecheck = True,
+ tsconfig = {
+ "compilerOptions": {
+ "declaration": True,
+ "isolatedDeclarations": True,
+ },
+ },
+ deps = ["//examples/isolated_typecheck/core"],
+)
diff --git a/examples/isolated_typecheck/frontend/index.ts b/examples/isolated_typecheck/frontend/index.ts
new file mode 100644
index 00000000..4f68e360
--- /dev/null
+++ b/examples/isolated_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/isolated_typecheck/tsconfig.json b/examples/isolated_typecheck/tsconfig.json
new file mode 100644
index 00000000..281da336
--- /dev/null
+++ b/examples/isolated_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 25edf7cb..03e6d17e 100644
--- a/ts/defs.bzl
+++ b/ts/defs.bzl
@@ -40,6 +40,7 @@ def ts_project(
assets = [],
extends = None,
allow_js = False,
+ isolated_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`.
+ isolated_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
@@ -416,6 +421,7 @@ def ts_project(
incremental = incremental,
preserve_jsx = preserve_jsx,
composite = composite,
+ isolated_typecheck = isolated_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..a0e9b55c 100644
--- a/ts/private/ts_lib.bzl
+++ b/ts/private/ts_lib.bzl
@@ -87,6 +87,20 @@ https://docs.aspect.build/rulesets/aspect_rules_js/docs/js_library#deps for more
mandatory = True,
allow_single_file = [".json"],
),
+ "isolated_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 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`.
+ """,
+ ),
"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..6cda0340 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 isolated typechecking action was explicitly requested
+ # or
+ # - not invoking tsc for output files at all
+ use_isolated_typecheck = ctx.attr.isolated_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)
- if use_tsc_noemit:
+ 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_isolated_typecheck:
# 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.isolated_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.isolated_typecheck else "Transpiling & type-checking"
- if stdout_file != "":
- env["JS_BINARY__STDOUT_OUTPUT_FILE"] = stdout_file
+ inputs_depset = inputs if ctx.attr.isolated_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.isolated_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..016213b6 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.isolated_typecheck) {
+ var optionVal = getTsOption('isolatedDeclarations')
+ if (!optionVal) {
+ failures.push(
+ 'attribute isolated_typecheck=True requires compilerOptions.isolatedDeclarations=true\nSee documentation on ts_project(isolated_typecheck) for more info"'
+ )
+ buildozerCmds.push('set isolated_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..ff5f2669 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,
+ isolated_typecheck = ctx.attr.isolated_typecheck,
)
arguments.add_all([
to_output_relative_path(tsconfig),