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..0caeeb98b1 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,22 +13,14 @@ 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", + ], + declaration = False, + deps = [ "@npm//@types", "@npm//csstype", ], 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..55aa5d7677 --- /dev/null +++ b/examples/react_webpack/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "jsx": "react", + "lib": ["ES2015", "DOM"], + "rootDirs": [ + ".", + "bazel-out/darwin-fastbuild/bin", + ], + "listFiles": true, + "traceResolution": true + }, + "files": ["index.tsx", "types.d.ts"] +} \ 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..3cb5ef996d --- /dev/null +++ b/packages/typescript/src/internal/ts_project.bzl @@ -0,0 +1,192 @@ +"ts_project rule" + +load("@build_bazel_rules_nodejs//:providers.bzl", "DeclarationInfo", "NpmPackageInfo", "run_node") + +_ATTRS = { + # TODO: limit to the extensions we expect? + # ts, tsx, js, jsx, json + "srcs": attr.label_list(allow_files = 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]), +} + +_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]), + ]) + + 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. 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", + ] + } + ``` + + 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"]) + 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 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..80a0c8ed6c --- /dev/null +++ b/packages/typescript/test/ts_project/a/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig-base.json", + "compilerOptions": { + "sourceMap": true + } +} \ 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..edcf7705e0 --- /dev/null +++ b/packages/typescript/test/ts_project/b/BUILD.bazel @@ -0,0 +1,31 @@ +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", + #"//packages/typescript/test/ts_project/a: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..adf55b3cd7 --- /dev/null +++ b/packages/typescript/test/ts_project/b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig-base.json", + "references": [ + {"path": "../a"} + ], + "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..dddae28c74 --- /dev/null +++ b/packages/typescript/test/ts_project/tsconfig-base.json @@ -0,0 +1,19 @@ +{ + "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", + ], + } +} \ No newline at end of file