From 193d2f29f5e59af7273c3e4abcdaadefaa5fed67 Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Wed, 28 Aug 2024 22:20:47 -0700 Subject: [PATCH] feat: transpile using --noCheck when isolatedDeclarations Ref #88 Ref #374 --- docs/rules.md | 18 +-- examples/isolated_declarations/README.md | 13 --- .../isolated_declarations/backend/BUILD.bazel | 1 + .../isolated_declarations/core/BUILD.bazel | 1 + examples/isolated_declarations/core/index.ts | 6 + .../frontend/BUILD.bazel | 1 + .../isolated_declarations/frontend/index.ts | 5 +- ts/defs.bzl | 50 +++++++-- ts/private/ts_lib.bzl | 3 + ts/private/ts_project.bzl | 103 ++++++++++++------ ts/private/ts_project_options_validator.js | 1 + ts/private/ts_validate_options.bzl | 1 + 12 files changed, 140 insertions(+), 63 deletions(-) delete mode 100644 examples/isolated_declarations/README.md diff --git a/docs/rules.md b/docs/rules.md index aa5f71f4..87632c2d 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, 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_declarations, js_outs, map_outs,
+                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_declarations | https://www.typescriptlang.org/tsconfig/#isolatedDeclarations | 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 | `[]` | | out_dir | https://www.typescriptlang.org/tsconfig#outDir | String | optional | `""` | @@ -115,10 +116,10 @@ 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,
-           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_declarations,
+           declaration, source_map, declaration_map, resolve_json_module, preserve_jsx, composite,
+           incremental, 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`. @@ -157,6 +158,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_declarations | Whether to enforce that declaration output (.d.ts file) can be produced for a single source file at a time. Requires some additional explicit types on exported symbols. See https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-5.html#isolated-declarations | `None` | | 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_declarations/README.md b/examples/isolated_declarations/README.md deleted file mode 100644 index ec8aefc8..00000000 --- a/examples/isolated_declarations/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# TypeScript isolated declarations - -When the `isolated_declarations` compiler option is used, it ensures that a declarations file `.d.ts` can be produced from a single source file at a time. - -See https://devblogs.microsoft.com/typescript/announcing-typescript-5-5/#isolated-declarations -in particular, the section "Use-case: Parallel Declaration Emit and Parallel Checking" -which this example is based on. - -In this example, both `backend` and `frontend` packages depend on `core`, however they could be type-checked in parallel -before `core` has produced any declarationEmit. - -This doesn't make anything faster yet. -Follow https://github.com/aspect-build/rules_ts/issues/374 for updates on performance improvements to the Bazel action graph. diff --git a/examples/isolated_declarations/backend/BUILD.bazel b/examples/isolated_declarations/backend/BUILD.bazel index 1d5ff99d..f494f7e3 100644 --- a/examples/isolated_declarations/backend/BUILD.bazel +++ b/examples/isolated_declarations/backend/BUILD.bazel @@ -4,6 +4,7 @@ load("@aspect_rules_ts//ts:defs.bzl", "ts_project") ts_project( name = "backend", declaration = True, + isolated_declarations = True, tsconfig = "//examples/isolated_declarations:tsconfig", deps = ["//examples/isolated_declarations/core"], ) diff --git a/examples/isolated_declarations/core/BUILD.bazel b/examples/isolated_declarations/core/BUILD.bazel index b338be5b..e56cb348 100644 --- a/examples/isolated_declarations/core/BUILD.bazel +++ b/examples/isolated_declarations/core/BUILD.bazel @@ -3,6 +3,7 @@ load("@aspect_rules_ts//ts:defs.bzl", "ts_project") ts_project( name = "core", declaration = True, + isolated_declarations = True, tsconfig = "//examples/isolated_declarations:tsconfig", visibility = ["//examples/isolated_declarations:__subpackages__"], ) diff --git a/examples/isolated_declarations/core/index.ts b/examples/isolated_declarations/core/index.ts index 5b2b2b36..7870ee3c 100644 --- a/examples/isolated_declarations/core/index.ts +++ b/examples/isolated_declarations/core/index.ts @@ -12,3 +12,9 @@ type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( 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_declarations/frontend/BUILD.bazel b/examples/isolated_declarations/frontend/BUILD.bazel index 0bfcc5d7..3eb38cd5 100644 --- a/examples/isolated_declarations/frontend/BUILD.bazel +++ b/examples/isolated_declarations/frontend/BUILD.bazel @@ -5,6 +5,7 @@ ts_project( tsconfig = { "compilerOptions": { "declaration": True, + "isolatedDeclarations": True, }, }, deps = ["//examples/isolated_declarations/core"], diff --git a/examples/isolated_declarations/frontend/index.ts b/examples/isolated_declarations/frontend/index.ts index 92eb0e08..4f68e360 100644 --- a/examples/isolated_declarations/frontend/index.ts +++ b/examples/isolated_declarations/frontend/index.ts @@ -1,4 +1,5 @@ import type { IntersectionType } from '../core' +import { MyIntersectingValue } from '../core' // Example object of IntersectionType const myObject: IntersectionType = { @@ -7,4 +8,6 @@ const myObject: IntersectionType = { c: true, } -console.log(myObject) +const otherObject = MyIntersectingValue + +console.log(myObject, otherObject, myObject === otherObject) diff --git a/ts/defs.bzl b/ts/defs.bzl index aa53f75c..62fc8ed1 100644 --- a/ts/defs.bzl +++ b/ts/defs.bzl @@ -40,6 +40,7 @@ def ts_project( assets = [], extends = None, allow_js = False, + isolated_declarations = None, declaration = False, source_map = False, declaration_map = False, @@ -152,6 +153,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_declarations: Whether to enforce that declaration output (.d.ts file) can be produced for a + single source file at a time. Requires some additional explicit types on exported symbols. + See 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 @@ -280,6 +285,14 @@ def ts_project( allow_js = compiler_options.setdefault("allowJs", allow_js) if resolve_json_module != None: resolve_json_module = compiler_options.setdefault("resolveJsonModule", resolve_json_module) + if isolated_declarations != None: + isolated_declarations = compiler_options.setdefault("isolatedDeclarations", isolated_declarations) + + # Options not set by default due to tsc version compatibility + if "isolatedDeclarations" in compiler_options.keys(): + isolated_declarations = compiler_options.get("isolatedDeclarations") + elif isolated_declarations != None: + compiler_options["isolatedDeclarations"] = isolated_declarations # These options are always passed on the tsc command line so don't include them # in the tsconfig. At best they're redundant, but at worst we'll have a conflict @@ -321,8 +334,9 @@ def ts_project( else: # To stitch together a tree of ts_project where transpiler is a separate rule, # we have to produce a few targets - tsc_target_name = "%s_typings" % name + tsc_target_name = "%s_tsc" % name transpile_target_name = "%s_transpile" % name + typings_target_name = "%s_typings" % name typecheck_target_name = "%s_typecheck" % name test_target_name = "%s_typecheck_test" % name @@ -342,14 +356,31 @@ def ts_project( else: fail("transpiler attribute should be a rule/macro or a skylib partial. Got " + type(transpiler)) - # Users should build this target to get a failed build when typechecking fails - native.filegroup( - name = typecheck_target_name, - srcs = [tsc_target_name], - # This causes the types to be produced, which in turn triggers the tsc action to typecheck - output_group = "types", - **common_kwargs - ) + if isolated_declarations: + # Users should build this target to get a failed build when typechecking fails + native.filegroup( + name = typecheck_target_name, + srcs = [tsc_target_name], + output_group = "typecheck", + **common_kwargs + ) + + native.filegroup( + name = typings_target_name, + srcs = [tsc_target_name], + # This causes the types to be produced, which in turn triggers the tsc action to typecheck + output_group = "types", + **common_kwargs + ) + else: + # Users should build this target to get a failed build when typechecking fails + native.filegroup( + name = typecheck_target_name, + srcs = [tsc_target_name], + # This causes the types to be produced, which in turn triggers the tsc action to typecheck + output_group = "types", + **common_kwargs + ) # Ensures the typecheck target gets built under `bazel test --build_tests_only` build_test( @@ -389,6 +420,7 @@ def ts_project( incremental = incremental, preserve_jsx = preserve_jsx, composite = composite, + isolated_declarations = isolated_declarations, 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 de720e58..33244dce 100644 --- a/ts/private/ts_lib.bzl +++ b/ts/private/ts_lib.bzl @@ -108,6 +108,9 @@ COMPILER_OPTION_ATTRS = { "composite": attr.bool( doc = "https://www.typescriptlang.org/tsconfig#composite", ), + "isolated_declarations": attr.bool( + doc = "https://www.typescriptlang.org/tsconfig/#isolatedDeclarations", + ), "declaration": attr.bool( doc = "https://www.typescriptlang.org/tsconfig#declaration", ), diff --git a/ts/private/ts_project.bzl b/ts/private/ts_project.bzl index 7437c4ef..4b577366 100644 --- a/ts/private/ts_project.bzl +++ b/ts/private/ts_project.bzl @@ -236,40 +236,79 @@ This is an error because Bazel does not run actions unless their outputs are nee inputs_depset = depset() if len(outputs) > 0: transitive_inputs.append(_gather_types_from_js_infos(srcs_tsconfig_deps)) + copied_srcs = copy_files_to_bin_actions(ctx, inputs) + inputs_depset = depset(copied_srcs, transitive = transitive_inputs) + + # Make sure the user has acknowledged that transpiling is slow + if ctx.attr.transpile == -1 and not options.default_to_tsc_transpiler: + fail(transpiler_selection_required) + + if ctx.attr.isolated_declarations: + ctx.actions.run( + executable = executable, + inputs = depset(copied_srcs), + # See discussion in https://github.com/microsoft/TypeScript/issues/58863 + # TODO: add --noLib after https://github.com/microsoft/TypeScript/pull/58867 + arguments = [arguments, "--noCheck", "--skipLibCheck", "--noResolve"], + outputs = outputs, + mnemonic = "TsProjectEmit", + execution_requirements = execution_requirements, + resource_set = resource_set(ctx.attr), + progress_message = "Transpiling TypeScript project %s [tsc -p %s --noCheck]" % ( + ctx.label, + tsconfig_path, + ), + env = { + "BAZEL_BINDIR": ctx.bin_dir.path, + }, + ) - inputs_depset = depset( - copy_files_to_bin_actions(ctx, inputs), - transitive = transitive_inputs, - ) - - if ctx.attr.transpile != 0 and not ctx.attr.emit_declaration_only: - # Make sure the user has acknowledged that transpiling is slow - if ctx.attr.transpile == -1 and not options.default_to_tsc_transpiler: - fail(transpiler_selection_required) - if ctx.attr.declaration: - verb = "Transpiling & type-checking" - else: - verb = "Transpiling" + typecheck_marker = ctx.actions.declare_file(ctx.label.name + ".typecheck_ok") + validation_outs.append(typecheck_marker) + + ctx.actions.run_shell( + command = """{} $@ && echo "" > {} """.format(executable.path, typecheck_marker.path), + tools = [executable], + inputs = inputs_depset, + arguments = [arguments, "--noEmit"], + outputs = [typecheck_marker], + mnemonic = "TsProjectCheck", + execution_requirements = execution_requirements, + resource_set = resource_set(ctx.attr), + progress_message = "Type-checking TypeScript project %s [tsc -p %s --noEmit]" % ( + ctx.label, + tsconfig_path, + ), + env = { + "BAZEL_BINDIR": ctx.bin_dir.path, + }, + ) else: - verb = "Type-checking" - - ctx.actions.run( - executable = executable, - inputs = inputs_depset, - arguments = [arguments], - outputs = outputs, - mnemonic = "TsProject", - execution_requirements = execution_requirements, - resource_set = resource_set(ctx.attr), - progress_message = "%s TypeScript project %s [tsc -p %s]" % ( - verb, - ctx.label, - tsconfig_path, - ), - env = { - "BAZEL_BINDIR": ctx.bin_dir.path, - }, - ) + if ctx.attr.transpile != 0 and not ctx.attr.emit_declaration_only: + if ctx.attr.declaration: + verb = "Transpiling & type-checking" + else: + verb = "Transpiling" + else: + verb = "Type-checking" + + ctx.actions.run( + executable = executable, + inputs = inputs_depset, + arguments = [arguments], + outputs = outputs, + mnemonic = "TsProject", + execution_requirements = execution_requirements, + resource_set = resource_set(ctx.attr), + progress_message = "%s TypeScript project %s [tsc -p %s]" % ( + verb, + ctx.label, + tsconfig_path, + ), + env = { + "BAZEL_BINDIR": ctx.bin_dir.path, + }, + ) transitive_sources = js_lib_helpers.gather_transitive_sources(output_sources, srcs_tsconfig_deps) diff --git a/ts/private/ts_project_options_validator.js b/ts/private/ts_project_options_validator.js index 336e0047..b84fd82a 100755 --- a/ts/private/ts_project_options_validator.js +++ b/ts/private/ts_project_options_validator.js @@ -170,6 +170,7 @@ function main(_a) { check('declaration') check('incremental') check('tsBuildInfoFile', 'ts_build_info_file') + check('isolatedDeclarations', 'isolated_declarations') 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 09183601..fd4cc27a 100644 --- a/ts/private/ts_validate_options.bzl +++ b/ts/private/ts_validate_options.bzl @@ -37,6 +37,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_declarations = ctx.attr.isolated_declarations, ) arguments.add_all([ to_output_relative_path(tsconfig),