From d4f7750c6f0827afb86c541f3a646eb44b68fe69 Mon Sep 17 00:00:00 2001 From: Alex Eagle Date: Sun, 15 Mar 2020 22:06:51 -0700 Subject: [PATCH 1/6] feat(typescript): add ts_project rule This is a very thin layer on top of vanilla tsc --- examples/BUILD.bazel | 3 + examples/react_webpack/BUILD.bazel | 32 +-- examples/react_webpack/WORKSPACE | 4 + examples/react_webpack/package.json | 1 + examples/react_webpack/tsconfig.json | 6 + packages/typescript/src/index.bzl | 2 + packages/typescript/src/index.docs.bzl | 2 + packages/typescript/src/internal/BUILD.bazel | 1 + .../typescript/src/internal/ts_project.bzl | 229 ++++++++++++++++++ packages/typescript/test/ts_project/BUILD | 1 + .../typescript/test/ts_project/a/BUILD.bazel | 9 + packages/typescript/test/ts_project/a/a.ts | 1 + .../test/ts_project/a/tsconfig.json | 20 ++ .../typescript/test/ts_project/b/BUILD.bazel | 28 +++ .../typescript/test/ts_project/b/b.spec.ts | 10 + packages/typescript/test/ts_project/b/b.ts | 5 + .../test/ts_project/b/tsconfig-test.json | 7 + .../test/ts_project/b/tsconfig.json | 11 + .../typescript/test/ts_project/c/BUILD.bazel | 8 + packages/typescript/test/ts_project/c/c.ts | 5 + .../test/ts_project/c/tsconfig.json | 6 + .../test/ts_project/tsconfig-base.json | 22 ++ 22 files changed, 398 insertions(+), 15 deletions(-) create mode 100644 examples/react_webpack/tsconfig.json create mode 100644 packages/typescript/src/internal/ts_project.bzl create mode 100644 packages/typescript/test/ts_project/BUILD create mode 100644 packages/typescript/test/ts_project/a/BUILD.bazel create mode 100644 packages/typescript/test/ts_project/a/a.ts create mode 100644 packages/typescript/test/ts_project/a/tsconfig.json create mode 100644 packages/typescript/test/ts_project/b/BUILD.bazel create mode 100644 packages/typescript/test/ts_project/b/b.spec.ts create mode 100644 packages/typescript/test/ts_project/b/b.ts create mode 100644 packages/typescript/test/ts_project/b/tsconfig-test.json create mode 100644 packages/typescript/test/ts_project/b/tsconfig.json create mode 100644 packages/typescript/test/ts_project/c/BUILD.bazel create mode 100644 packages/typescript/test/ts_project/c/c.ts create mode 100644 packages/typescript/test/ts_project/c/tsconfig.json create mode 100644 packages/typescript/test/ts_project/tsconfig-base.json diff --git a/examples/BUILD.bazel b/examples/BUILD.bazel index 1b31481ce7..b5228ea44e 100644 --- a/examples/BUILD.bazel +++ b/examples/BUILD.bazel @@ -64,6 +64,9 @@ example_integration_test( name = "examples_react_webpack", # TODO: add some tests in the example bazel_commands = ["build ..."], + npm_packages = { + "//packages/typescript:npm_package": "@bazel/typescript", + }, # TODO(alexeagle): somehow this is broken by the new node-patches based node_patches script # ERROR: D:/temp/tmp-6900sejcsrcttpdb/BUILD.bazel:37:1: output 'app.bundle.js' was not created tags = ["no-bazelci-windows"], diff --git a/examples/react_webpack/BUILD.bazel b/examples/react_webpack/BUILD.bazel index ffeb3a3891..86bb368625 100644 --- a/examples/react_webpack/BUILD.bazel +++ b/examples/react_webpack/BUILD.bazel @@ -1,7 +1,7 @@ load("@npm//http-server:index.bzl", "http_server") load("@npm//sass:index.bzl", "sass") -load("@npm//typescript:index.bzl", "tsc") load("@npm//webpack-cli:index.bzl", webpack = "webpack_cli") +load("@npm_bazel_typescript//:index.bzl", "ts_project") sass( name = "styles", @@ -13,27 +13,29 @@ sass( data = ["styles.scss"], ) -tsc( - name = "compile", - outs = ["index.js"], - args = [ - "$(execpath index.tsx)", - "$(execpath types.d.ts)", - "--outDir", - "$(RULEDIR)", - "--lib", - "es2015,dom", - "--jsx", - "react", - ], - data = [ +ts_project( + name = "tsconfig", + srcs = [ "index.tsx", "types.d.ts", + ], + # The tsconfig.json file doesn't enable "declaration" or "composite" + # so tsc won't produce any .d.ts outputs. We need to tell Bazel not + # to expect those outputs being produced. + declaration = False, + deps = [ "@npm//@types", "@npm//csstype", ], ) +# If you don't want to write //:tsconfig as the label for the TypeScript compilation, +# use an alias like this, so you can write //:compile_ts instead. +alias( + name = "compile_ts", + actual = "tsconfig", +) + webpack( name = "bundle", outs = ["app.bundle.js"], diff --git a/examples/react_webpack/WORKSPACE b/examples/react_webpack/WORKSPACE index 0022821781..a9086fba54 100644 --- a/examples/react_webpack/WORKSPACE +++ b/examples/react_webpack/WORKSPACE @@ -19,3 +19,7 @@ yarn_install( package_json = "//:package.json", yarn_lock = "//:yarn.lock", ) + +load("@npm//:install_bazel_dependencies.bzl", "install_bazel_dependencies") + +install_bazel_dependencies() diff --git a/examples/react_webpack/package.json b/examples/react_webpack/package.json index 97710aace2..add1df42c7 100644 --- a/examples/react_webpack/package.json +++ b/examples/react_webpack/package.json @@ -4,6 +4,7 @@ "@bazel/bazelisk": "^1.3.0", "@bazel/buildifier": "^0.29.0", "@bazel/ibazel": "^0.12.2", + "@bazel/typescript": "^1.4.1", "@types/react": "^16.9.5", "@types/react-dom": "^16.9.1", "css-loader": "^3.2.0", diff --git a/examples/react_webpack/tsconfig.json b/examples/react_webpack/tsconfig.json new file mode 100644 index 0000000000..0ece51d45f --- /dev/null +++ b/examples/react_webpack/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "jsx": "react", + "lib": ["ES2015", "DOM"] + } +} \ No newline at end of file diff --git a/packages/typescript/src/index.bzl b/packages/typescript/src/index.bzl index d1b620ee73..e45c0c5cb0 100644 --- a/packages/typescript/src/index.bzl +++ b/packages/typescript/src/index.bzl @@ -19,6 +19,7 @@ Users should not load files under "/internal" load("//internal:build_defs.bzl", _ts_library = "ts_library_macro") load("//internal:ts_config.bzl", _ts_config = "ts_config") +load("//internal:ts_project.bzl", _ts_project = "ts_project_macro") load("//internal:ts_repositories.bzl", _ts_setup_workspace = "ts_setup_workspace") load("//internal/devserver:ts_devserver.bzl", _ts_devserver = "ts_devserver_macro") @@ -26,4 +27,5 @@ ts_setup_workspace = _ts_setup_workspace ts_library = _ts_library ts_config = _ts_config ts_devserver = _ts_devserver +ts_project = _ts_project # If adding rules here also add to index.docs.bzl diff --git a/packages/typescript/src/index.docs.bzl b/packages/typescript/src/index.docs.bzl index a44d119049..2ec770c3d7 100644 --- a/packages/typescript/src/index.docs.bzl +++ b/packages/typescript/src/index.docs.bzl @@ -20,12 +20,14 @@ So this is a copy of index.bzl with macro indirection removed. load("//internal:build_defs.bzl", _ts_library = "ts_library") load("//internal:ts_config.bzl", _ts_config = "ts_config") +load("//internal:ts_project.bzl", _ts_project = "ts_project_macro") load("//internal:ts_repositories.bzl", _ts_setup_workspace = "ts_setup_workspace") load("//internal/devserver:ts_devserver.bzl", _ts_devserver = "ts_devserver") ts_setup_workspace = _ts_setup_workspace ts_library = _ts_library ts_config = _ts_config +ts_project = _ts_project ts_devserver = _ts_devserver # DO NOT ADD MORE rules here unless they appear in the generated docsite. # Run yarn stardoc to re-generate the docsite. diff --git a/packages/typescript/src/internal/BUILD.bazel b/packages/typescript/src/internal/BUILD.bazel index daf761f170..839caa36a6 100644 --- a/packages/typescript/src/internal/BUILD.bazel +++ b/packages/typescript/src/internal/BUILD.bazel @@ -47,6 +47,7 @@ filegroup( srcs = [ "build_defs.bzl", "ts_config.bzl", + "ts_project.bzl", "ts_repositories.bzl", "//internal/devserver:package_contents", ], diff --git a/packages/typescript/src/internal/ts_project.bzl b/packages/typescript/src/internal/ts_project.bzl new file mode 100644 index 0000000000..0561c4a45f --- /dev/null +++ b/packages/typescript/src/internal/ts_project.bzl @@ -0,0 +1,229 @@ +"ts_project rule" + +load("@build_bazel_rules_nodejs//:providers.bzl", "DeclarationInfo", "NpmPackageInfo", "run_node") + +_ATTRS = { + # NB: no restriction on extensions here, because tsc sometimes adds type-check support + # for more file kinds (like require('some.json')) and also + # if you swap out the `compiler` attribute (like with ngtsc) + # that compiler might allow more sources than tsc does. + "srcs": attr.label_list(allow_files = True, mandatory = True), + "extends": attr.label_list(allow_files = [".json"]), + "tsc": attr.label(default = Label("@npm//typescript/bin:tsc"), executable = True, cfg = "host"), + "tsconfig": attr.label(mandatory = True, allow_single_file = [".json"]), + "deps": attr.label_list(providers = [DeclarationInfo]), +} + +# tsc knows how to produce the following kinds of output files. +# NB: the macro `ts_project_macro` will set these outputs based on user +# telling us which settings are enabled in the tsconfig for this project. +_OUTPUTS = { + "js_outs": attr.output_list(), + "map_outs": attr.output_list(), + "typing_maps_outs": attr.output_list(), + "typings_outs": attr.output_list(), +} + +_TsConfigInfo = provider( + doc = """Passes tsconfig.json files to downstream compilations so that TypeScript can read them. + This is needed to support Project References""", + fields = { + "tsconfigs": "depset of tsconfig.json files", + }, +) + +def _ts_project_impl(ctx): + arguments = ctx.actions.args() + arguments.add_all([ + "-p", + ctx.file.tsconfig.short_path, + "--outDir", + "/".join([ctx.bin_dir.path, ctx.label.package]), + ]) + if len(ctx.outputs.typings_outs) > 0: + arguments.add_all([ + "--declarationDir", + "/".join([ctx.bin_dir.path, ctx.label.package]), + ]) + + # When users report problems, we can ask them to re-build with + # --define=VERBOSE_LOGS=1 + # so anything that's useful to diagnose rule failures belongs here + if "VERBOSE_LOGS" in ctx.var.keys(): + arguments.add_all([ + # What files were in the ts.Program + "--listFiles", + # Did tsc write all outputs to the place we expect to find them? + "--listEmittedFiles", + # Why did module resolution fail? + "--traceResolution", + # Why was the build slow? + "--diagnostics", + "--extendedDiagnostics", + ]) + + deps_depsets = [] + for dep in ctx.attr.deps: + if _TsConfigInfo in dep: + deps_depsets.append(dep[_TsConfigInfo].tsconfigs) + if NpmPackageInfo in dep: + # TODO: we could maybe filter these to be tsconfig.json or *.d.ts only + # we don't expect tsc wants to read any other files from npm packages. + deps_depsets.append(dep[NpmPackageInfo].sources) + elif DeclarationInfo in dep: + deps_depsets.append(dep[DeclarationInfo].transitive_declarations) + + inputs = ctx.files.srcs + depset(transitive = deps_depsets).to_list() + [ctx.file.tsconfig] + if ctx.attr.extends: + inputs.extend(ctx.files.extends) + outputs = ctx.outputs.js_outs + ctx.outputs.map_outs + ctx.outputs.typings_outs + ctx.outputs.typing_maps_outs + + if len(outputs) == 0: + return [] + + run_node( + ctx, + inputs = inputs, + arguments = [arguments], + outputs = outputs, + executable = "tsc", + progress_message = "Compiling TypeScript project %s" % ctx.file.tsconfig.short_path, + ) + + runtime_files = depset(ctx.outputs.js_outs + ctx.outputs.map_outs) + typings_files = ctx.outputs.typings_outs + [s for s in ctx.files.srcs if s.path.endswith(".d.ts")] + + return [ + DeclarationInfo( + declarations = depset(typings_files), + transitive_declarations = depset(typings_files, transitive = [ + dep[DeclarationInfo].transitive_declarations + for dep in ctx.attr.deps + ]), + ), + DefaultInfo( + files = runtime_files, + runfiles = ctx.runfiles( + transitive_files = runtime_files, + collect_default = True, + ), + ), + _TsConfigInfo(tsconfigs = depset([ctx.file.tsconfig], transitive = [ + dep[_TsConfigInfo].tsconfigs + for dep in ctx.attr.deps + if _TsConfigInfo in dep + ])), + ] + +ts_project = rule( + implementation = _ts_project_impl, + attrs = dict(_ATTRS, **_OUTPUTS), +) + +def _out_paths(srcs, ext): + return [f[:f.rindex(".")] + ext for f in srcs if not f.endswith(".d.ts")] + +def ts_project_macro( + name = "tsconfig", + srcs = None, + deps = [], + extends = None, + declaration = True, + source_map = False, + declaration_map = False, + emit_declaration_only = False, + **kwargs): + """Compiles one TypeScript project using `tsc -p` + + Unlike `ts_library`, this rule is the thinnest possible layer of Bazel awareness on top + of the TypeScript compiler. It shifts the burden of configuring TypeScript into the tsconfig.json file. + TODO(alexeagle): update https://github.com/bazelbuild/rules_nodejs/blob/master/docs/TypeScript.md#alternatives + # to describe the trade-offs between the two rules. + + Any code that works with `tsc` should work with `ts_project` with a few caveats: + + - Bazel requires that the `outDir` (and `declarationDir`) be set to + `bazel-out/[arch]/bin/path/to/package` + so we override whatever settings appear in your tsconfig. + - Bazel expects that each output is produced by a single rule. + Thus if you have two `ts_project` rules with overlapping sources (the same .ts file + appears in more than one) then you get an error if you try to build both together. + Worse, if you build them separately then the output directory will contain whichever + one you happened to build most recently. This is highly discouraged. + + > Note, in order for TypeScript to find referenced projects in the bazel-out folder, + > we recommend that the base tsconfig contain a rootDirs section that includes all + > possible locations they may appear. + > + > We hope this will not be needed in some future release of TypeScript. + > Follow https://github.com/microsoft/TypeScript/issues/37257 for more info. + > + > For example, if the base tsconfig file relative to the workspace root is + > `path/to/tsconfig.json` then you should configure like: + > + > ``` + > "compilerOptions": { + > "rootDirs": [ + > ".", + > "../../bazel-out/darwin-fastbuild/bin/path/to", + > "../../bazel-out/k8-fastbuild/bin/path/to", + > "../../bazel-out/x64_windows-fastbuild/bin/path/to", + > "../../bazel-out/darwin-dbg/bin/path/to", + > "../../bazel-out/k8-dbg/bin/path/to", + > "../../bazel-out/x64_windows-dbg/bin/path/to", + > ] + > } + > ``` + + This rule requires that the name match the tsconfig file. + The reason is that it makes BUILD file generation (autoconfig) much simpler. + If you want to use a different label, you can use a Bazel `alias` rule to give it an additional name. + + If your dependencies are not installed into a workspace called `npm`, or if you want to + use a custom TypeScript compiler binary, you can pass `tsc = "@some_wksp//path/to:tsc_bin"` + to this rule to override the compiler. + + Args: + name: The basename (no `.json` extension) of the tsconfig file that should be compiled. + srcs: List of labels of TypeScript source files to be provided to the compiler. + + If absent, defaults to `**/*.ts` (all TypeScript files in the package). + deps: List of labels of other rules that produce TypeScript typings (.d.ts files) + extends: List of labels of tsconfig file(s) referenced in `extends` section of tsconfig. + + Must include any tsconfig files "chained" by extends clauses. + declaration: if the `declaration` or `composite` bit are set in the tsconfig. + Instructs Bazel to expect a `.d.ts` output for each `.ts` source. + source_map: if the `sourceMap` bit is set in the tsconfig. + Instructs Bazel to expect a `.js.map` output for each `.ts` source. + declaration_map: if the `declarationMap` bit is set in the tsconfig. + Instructs Bazel to expect a `.d.ts.map` output for each `.ts` source. + emit_declaration_only: if the `emitDeclarationOnly` bit is set in the tsconfig. + Instructs Bazel *not* to expect `.js` outputs for `.ts` sources. + """ + + if srcs == None: + srcs = native.glob(["**/*.ts"]) + + # Bazel's `name` property is arbitrarily controlled by the user. + # But here we constrain it to match the tsconfig file. + # The reason for doing this is so that BUILD file generation can be trivial. + # If you know the path of a referenced tsconfig file, you know what label to use + # to include it in the deps of another rule. + # If we allowed users to control it, then BUILD file generation would need + # heuristic semantics to do bazel query and try to figure out which label to use + # to satisfy a project references dependency. + tsconfig = name + ".json" + + ts_project( + name = name, + srcs = srcs, + deps = deps, + tsconfig = tsconfig, + extends = extends, + js_outs = _out_paths(srcs, ".js") if not emit_declaration_only else [], + map_outs = _out_paths(srcs, ".js.map") if source_map and not emit_declaration_only else [], + typings_outs = _out_paths(srcs, ".d.ts") if declaration else [], + typing_maps_outs = _out_paths(srcs, ".d.ts.map") if declaration_map else [], + **kwargs + ) diff --git a/packages/typescript/test/ts_project/BUILD b/packages/typescript/test/ts_project/BUILD new file mode 100644 index 0000000000..96b6172fd3 --- /dev/null +++ b/packages/typescript/test/ts_project/BUILD @@ -0,0 +1 @@ +exports_files(["tsconfig-base.json"]) diff --git a/packages/typescript/test/ts_project/a/BUILD.bazel b/packages/typescript/test/ts_project/a/BUILD.bazel new file mode 100644 index 0000000000..0b122a74ae --- /dev/null +++ b/packages/typescript/test/ts_project/a/BUILD.bazel @@ -0,0 +1,9 @@ +load("@npm_bazel_typescript//:index.bzl", "ts_project") + +ts_project( + name = "tsconfig", # This will use ./tsconfig.json + srcs = [":a.ts"], + extends = ["//packages/typescript/test/ts_project:tsconfig-base.json"], + source_map = True, + visibility = ["//packages/typescript/test:__subpackages__"], +) diff --git a/packages/typescript/test/ts_project/a/a.ts b/packages/typescript/test/ts_project/a/a.ts new file mode 100644 index 0000000000..a668b7e336 --- /dev/null +++ b/packages/typescript/test/ts_project/a/a.ts @@ -0,0 +1 @@ +export const a: string = 'hello'; diff --git a/packages/typescript/test/ts_project/a/tsconfig.json b/packages/typescript/test/ts_project/a/tsconfig.json new file mode 100644 index 0000000000..8fa4bcb5e0 --- /dev/null +++ b/packages/typescript/test/ts_project/a/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../tsconfig-base.json", + "compilerOptions": { + "sourceMap": true, + // WORKAROUND https://github.com/microsoft/TypeScript/issues/37378 + // When running the action with standalone strategy (no sandbox), + // tsc will see the .ts source files + // from our dependencies. It then tries to do an up-to-date check on the outputs. + // We are forced to have outDir in the tsconfig in this case (on windows) + // so that tsc can know where to look for the .d.ts outputs + // Error looks like + // (05:54:49) ERROR: C:/b/bk-windows-b4qr/bazel/rules-nodejs-nodejs/packages/typescript/test/ts_project/b/BUILD.bazel:6:1: Compiling TypeScript project packages/typescript/test/ts_project/b/tsconfig.json failed (Exit 2): tsc.bat failed: error executing command + // cd C:/b/uuxnwop3/execroot/build_bazel_rules_nodejs + // SET COMPILATION_MODE=fastbuild + // bazel-out/host/bin/external/npm/typescript/bin/tsc.bat -p packages/typescript/test/ts_project/b/tsconfig.json --outDir bazel-out/x64_windows-fastbuild/bin/packages/typescript/test/ts_project/b --declarationDir bazel-out/x64_windows-fastbuild/bin/packages/typescript/test/ts_project/b --bazel_node_modules_manifest=bazel-out/x64_windows-fastbuild/bin/packages/typescript/test/ts_project/b/_tsconfig.module_mappings.json --nobazel_patch_module_resolver + // Execution platform: @local_config_platform//:host + // packages/typescript/test/ts_project/b/b.ts(1,17): error TS6305: Output file 'C:/b/uuxnwop3/execroot/build_bazel_rules_nodejs/packages/typescript/test/ts_project/a/a.d.ts' has not been built from source file 'C:/b/uuxnwop3/execroot/build_bazel_rules_nodejs/packages/typescript/test/ts_project/a/a.ts'. + "outDir": "../../../../../bazel-out/x64_windows-fastbuild/bin/packages/typescript/test/ts_project/a", + } +} \ No newline at end of file diff --git a/packages/typescript/test/ts_project/b/BUILD.bazel b/packages/typescript/test/ts_project/b/BUILD.bazel new file mode 100644 index 0000000000..c32f3e286a --- /dev/null +++ b/packages/typescript/test/ts_project/b/BUILD.bazel @@ -0,0 +1,28 @@ +load("@npm_bazel_jasmine//:index.from_src.bzl", "jasmine_node_test") +load("@npm_bazel_typescript//:index.bzl", "ts_project") + +package(default_visibility = ["//packages/typescript/test:__subpackages__"]) + +ts_project( + name = "tsconfig", # This will use ./tsconfig.json + srcs = [":b.ts"], + extends = ["//packages/typescript/test/ts_project:tsconfig-base.json"], + deps = ["//packages/typescript/test/ts_project/a:tsconfig"], +) + +ts_project( + name = "tsconfig-test", # This will use ./tsconfig-test.json + testonly = True, + srcs = [":b.spec.ts"], + extends = ["//packages/typescript/test/ts_project:tsconfig-base.json"], + deps = [ + ":tsconfig", + "@npm//@types/jasmine", + ], +) + +jasmine_node_test( + name = "test", + srcs = ["b.spec.js"], + data = [":tsconfig"], +) diff --git a/packages/typescript/test/ts_project/b/b.spec.ts b/packages/typescript/test/ts_project/b/b.spec.ts new file mode 100644 index 0000000000..e72c0a1c3d --- /dev/null +++ b/packages/typescript/test/ts_project/b/b.spec.ts @@ -0,0 +1,10 @@ +import {sayHello} from './b'; + +describe('b', () => { + it('should say hello', () => { + let captured: string = ''; + console.log = (s: string) => captured = s; + sayHello(' world'); + expect(captured).toBe('hello world'); + }); +}); diff --git a/packages/typescript/test/ts_project/b/b.ts b/packages/typescript/test/ts_project/b/b.ts new file mode 100644 index 0000000000..1879b29fa3 --- /dev/null +++ b/packages/typescript/test/ts_project/b/b.ts @@ -0,0 +1,5 @@ +import {a} from '../a/a'; + +export function sayHello(f: string) { + console.log(a + f); +} diff --git a/packages/typescript/test/ts_project/b/tsconfig-test.json b/packages/typescript/test/ts_project/b/tsconfig-test.json new file mode 100644 index 0000000000..288ba2408c --- /dev/null +++ b/packages/typescript/test/ts_project/b/tsconfig-test.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig-base.json", + "references": [ + {"path": "./"} + ], + "include": ["*.spec.ts"] +} diff --git a/packages/typescript/test/ts_project/b/tsconfig.json b/packages/typescript/test/ts_project/b/tsconfig.json new file mode 100644 index 0000000000..4746a3c94f --- /dev/null +++ b/packages/typescript/test/ts_project/b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig-base.json", + "references": [ + {"path": "../a"} + ], + "compilerOptions": { + // WORKAROUND - See comment in ../a/tsconfig.json + "outDir": "../../../../../bazel-out/x64_windows-fastbuild/bin/packages/typescript/test/ts_project/b" + }, + "exclude": ["*.spec.ts"] +} diff --git a/packages/typescript/test/ts_project/c/BUILD.bazel b/packages/typescript/test/ts_project/c/BUILD.bazel new file mode 100644 index 0000000000..29d38649f1 --- /dev/null +++ b/packages/typescript/test/ts_project/c/BUILD.bazel @@ -0,0 +1,8 @@ +load("@npm_bazel_typescript//:index.bzl", "ts_project") + +ts_project( + name = "tsconfig", # This will use ./tsconfig.json + srcs = [":c.ts"], + extends = ["//packages/typescript/test/ts_project:tsconfig-base.json"], + deps = ["//packages/typescript/test/ts_project/b:tsconfig"], +) diff --git a/packages/typescript/test/ts_project/c/c.ts b/packages/typescript/test/ts_project/c/c.ts new file mode 100644 index 0000000000..5c4b70280b --- /dev/null +++ b/packages/typescript/test/ts_project/c/c.ts @@ -0,0 +1,5 @@ +import {a} from '../a/a'; // SHOULD FAIL HERE per https://github.com/microsoft/TypeScript/issues/36743 +import {sayHello} from '../b/b'; + +sayHello('world'); +console.error(a); diff --git a/packages/typescript/test/ts_project/c/tsconfig.json b/packages/typescript/test/ts_project/c/tsconfig.json new file mode 100644 index 0000000000..0c7bf6c73c --- /dev/null +++ b/packages/typescript/test/ts_project/c/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig-base.json", + "references": [ + {"path": "../b/tsconfig.json"} + ] +} \ No newline at end of file diff --git a/packages/typescript/test/ts_project/tsconfig-base.json b/packages/typescript/test/ts_project/tsconfig-base.json new file mode 100644 index 0000000000..11a249c71d --- /dev/null +++ b/packages/typescript/test/ts_project/tsconfig-base.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "strict": true, + "lib": [ + "ES2015", + "ES2016.Array.Include", + "DOM" + ], + "module": "commonjs", + "target": "ES2015", + "composite": true, + "rootDirs": [ + ".", + "../../../../bazel-out/darwin-fastbuild/bin/packages/typescript/test/ts_project", + "../../../../bazel-out/k8-fastbuild/bin/packages/typescript/test/ts_project", + "../../../../bazel-out/x64_windows-fastbuild/bin/packages/typescript/test/ts_project", + "../../../../bazel-out/darwin-dbg/bin/packages/typescript/test/ts_project", + "../../../../bazel-out/k8-dbg/bin/packages/typescript/test/ts_project", + "../../../../bazel-out/x64_windows-dbg/bin/packages/typescript/test/ts_project", + ], + } +} \ No newline at end of file From 71e77d9e0cf4d321768c84a8fa81ca19f3e1f65a Mon Sep 17 00:00:00 2001 From: Alex Eagle Date: Sun, 22 Mar 2020 19:37:43 -0700 Subject: [PATCH 2/6] fixup! feat(typescript): add ts_project rule --- .../typescript/src/internal/ts_project.bzl | 34 +++++++++---------- .../typescript/test/ts_project/a/BUILD.bazel | 2 -- .../typescript/test/ts_project/c/BUILD.bazel | 3 +- .../test/ts_project/c/tsconfig.json | 4 ++- 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/packages/typescript/src/internal/ts_project.bzl b/packages/typescript/src/internal/ts_project.bzl index 0561c4a45f..2cfed0a3d8 100644 --- a/packages/typescript/src/internal/ts_project.bzl +++ b/packages/typescript/src/internal/ts_project.bzl @@ -125,6 +125,7 @@ def _out_paths(srcs, ext): def ts_project_macro( name = "tsconfig", + tsconfig = None, srcs = None, deps = [], extends = None, @@ -132,13 +133,14 @@ def ts_project_macro( source_map = False, declaration_map = False, emit_declaration_only = False, + tsc = "@npm//typescript/bin:tsc", **kwargs): """Compiles one TypeScript project using `tsc -p` Unlike `ts_library`, this rule is the thinnest possible layer of Bazel awareness on top of the TypeScript compiler. It shifts the burden of configuring TypeScript into the tsconfig.json file. TODO(alexeagle): update https://github.com/bazelbuild/rules_nodejs/blob/master/docs/TypeScript.md#alternatives - # to describe the trade-offs between the two rules. + to describe the trade-offs between the two rules. Any code that works with `tsc` should work with `ts_project` with a few caveats: @@ -175,23 +177,25 @@ def ts_project_macro( > } > ``` - This rule requires that the name match the tsconfig file. - The reason is that it makes BUILD file generation (autoconfig) much simpler. - If you want to use a different label, you can use a Bazel `alias` rule to give it an additional name. + Args: + name: A name for the target. - If your dependencies are not installed into a workspace called `npm`, or if you want to - use a custom TypeScript compiler binary, you can pass `tsc = "@some_wksp//path/to:tsc_bin"` - to this rule to override the compiler. + We recommend you use the basename (no `.json` extension) of the tsconfig file that should be compiled. - Args: - name: The basename (no `.json` extension) of the tsconfig file that should be compiled. srcs: List of labels of TypeScript source files to be provided to the compiler. If absent, defaults to `**/*.ts` (all TypeScript files in the package). + deps: List of labels of other rules that produce TypeScript typings (.d.ts files) + + tsconfig: Label of the tsconfig.json file to use for the compilation. + + By default, we add `.json` to the `name` attribute. + extends: List of labels of tsconfig file(s) referenced in `extends` section of tsconfig. Must include any tsconfig files "chained" by extends clauses. + declaration: if the `declaration` or `composite` bit are set in the tsconfig. Instructs Bazel to expect a `.d.ts` output for each `.ts` source. source_map: if the `sourceMap` bit is set in the tsconfig. @@ -205,15 +209,8 @@ def ts_project_macro( if srcs == None: srcs = native.glob(["**/*.ts"]) - # Bazel's `name` property is arbitrarily controlled by the user. - # But here we constrain it to match the tsconfig file. - # The reason for doing this is so that BUILD file generation can be trivial. - # If you know the path of a referenced tsconfig file, you know what label to use - # to include it in the deps of another rule. - # If we allowed users to control it, then BUILD file generation would need - # heuristic semantics to do bazel query and try to figure out which label to use - # to satisfy a project references dependency. - tsconfig = name + ".json" + if tsconfig == None: + tsconfig = name + ".json" ts_project( name = name, @@ -225,5 +222,6 @@ def ts_project_macro( map_outs = _out_paths(srcs, ".js.map") if source_map and not emit_declaration_only else [], typings_outs = _out_paths(srcs, ".d.ts") if declaration else [], typing_maps_outs = _out_paths(srcs, ".d.ts.map") if declaration_map else [], + tsc = tsc, **kwargs ) diff --git a/packages/typescript/test/ts_project/a/BUILD.bazel b/packages/typescript/test/ts_project/a/BUILD.bazel index 0b122a74ae..1fe9a2ace4 100644 --- a/packages/typescript/test/ts_project/a/BUILD.bazel +++ b/packages/typescript/test/ts_project/a/BUILD.bazel @@ -1,8 +1,6 @@ load("@npm_bazel_typescript//:index.bzl", "ts_project") ts_project( - name = "tsconfig", # This will use ./tsconfig.json - srcs = [":a.ts"], extends = ["//packages/typescript/test/ts_project:tsconfig-base.json"], source_map = True, visibility = ["//packages/typescript/test:__subpackages__"], diff --git a/packages/typescript/test/ts_project/c/BUILD.bazel b/packages/typescript/test/ts_project/c/BUILD.bazel index 29d38649f1..599069cc78 100644 --- a/packages/typescript/test/ts_project/c/BUILD.bazel +++ b/packages/typescript/test/ts_project/c/BUILD.bazel @@ -1,8 +1,9 @@ load("@npm_bazel_typescript//:index.bzl", "ts_project") ts_project( - name = "tsconfig", # This will use ./tsconfig.json + name = "compile", srcs = [":c.ts"], extends = ["//packages/typescript/test/ts_project:tsconfig-base.json"], + tsconfig = "tsconfig.json", deps = ["//packages/typescript/test/ts_project/b:tsconfig"], ) diff --git a/packages/typescript/test/ts_project/c/tsconfig.json b/packages/typescript/test/ts_project/c/tsconfig.json index 0c7bf6c73c..90cd4e5d5e 100644 --- a/packages/typescript/test/ts_project/c/tsconfig.json +++ b/packages/typescript/test/ts_project/c/tsconfig.json @@ -2,5 +2,7 @@ "extends": "../tsconfig-base.json", "references": [ {"path": "../b/tsconfig.json"} - ] + ], + // NB: we don't need the compilerOptions.outDir Windows workaround here + // because there is no project that references this one. } \ No newline at end of file From fc00d38e4dc163e7f80359e778c8934cc7f50b20 Mon Sep 17 00:00:00 2001 From: Alex Eagle Date: Sun, 22 Mar 2020 21:28:29 -0700 Subject: [PATCH 3/6] fix(typescript): declare the .tsbuildinfo output this fixes a bug on Windows where Bazel deletes known outputs like .js but then tsc doesn't write the .js file again on next action because the tsbuildinfo file indicates it is up-to-date --- .../typescript/src/internal/ts_project.bzl | 19 ++++++++++++++++--- .../typescript/test/ts_project/a/BUILD.bazel | 1 + .../typescript/test/ts_project/b/BUILD.bazel | 2 ++ .../typescript/test/ts_project/c/BUILD.bazel | 1 + 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/typescript/src/internal/ts_project.bzl b/packages/typescript/src/internal/ts_project.bzl index 2cfed0a3d8..2a2229a392 100644 --- a/packages/typescript/src/internal/ts_project.bzl +++ b/packages/typescript/src/internal/ts_project.bzl @@ -18,6 +18,7 @@ _ATTRS = { # NB: the macro `ts_project_macro` will set these outputs based on user # telling us which settings are enabled in the tsconfig for this project. _OUTPUTS = { + "buildinfo_out": attr.output(), "js_outs": attr.output_list(), "map_outs": attr.output_list(), "typing_maps_outs": attr.output_list(), @@ -77,6 +78,8 @@ def _ts_project_impl(ctx): if ctx.attr.extends: inputs.extend(ctx.files.extends) outputs = ctx.outputs.js_outs + ctx.outputs.map_outs + ctx.outputs.typings_outs + ctx.outputs.typing_maps_outs + if ctx.outputs.buildinfo_out: + outputs.append(ctx.outputs.buildinfo_out) if len(outputs) == 0: return [] @@ -132,6 +135,7 @@ def ts_project_macro( declaration = True, source_map = False, declaration_map = False, + composite = False, emit_declaration_only = False, tsc = "@npm//typescript/bin:tsc", **kwargs): @@ -196,14 +200,22 @@ def ts_project_macro( Must include any tsconfig files "chained" by extends clauses. - declaration: if the `declaration` or `composite` bit are set in the tsconfig. + tsc: Label of the TypeScript compiler binary to run. + + Override this if your npm_install or yarn_install isn't named "npm" + For example, `tsc = "@my_deps//typescript/bin:tsc"` + Or you can pass a custom compiler binary instead. + + declaration: if the `declaration` bit is set in the tsconfig. Instructs Bazel to expect a `.d.ts` output for each `.ts` source. source_map: if the `sourceMap` bit is set in the tsconfig. Instructs Bazel to expect a `.js.map` output for each `.ts` source. declaration_map: if the `declarationMap` bit is set in the tsconfig. Instructs Bazel to expect a `.d.ts.map` output for each `.ts` source. + composite: if the `composite` bit is set in the tsconfig. + Instructs Bazel to expect a `.tsbuildinfo` output and a `.d.ts` output for each `.ts` source. emit_declaration_only: if the `emitDeclarationOnly` bit is set in the tsconfig. - Instructs Bazel *not* to expect `.js` outputs for `.ts` sources. + Instructs Bazel *not* to expect `.js` or `.js.map` outputs for `.ts` sources. """ if srcs == None: @@ -220,8 +232,9 @@ def ts_project_macro( extends = extends, js_outs = _out_paths(srcs, ".js") if not emit_declaration_only else [], map_outs = _out_paths(srcs, ".js.map") if source_map and not emit_declaration_only else [], - typings_outs = _out_paths(srcs, ".d.ts") if declaration else [], + typings_outs = _out_paths(srcs, ".d.ts") if declaration or composite else [], typing_maps_outs = _out_paths(srcs, ".d.ts.map") if declaration_map else [], + buildinfo_out = tsconfig[:-5] + ".tsbuildinfo" if composite else None, tsc = tsc, **kwargs ) diff --git a/packages/typescript/test/ts_project/a/BUILD.bazel b/packages/typescript/test/ts_project/a/BUILD.bazel index 1fe9a2ace4..ebbb07a282 100644 --- a/packages/typescript/test/ts_project/a/BUILD.bazel +++ b/packages/typescript/test/ts_project/a/BUILD.bazel @@ -1,6 +1,7 @@ load("@npm_bazel_typescript//:index.bzl", "ts_project") ts_project( + composite = True, extends = ["//packages/typescript/test/ts_project:tsconfig-base.json"], source_map = True, visibility = ["//packages/typescript/test:__subpackages__"], diff --git a/packages/typescript/test/ts_project/b/BUILD.bazel b/packages/typescript/test/ts_project/b/BUILD.bazel index c32f3e286a..6f141bd59f 100644 --- a/packages/typescript/test/ts_project/b/BUILD.bazel +++ b/packages/typescript/test/ts_project/b/BUILD.bazel @@ -6,6 +6,7 @@ package(default_visibility = ["//packages/typescript/test:__subpackages__"]) ts_project( name = "tsconfig", # This will use ./tsconfig.json srcs = [":b.ts"], + composite = True, extends = ["//packages/typescript/test/ts_project:tsconfig-base.json"], deps = ["//packages/typescript/test/ts_project/a:tsconfig"], ) @@ -14,6 +15,7 @@ ts_project( name = "tsconfig-test", # This will use ./tsconfig-test.json testonly = True, srcs = [":b.spec.ts"], + composite = True, extends = ["//packages/typescript/test/ts_project:tsconfig-base.json"], deps = [ ":tsconfig", diff --git a/packages/typescript/test/ts_project/c/BUILD.bazel b/packages/typescript/test/ts_project/c/BUILD.bazel index 599069cc78..71b03bd5c2 100644 --- a/packages/typescript/test/ts_project/c/BUILD.bazel +++ b/packages/typescript/test/ts_project/c/BUILD.bazel @@ -3,6 +3,7 @@ load("@npm_bazel_typescript//:index.bzl", "ts_project") ts_project( name = "compile", srcs = [":c.ts"], + composite = True, extends = ["//packages/typescript/test/ts_project:tsconfig-base.json"], tsconfig = "tsconfig.json", deps = ["//packages/typescript/test/ts_project/b:tsconfig"], From 147968b9c8ff9b405159d1a58d958217b61e0e68 Mon Sep 17 00:00:00 2001 From: Alex Eagle Date: Mon, 23 Mar 2020 17:08:49 -0700 Subject: [PATCH 4/6] chore: set ts_project#declaration default to False --- packages/typescript/src/internal/ts_project.bzl | 2 +- packages/typescript/test/ts_project_simple/BUILD.bazel | 10 ++++++++++ .../typescript/test/ts_project_simple/index.golden.js | 3 +++ packages/typescript/test/ts_project_simple/index.ts | 1 + .../typescript/test/ts_project_simple/tsconfig.json | 1 + 5 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 packages/typescript/test/ts_project_simple/BUILD.bazel create mode 100644 packages/typescript/test/ts_project_simple/index.golden.js create mode 100644 packages/typescript/test/ts_project_simple/index.ts create mode 100644 packages/typescript/test/ts_project_simple/tsconfig.json diff --git a/packages/typescript/src/internal/ts_project.bzl b/packages/typescript/src/internal/ts_project.bzl index 2a2229a392..c1b40a25f8 100644 --- a/packages/typescript/src/internal/ts_project.bzl +++ b/packages/typescript/src/internal/ts_project.bzl @@ -132,7 +132,7 @@ def ts_project_macro( srcs = None, deps = [], extends = None, - declaration = True, + declaration = False, source_map = False, declaration_map = False, composite = False, diff --git a/packages/typescript/test/ts_project_simple/BUILD.bazel b/packages/typescript/test/ts_project_simple/BUILD.bazel new file mode 100644 index 0000000000..ffd8c23d32 --- /dev/null +++ b/packages/typescript/test/ts_project_simple/BUILD.bazel @@ -0,0 +1,10 @@ +load("@build_bazel_rules_nodejs//internal/golden_file_test:golden_file_test.bzl", "golden_file_test") +load("@npm_bazel_typescript//:index.bzl", "ts_project") + +ts_project() + +golden_file_test( + name = "test", + actual = "index.js", + golden = "index.golden.js", +) diff --git a/packages/typescript/test/ts_project_simple/index.golden.js b/packages/typescript/test/ts_project_simple/index.golden.js new file mode 100644 index 0000000000..8e6486da69 --- /dev/null +++ b/packages/typescript/test/ts_project_simple/index.golden.js @@ -0,0 +1,3 @@ +"use strict"; +exports.__esModule = true; +exports.a = 'hello'; diff --git a/packages/typescript/test/ts_project_simple/index.ts b/packages/typescript/test/ts_project_simple/index.ts new file mode 100644 index 0000000000..a668b7e336 --- /dev/null +++ b/packages/typescript/test/ts_project_simple/index.ts @@ -0,0 +1 @@ +export const a: string = 'hello'; diff --git a/packages/typescript/test/ts_project_simple/tsconfig.json b/packages/typescript/test/ts_project_simple/tsconfig.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/packages/typescript/test/ts_project_simple/tsconfig.json @@ -0,0 +1 @@ +{} From 28953336cea1a68d08d46d5a2b9496fe6c81f427 Mon Sep 17 00:00:00 2001 From: Alex Eagle Date: Mon, 23 Mar 2020 21:26:07 -0700 Subject: [PATCH 5/6] chore: add incremental param to ts_project macro It controls writing the .tsbuildinfo output --- .../typescript/src/internal/ts_project.bzl | 45 ++++++++++++++++--- .../test/ts_project_simple/BUILD.bazel | 3 ++ 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/packages/typescript/src/internal/ts_project.bzl b/packages/typescript/src/internal/ts_project.bzl index c1b40a25f8..7fa53d5d20 100644 --- a/packages/typescript/src/internal/ts_project.bzl +++ b/packages/typescript/src/internal/ts_project.bzl @@ -136,12 +136,13 @@ def ts_project_macro( source_map = False, declaration_map = False, composite = False, + incremental = False, emit_declaration_only = False, tsc = "@npm//typescript/bin:tsc", **kwargs): """Compiles one TypeScript project using `tsc -p` - Unlike `ts_library`, this rule is the thinnest possible layer of Bazel awareness on top + Unlike `ts_library`, this rule is the thinnest possible layer of Bazel interoperability on top of the TypeScript compiler. It shifts the burden of configuring TypeScript into the tsconfig.json file. TODO(alexeagle): update https://github.com/bazelbuild/rules_nodejs/blob/master/docs/TypeScript.md#alternatives to describe the trade-offs between the two rules. @@ -149,15 +150,16 @@ def ts_project_macro( Any code that works with `tsc` should work with `ts_project` with a few caveats: - Bazel requires that the `outDir` (and `declarationDir`) be set to - `bazel-out/[arch]/bin/path/to/package` + `bazel-out/[target architecture]/bin/path/to/package` so we override whatever settings appear in your tsconfig. - Bazel expects that each output is produced by a single rule. - Thus if you have two `ts_project` rules with overlapping sources (the same .ts file - appears in more than one) then you get an error if you try to build both together. + Thus if you have two `ts_project` rules with overlapping sources (the same `.ts` file + appears in more than one) then you get an error about conflicting `.js` output + files if you try to build both together. Worse, if you build them separately then the output directory will contain whichever one you happened to build most recently. This is highly discouraged. - > Note, in order for TypeScript to find referenced projects in the bazel-out folder, + > Note: in order for TypeScript to resolve relative references to the bazel-out folder, > we recommend that the base tsconfig contain a rootDirs section that includes all > possible locations they may appear. > @@ -181,6 +183,35 @@ def ts_project_macro( > } > ``` + > Note: when using a non-sandboxed spawn strategy (which is the default on Windows), + > Bazel deletes outputs from the previous execution before running `tsc`. + > This causes a problem with TypeScript's incremental mode: if the `.tsbuildinfo` file + > is not known to be an output of the rule, then Bazel will leave it in the output + > directory, and when `tsc` runs, it may see that the outputs written by the prior + > invocation are up-to-date and skip the emit of these files. This will cause Bazel + > to intermittently fail with an error that some outputs were not written. + > This is why we depend on + > `composite` and/or `incremental` attributes to be provided, so we can tell Bazel to + > expect a `.tsbuildinfo` output to ensure it is deleted before a subsequent compilation. + > At present, we don't do anything useful with the `.tsbuildinfo` output, and this rule + > does not actually have incremental behavior. Deleting the file is actually + > counter-productive in terms of TypeScript compile performance. + > Follow https://github.com/bazelbuild/rules_nodejs/issues/1726 + + > Note: When using Project References, TypeScript will expect to verify that the outputs of referenced + > projects are up-to-date with respect to their inputs (this is true even without using the `--build` option). + > When using a non-sandboxed spawn strategy, `tsc` can read the sources from other `ts_project` + > rules in your project, and will expect that the `tsconfig.json` file for those references will + > indicate where the outputs were written. However the `outDir` is determined by this Bazel rule so + > it cannot be known from reading the `tsconfig.json` file. + > This problem is manifested as a TypeScript diagnostic like + > `error TS6305: Output file '/path/to/execroot/a.d.ts' has not been built from source file '/path/to/execroot/a.ts'.` + > As a workaround, you can give the Windows "fastbuild" output directory as the `outDir` in your tsconfig file. + > On other platforms, the value isn't read so it does no harm. + > See https://github.com/bazelbuild/rules_nodejs/tree/master/packages/typescript/test/ts_project as an example. + > We hope this will be fixed in a future release of TypeScript; + > follow https://github.com/microsoft/TypeScript/issues/37378 + Args: name: A name for the target. @@ -214,6 +245,8 @@ def ts_project_macro( Instructs Bazel to expect a `.d.ts.map` output for each `.ts` source. composite: if the `composite` bit is set in the tsconfig. Instructs Bazel to expect a `.tsbuildinfo` output and a `.d.ts` output for each `.ts` source. + incremental: if the `incremental` bit is set in the tsconfig. + Instructs Bazel to expect a `.tsbuildinfo` output. emit_declaration_only: if the `emitDeclarationOnly` bit is set in the tsconfig. Instructs Bazel *not* to expect `.js` or `.js.map` outputs for `.ts` sources. """ @@ -234,7 +267,7 @@ def ts_project_macro( map_outs = _out_paths(srcs, ".js.map") if source_map and not emit_declaration_only else [], typings_outs = _out_paths(srcs, ".d.ts") if declaration or composite else [], typing_maps_outs = _out_paths(srcs, ".d.ts.map") if declaration_map else [], - buildinfo_out = tsconfig[:-5] + ".tsbuildinfo" if composite else None, + buildinfo_out = tsconfig[:-5] + ".tsbuildinfo" if composite or incremental else None, tsc = tsc, **kwargs ) diff --git a/packages/typescript/test/ts_project_simple/BUILD.bazel b/packages/typescript/test/ts_project_simple/BUILD.bazel index ffd8c23d32..20631cfccc 100644 --- a/packages/typescript/test/ts_project_simple/BUILD.bazel +++ b/packages/typescript/test/ts_project_simple/BUILD.bazel @@ -1,10 +1,13 @@ load("@build_bazel_rules_nodejs//internal/golden_file_test:golden_file_test.bzl", "golden_file_test") load("@npm_bazel_typescript//:index.bzl", "ts_project") +# This uses defaults for all attributes. +# It will find `index.ts` and produce `index.js` ts_project() golden_file_test( name = "test", + # Refers to the output from ts_project above actual = "index.js", golden = "index.golden.js", ) From a2d87354b6f79a9c4b3e8522a643faf184870ab2 Mon Sep 17 00:00:00 2001 From: Alex Eagle Date: Mon, 23 Mar 2020 22:35:31 -0700 Subject: [PATCH 6/6] chore: add args attribute to ts_project Also add a test for an intermediate ts_project rule with no srcs --- examples/react_webpack/BUILD.bazel | 16 -- packages/typescript/docs/install.md | 52 +++--- .../typescript/src/internal/ts_project.bzl | 149 +++++++++++------- .../typescript/test/ts_project/b/BUILD.bazel | 3 + .../typescript/test/ts_project/b/b.spec.ts | 3 + .../ts_project/empty_intermediate/BUILD.bazel | 18 +++ .../test/ts_project/empty_intermediate/a.d.ts | 1 + .../test/ts_project/empty_intermediate/c.ts | 3 + .../empty_intermediate/tsconfig-a.json | 6 + .../empty_intermediate/tsconfig-b.json | 3 + .../empty_intermediate/tsconfig-c.json | 15 ++ .../simple}/BUILD.bazel | 0 .../simple}/index.golden.js | 0 .../simple}/index.ts | 0 .../simple}/tsconfig.json | 0 15 files changed, 164 insertions(+), 105 deletions(-) create mode 100644 packages/typescript/test/ts_project/empty_intermediate/BUILD.bazel create mode 100644 packages/typescript/test/ts_project/empty_intermediate/a.d.ts create mode 100644 packages/typescript/test/ts_project/empty_intermediate/c.ts create mode 100644 packages/typescript/test/ts_project/empty_intermediate/tsconfig-a.json create mode 100644 packages/typescript/test/ts_project/empty_intermediate/tsconfig-b.json create mode 100644 packages/typescript/test/ts_project/empty_intermediate/tsconfig-c.json rename packages/typescript/test/{ts_project_simple => ts_project/simple}/BUILD.bazel (100%) rename packages/typescript/test/{ts_project_simple => ts_project/simple}/index.golden.js (100%) rename packages/typescript/test/{ts_project_simple => ts_project/simple}/index.ts (100%) rename packages/typescript/test/{ts_project_simple => ts_project/simple}/tsconfig.json (100%) diff --git a/examples/react_webpack/BUILD.bazel b/examples/react_webpack/BUILD.bazel index 86bb368625..2a834eebd4 100644 --- a/examples/react_webpack/BUILD.bazel +++ b/examples/react_webpack/BUILD.bazel @@ -14,28 +14,12 @@ sass( ) ts_project( - name = "tsconfig", - srcs = [ - "index.tsx", - "types.d.ts", - ], - # The tsconfig.json file doesn't enable "declaration" or "composite" - # so tsc won't produce any .d.ts outputs. We need to tell Bazel not - # to expect those outputs being produced. - declaration = False, deps = [ "@npm//@types", "@npm//csstype", ], ) -# If you don't want to write //:tsconfig as the label for the TypeScript compilation, -# use an alias like this, so you can write //:compile_ts instead. -alias( - name = "compile_ts", - actual = "tsconfig", -) - webpack( name = "bundle", outs = ["app.bundle.js"], diff --git a/packages/typescript/docs/install.md b/packages/typescript/docs/install.md index f6b0537e73..7f416cda33 100644 --- a/packages/typescript/docs/install.md +++ b/packages/typescript/docs/install.md @@ -2,47 +2,33 @@ The TypeScript rules integrate the TypeScript compiler with Bazel. -Looking for Karma rules `ts_web_test` and `karma_web_test`? -These are now documented in the README at http://npmjs.com/package/@bazel/karma - ## Alternatives -This package provides Bazel wrappers around the TypeScript compiler, and are how we compile TS code at Google. - -These rules are opinionated, for example: - -- Your TS code must compile under the `--declaration` flag so that downstream libraries depend only on types, not implementation. This makes Bazel faster by avoiding cascading rebuilds in cases where the types aren't changed. -- We control the output format and module syntax so that downstream rules can rely on them. +This package provides Bazel wrappers around the TypeScript compiler. -They are also fast and optimized: +At a high level, there are two alternatives provided: `ts_project` and `ts_library`. +This section describes the trade-offs between these rules. -- We keep a running TypeScript compile running as a daemon, using Bazel workers. This process avoids re-parse and re-JIT of the >1MB `typescript.js` and keeps cached bound ASTs for input files which saves time. +`ts_project` simply runs `tsc --project`, with Bazel knowing which outputs to expect based on the TypeScript compiler options, and with interoperability with other TypeScript rules via a Bazel Provider (DeclarationInfo) that transmits the type information. +It is intended as an easy on-boarding for existing TypeScript code and should be familiar if your background is in frontend ecosystem idioms. +Any behavior of `ts_project` should be reproducible outside of Bazel, with a couple of caveats noted in the rule documentation below. -We understand this is a tradeoff. If you want to use the plain TypeScript compiler provided by the TS team at Microsoft, you can do this by calling its CLI directly. For example, +> We used to recommend using the `tsc` rule directly from the `typescript` project, like +> `load("@npm//typescript:index.bzl", "tsc")` +> However `ts_project` is strictly better and should be used instead. -```python -load("@npm//typescript:index.bzl", "tsc") +`ts_library` is an open-sourced version of the rule we use to compile TS code at Google. +It should be familiar if your background is in Bazel idioms. +It is very complex, involving code generation of the `tsconfig.json` file, a custom compiler binary, and a lot of extra features. +It is also opinionated, and may not work with existing TypeScript code. For example: -srcs = glob(["*.ts"]) -deps = ["@npm//@types/node"] +- Your TS code must compile under the `--declaration` flag so that downstream libraries depend only on types, not implementation. This makes Bazel faster by avoiding cascading rebuilds in cases where the types aren't changed. +- We control the output format and module syntax so that downstream rules can rely on them. -tsc( - name = "compile", - data = srcs + deps, - outs = [s.replace(".ts", ext) for ext in [".js", ".d.ts"] for s in srcs], - args = [ - "--outDir", - "$(RULEDIR)", - "--lib", - "es2017,dom", - "--downlevelIteration", - "--declaration", - ] + [ - "$(location %s)" % s - for s in srcs - ], -) -``` +On the other hand, `ts_library` is also fast and optimized. +We keep a running TypeScript compile running as a daemon, using Bazel workers. +This process avoids re-parse and re-JIT of the >1MB `typescript.js` and keeps cached bound ASTs for input files which saves time. +We also produce JS code which can be loaded faster (using named AMD module format) and which can be consumed by the Closure Compiler (via integration with [tsickle](https://github.com/angular/tsickle)). ## Installation diff --git a/packages/typescript/src/internal/ts_project.bzl b/packages/typescript/src/internal/ts_project.bzl index 7fa53d5d20..a9df74a363 100644 --- a/packages/typescript/src/internal/ts_project.bzl +++ b/packages/typescript/src/internal/ts_project.bzl @@ -2,14 +2,17 @@ load("@build_bazel_rules_nodejs//:providers.bzl", "DeclarationInfo", "NpmPackageInfo", "run_node") +_DEFAULT_TSC = "@npm//typescript/bin:tsc" + _ATTRS = { # NB: no restriction on extensions here, because tsc sometimes adds type-check support # for more file kinds (like require('some.json')) and also # if you swap out the `compiler` attribute (like with ngtsc) # that compiler might allow more sources than tsc does. "srcs": attr.label_list(allow_files = True, mandatory = True), + "args": attr.string_list(), "extends": attr.label_list(allow_files = [".json"]), - "tsc": attr.label(default = Label("@npm//typescript/bin:tsc"), executable = True, cfg = "host"), + "tsc": attr.label(default = Label(_DEFAULT_TSC), executable = True, cfg = "host"), "tsconfig": attr.label(mandatory = True, allow_single_file = [".json"]), "deps": attr.label_list(providers = [DeclarationInfo]), } @@ -35,8 +38,12 @@ _TsConfigInfo = provider( def _ts_project_impl(ctx): arguments = ctx.actions.args() + + # Add user specified arguments *before* rule supplied arguments + arguments.add_all(ctx.attr.args) + arguments.add_all([ - "-p", + "--project", ctx.file.tsconfig.short_path, "--outDir", "/".join([ctx.bin_dir.path, ctx.label.package]), @@ -71,7 +78,7 @@ def _ts_project_impl(ctx): # TODO: we could maybe filter these to be tsconfig.json or *.d.ts only # we don't expect tsc wants to read any other files from npm packages. deps_depsets.append(dep[NpmPackageInfo].sources) - elif DeclarationInfo in dep: + if DeclarationInfo in dep: deps_depsets.append(dep[DeclarationInfo].transitive_declarations) inputs = ctx.files.srcs + depset(transitive = deps_depsets).to_list() + [ctx.file.tsconfig] @@ -80,34 +87,36 @@ def _ts_project_impl(ctx): outputs = ctx.outputs.js_outs + ctx.outputs.map_outs + ctx.outputs.typings_outs + ctx.outputs.typing_maps_outs if ctx.outputs.buildinfo_out: outputs.append(ctx.outputs.buildinfo_out) - - if len(outputs) == 0: - return [] - - run_node( - ctx, - inputs = inputs, - arguments = [arguments], - outputs = outputs, - executable = "tsc", - progress_message = "Compiling TypeScript project %s" % ctx.file.tsconfig.short_path, - ) - - runtime_files = depset(ctx.outputs.js_outs + ctx.outputs.map_outs) - typings_files = ctx.outputs.typings_outs + [s for s in ctx.files.srcs if s.path.endswith(".d.ts")] + runtime_outputs = depset(ctx.outputs.js_outs + ctx.outputs.map_outs) + typings_outputs = ctx.outputs.typings_outs + [s for s in ctx.files.srcs if s.path.endswith(".d.ts")] + + if len(outputs) > 0: + run_node( + ctx, + inputs = inputs, + arguments = [arguments], + outputs = outputs, + executable = "tsc", + progress_message = "Compiling TypeScript project %s" % ctx.file.tsconfig.short_path, + ) return [ DeclarationInfo( - declarations = depset(typings_files), - transitive_declarations = depset(typings_files, transitive = [ + declarations = depset(typings_outputs), + transitive_declarations = depset(typings_outputs, transitive = [ dep[DeclarationInfo].transitive_declarations for dep in ctx.attr.deps ]), ), + # DefaultInfo 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. DefaultInfo( - files = runtime_files, + files = runtime_outputs, runfiles = ctx.runfiles( - transitive_files = runtime_files, + transitive_files = runtime_outputs, collect_default = True, ), ), @@ -130,6 +139,7 @@ def ts_project_macro( name = "tsconfig", tsconfig = None, srcs = None, + args = [], deps = [], extends = None, declaration = False, @@ -138,14 +148,23 @@ def ts_project_macro( composite = False, incremental = False, emit_declaration_only = False, - tsc = "@npm//typescript/bin:tsc", + tsc = _DEFAULT_TSC, **kwargs): - """Compiles one TypeScript project using `tsc -p` + """Compiles one TypeScript project using `tsc --project` + + This is a drop-in replacement for the `tsc` rule automatically generated for the "typescript" + package, typically loaded from `@npm//typescript:index.bzl`. Unlike bare `tsc`, this rule understands + the Bazel interop mechanism (Providers) so that this rule works with others that produce or consume + TypeScript typings (`.d.ts` files). Unlike `ts_library`, this rule is the thinnest possible layer of Bazel interoperability on top of the TypeScript compiler. It shifts the burden of configuring TypeScript into the tsconfig.json file. - TODO(alexeagle): update https://github.com/bazelbuild/rules_nodejs/blob/master/docs/TypeScript.md#alternatives - to describe the trade-offs between the two rules. + See https://github.com/bazelbuild/rules_nodejs/blob/master/docs/TypeScript.md#alternatives + for more details about the trade-offs between the two rules. + + Some TypeScript options affect which files are emitted, and Bazel wants to know these ahead-of-time. + So several options from the tsconfig file must be mirrored as attributes to ts_project. + See https://www.typescriptlang.org/v2/en/tsconfig for a listing of the TypeScript options. Any code that works with `tsc` should work with `ts_project` with a few caveats: @@ -183,34 +202,49 @@ def ts_project_macro( > } > ``` - > Note: when using a non-sandboxed spawn strategy (which is the default on Windows), - > Bazel deletes outputs from the previous execution before running `tsc`. - > This causes a problem with TypeScript's incremental mode: if the `.tsbuildinfo` file - > is not known to be an output of the rule, then Bazel will leave it in the output - > directory, and when `tsc` runs, it may see that the outputs written by the prior - > invocation are up-to-date and skip the emit of these files. This will cause Bazel - > to intermittently fail with an error that some outputs were not written. - > This is why we depend on - > `composite` and/or `incremental` attributes to be provided, so we can tell Bazel to - > expect a `.tsbuildinfo` output to ensure it is deleted before a subsequent compilation. - > At present, we don't do anything useful with the `.tsbuildinfo` output, and this rule - > does not actually have incremental behavior. Deleting the file is actually - > counter-productive in terms of TypeScript compile performance. - > Follow https://github.com/bazelbuild/rules_nodejs/issues/1726 - - > Note: When using Project References, TypeScript will expect to verify that the outputs of referenced - > projects are up-to-date with respect to their inputs (this is true even without using the `--build` option). - > When using a non-sandboxed spawn strategy, `tsc` can read the sources from other `ts_project` - > rules in your project, and will expect that the `tsconfig.json` file for those references will - > indicate where the outputs were written. However the `outDir` is determined by this Bazel rule so - > it cannot be known from reading the `tsconfig.json` file. - > This problem is manifested as a TypeScript diagnostic like - > `error TS6305: Output file '/path/to/execroot/a.d.ts' has not been built from source file '/path/to/execroot/a.ts'.` - > As a workaround, you can give the Windows "fastbuild" output directory as the `outDir` in your tsconfig file. - > On other platforms, the value isn't read so it does no harm. - > See https://github.com/bazelbuild/rules_nodejs/tree/master/packages/typescript/test/ts_project as an example. - > We hope this will be fixed in a future release of TypeScript; - > follow https://github.com/microsoft/TypeScript/issues/37378 + ### Issues when running non-sandboxed + + When using a non-sandboxed spawn strategy (which is the default on Windows), you may + observe these problems which require workarounds: + + 1) Bazel deletes outputs from the previous execution before running `tsc`. + This causes a problem with TypeScript's incremental mode: if the `.tsbuildinfo` file + is not known to be an output of the rule, then Bazel will leave it in the output + directory, and when `tsc` runs, it may see that the outputs written by the prior + invocation are up-to-date and skip the emit of these files. This will cause Bazel + to intermittently fail with an error that some outputs were not written. + This is why we depend on `composite` and/or `incremental` attributes to be provided, + so we can tell Bazel to expect a `.tsbuildinfo` output to ensure it is deleted before a + subsequent compilation. + At present, we don't do anything useful with the `.tsbuildinfo` output, and this rule + does not actually have incremental behavior. Deleting the file is actually + counter-productive in terms of TypeScript compile performance. + Follow https://github.com/bazelbuild/rules_nodejs/issues/1726 + + 2) When using Project References, TypeScript will expect to verify that the outputs of referenced + projects are up-to-date with respect to their inputs. + (This is true even without using the `--build` option). + When using a non-sandboxed spawn strategy, `tsc` can read the sources from other `ts_project` + rules in your project, and will expect that the `tsconfig.json` file for those references will + indicate where the outputs were written. However the `outDir` is determined by this Bazel rule so + it cannot be known from reading the `tsconfig.json` file. + This problem is manifested as a TypeScript diagnostic like + `error TS6305: Output file '/path/to/execroot/a.d.ts' has not been built from source file '/path/to/execroot/a.ts'.` + As a workaround, you can give the Windows "fastbuild" output directory as the `outDir` in your tsconfig file. + On other platforms, the value isn't read so it does no harm. + See https://github.com/bazelbuild/rules_nodejs/tree/master/packages/typescript/test/ts_project as an example. + We hope this will be fixed in a future release of TypeScript; + follow https://github.com/microsoft/TypeScript/issues/37378 + + 3) When TypeScript encounters an import statement, it adds the source file resolved by that reference + to the program. However you may have included that source file in a different project, so this causes + the problem mentioned above where a source file is in multiple programs. + (Note, if you use Project References this is not the case, TS will know the referenced + file is part of the other program.) + This will result in duplicate emit for the same file, which produces an error + since the files written to the output tree are read-only. + Workarounds include using using Project References, or simply grouping the whole compilation + into one program (if this doesn't exceed your time budget). Args: name: A name for the target. @@ -219,7 +253,7 @@ def ts_project_macro( srcs: List of labels of TypeScript source files to be provided to the compiler. - If absent, defaults to `**/*.ts` (all TypeScript files in the package). + If absent, defaults to `**/*.ts[x]` (all TypeScript files in the package). deps: List of labels of other rules that produce TypeScript typings (.d.ts files) @@ -231,6 +265,8 @@ def ts_project_macro( Must include any tsconfig files "chained" by extends clauses. + args: List of strings of additional command-line arguments to pass to tsc. + tsc: Label of the TypeScript compiler binary to run. Override this if your npm_install or yarn_install isn't named "npm" @@ -252,7 +288,7 @@ def ts_project_macro( """ if srcs == None: - srcs = native.glob(["**/*.ts"]) + srcs = native.glob(["**/*.ts", "**/*.tsx"]) if tsconfig == None: tsconfig = name + ".json" @@ -261,6 +297,7 @@ def ts_project_macro( name = name, srcs = srcs, deps = deps, + args = args, tsconfig = tsconfig, extends = extends, js_outs = _out_paths(srcs, ".js") if not emit_declaration_only else [], diff --git a/packages/typescript/test/ts_project/b/BUILD.bazel b/packages/typescript/test/ts_project/b/BUILD.bazel index 6f141bd59f..22304b501c 100644 --- a/packages/typescript/test/ts_project/b/BUILD.bazel +++ b/packages/typescript/test/ts_project/b/BUILD.bazel @@ -6,6 +6,8 @@ package(default_visibility = ["//packages/typescript/test:__subpackages__"]) ts_project( name = "tsconfig", # This will use ./tsconfig.json srcs = [":b.ts"], + # just a test for the pass-through args attribute + args = ["--emitBOM"], composite = True, extends = ["//packages/typescript/test/ts_project:tsconfig-base.json"], deps = ["//packages/typescript/test/ts_project/a:tsconfig"], @@ -20,6 +22,7 @@ ts_project( deps = [ ":tsconfig", "@npm//@types/jasmine", + "@npm//@types/node", ], ) diff --git a/packages/typescript/test/ts_project/b/b.spec.ts b/packages/typescript/test/ts_project/b/b.spec.ts index e72c0a1c3d..18fb545b46 100644 --- a/packages/typescript/test/ts_project/b/b.spec.ts +++ b/packages/typescript/test/ts_project/b/b.spec.ts @@ -7,4 +7,7 @@ describe('b', () => { sayHello(' world'); expect(captured).toBe('hello world'); }); + it('should include byte-order mark since that was passed in args attr', () => { + expect(require('fs').readFileSync(require.resolve('./b'), 'utf-8')[0]).toBe('\ufeff'); + }); }); diff --git a/packages/typescript/test/ts_project/empty_intermediate/BUILD.bazel b/packages/typescript/test/ts_project/empty_intermediate/BUILD.bazel new file mode 100644 index 0000000000..0877687cb5 --- /dev/null +++ b/packages/typescript/test/ts_project/empty_intermediate/BUILD.bazel @@ -0,0 +1,18 @@ +load("@npm_bazel_typescript//:index.bzl", "ts_project") + +ts_project( + name = "tsconfig-a", + srcs = ["a.d.ts"], +) + +ts_project( + name = "tsconfig-b", + srcs = [], + deps = ["tsconfig-a"], +) + +ts_project( + name = "tsconfig-c", + srcs = ["c.ts"], + deps = ["tsconfig-b"], +) diff --git a/packages/typescript/test/ts_project/empty_intermediate/a.d.ts b/packages/typescript/test/ts_project/empty_intermediate/a.d.ts new file mode 100644 index 0000000000..106d610ab9 --- /dev/null +++ b/packages/typescript/test/ts_project/empty_intermediate/a.d.ts @@ -0,0 +1 @@ +export declare const a: string; diff --git a/packages/typescript/test/ts_project/empty_intermediate/c.ts b/packages/typescript/test/ts_project/empty_intermediate/c.ts new file mode 100644 index 0000000000..6431a4f84c --- /dev/null +++ b/packages/typescript/test/ts_project/empty_intermediate/c.ts @@ -0,0 +1,3 @@ +import {a} from './a'; + +console.log(a); diff --git a/packages/typescript/test/ts_project/empty_intermediate/tsconfig-a.json b/packages/typescript/test/ts_project/empty_intermediate/tsconfig-a.json new file mode 100644 index 0000000000..3b2c25a809 --- /dev/null +++ b/packages/typescript/test/ts_project/empty_intermediate/tsconfig-a.json @@ -0,0 +1,6 @@ +{ + "files": ["a.ts"], + "compilerOptions": { + "declaration": true + } +} \ No newline at end of file diff --git a/packages/typescript/test/ts_project/empty_intermediate/tsconfig-b.json b/packages/typescript/test/ts_project/empty_intermediate/tsconfig-b.json new file mode 100644 index 0000000000..6f5fba878a --- /dev/null +++ b/packages/typescript/test/ts_project/empty_intermediate/tsconfig-b.json @@ -0,0 +1,3 @@ +{ + "files": [] +} \ No newline at end of file diff --git a/packages/typescript/test/ts_project/empty_intermediate/tsconfig-c.json b/packages/typescript/test/ts_project/empty_intermediate/tsconfig-c.json new file mode 100644 index 0000000000..dcdbec4409 --- /dev/null +++ b/packages/typescript/test/ts_project/empty_intermediate/tsconfig-c.json @@ -0,0 +1,15 @@ +{ + "files": ["c.ts"], + "compilerOptions": { + // Help TypeScript locate the a.d.ts file from previous compilation. Needed when running in a sandbox or remote. + "rootDirs": [ + ".", + "../../../../../bazel-out/darwin-fastbuild/bin/packages/typescript/test/ts_project/empty_intermediate", + "../../../../../bazel-out/k8-fastbuild/bin/packages/typescript/test/ts_project/empty_intermediate", + "../../../../../bazel-out/x64_windows-fastbuild/bin/packages/typescript/test/ts_project/empty_intermediate", + "../../../../../bazel-out/darwin-dbg/bin/packages/typescript/test/ts_project/empty_intermediate", + "../../../../../bazel-out/k8-dbg/bin/packages/typescript/test/ts_project/empty_intermediate", + "../../../../../bazel-out/x64_windows-dbg/bin/packages/typescript/test/ts_project/empty_intermediate", + ], + } +} \ No newline at end of file diff --git a/packages/typescript/test/ts_project_simple/BUILD.bazel b/packages/typescript/test/ts_project/simple/BUILD.bazel similarity index 100% rename from packages/typescript/test/ts_project_simple/BUILD.bazel rename to packages/typescript/test/ts_project/simple/BUILD.bazel diff --git a/packages/typescript/test/ts_project_simple/index.golden.js b/packages/typescript/test/ts_project/simple/index.golden.js similarity index 100% rename from packages/typescript/test/ts_project_simple/index.golden.js rename to packages/typescript/test/ts_project/simple/index.golden.js diff --git a/packages/typescript/test/ts_project_simple/index.ts b/packages/typescript/test/ts_project/simple/index.ts similarity index 100% rename from packages/typescript/test/ts_project_simple/index.ts rename to packages/typescript/test/ts_project/simple/index.ts diff --git a/packages/typescript/test/ts_project_simple/tsconfig.json b/packages/typescript/test/ts_project/simple/tsconfig.json similarity index 100% rename from packages/typescript/test/ts_project_simple/tsconfig.json rename to packages/typescript/test/ts_project/simple/tsconfig.json