Skip to content

Commit

Permalink
feat(typescript): add ts_project rule
Browse files Browse the repository at this point in the history
This is a very thin layer on top of vanilla tsc
  • Loading branch information
alexeagle committed Mar 16, 2020
1 parent 063fb13 commit a378a72
Show file tree
Hide file tree
Showing 22 changed files with 339 additions and 15 deletions.
3 changes: 3 additions & 0 deletions examples/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
22 changes: 7 additions & 15 deletions examples/react_webpack/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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",
],
Expand Down
4 changes: 4 additions & 0 deletions examples/react_webpack/WORKSPACE
Original file line number Diff line number Diff line change
Expand Up @@ -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()
1 change: 1 addition & 0 deletions examples/react_webpack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
9 changes: 9 additions & 0 deletions examples/react_webpack/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"jsx": "react",
"lib": ["ES2015", "DOM"],
"listFiles": true,
"traceResolution": true
},
"files": ["index.tsx", "types.d.ts"]
}
2 changes: 2 additions & 0 deletions packages/typescript/src/index.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ 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")

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
2 changes: 2 additions & 0 deletions packages/typescript/src/index.docs.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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.
1 change: 1 addition & 0 deletions packages/typescript/src/internal/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ filegroup(
srcs = [
"build_defs.bzl",
"ts_config.bzl",
"ts_project.bzl",
"ts_repositories.bzl",
"//internal/devserver:package_contents",
],
Expand Down
192 changes: 192 additions & 0 deletions packages/typescript/src/internal/ts_project.bzl
Original file line number Diff line number Diff line change
@@ -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
)
1 change: 1 addition & 0 deletions packages/typescript/test/ts_project/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
exports_files(["tsconfig-base.json"])
9 changes: 9 additions & 0 deletions packages/typescript/test/ts_project/a/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -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__"],
)
1 change: 1 addition & 0 deletions packages/typescript/test/ts_project/a/a.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const a: string = 'hello';
6 changes: 6 additions & 0 deletions packages/typescript/test/ts_project/a/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "../tsconfig-base.json",
"compilerOptions": {
"sourceMap": true
}
}
31 changes: 31 additions & 0 deletions packages/typescript/test/ts_project/b/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -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"
],
)
10 changes: 10 additions & 0 deletions packages/typescript/test/ts_project/b/b.spec.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
5 changes: 5 additions & 0 deletions packages/typescript/test/ts_project/b/b.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import {a} from '../a/a';

export function sayHello(f: string) {
console.log(a + f);
}
7 changes: 7 additions & 0 deletions packages/typescript/test/ts_project/b/tsconfig-test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "../tsconfig-base.json",
"references": [
{"path": "./"}
],
"include": ["*.spec.ts"]
}
7 changes: 7 additions & 0 deletions packages/typescript/test/ts_project/b/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "../tsconfig-base.json",
"references": [
{"path": "../a"}
],
"exclude": ["*.spec.ts"]
}
Loading

0 comments on commit a378a72

Please sign in to comment.