Skip to content

Commit 9663b85

Browse files
mrmekuAlex EagleDan Muller
authored
feat(typescript): worker mode for ts_project (#2136)
* feat(typescript): worker mode for ts_project * chore: cleanup and declare protobufjs dependency * fix: do not pass --watch for non-worker mode * fix: do not test worker on windows * fix: log on standalone failures * chore: docs improvements Co-authored-by: Alex Eagle <[email protected]> Co-authored-by: Dan Muller <[email protected]>
1 parent 19272ef commit 9663b85

File tree

11 files changed

+292
-8
lines changed

11 files changed

+292
-8
lines changed

examples/react_webpack/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ sass(
1414
)
1515

1616
ts_project(
17+
# Experimental: Start a tsc daemon to watch for changes to make recompiles faster.
18+
supports_workers = True,
1719
deps = [
1820
"@npm//@types",
1921
"@npm//csstype",

examples/react_webpack/tsconfig.json

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
{
22
"compilerOptions": {
33
"jsx": "react",
4-
"lib": ["ES2015", "DOM"]
5-
}
6-
}
4+
"lib": [
5+
"ES2015",
6+
"DOM"
7+
]
8+
},
9+
// When using ts_project in worker mode, we run outside the Bazel sandbox (unless using --worker_sandboxing).
10+
// We list the files that should be part of this particular compilation to avoid TypeScript discovering others.
11+
"include": [
12+
"*.tsx",
13+
"*.ts"
14+
]
15+
}

internal/node/node.bzl

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def _trim_package_node_modules(package_name):
3737
for n in package_name.split("/"):
3838
if n == "node_modules":
3939
break
40-
segments += [n]
40+
segments.append(n)
4141
return "/".join(segments)
4242

4343
def _compute_node_modules_root(ctx):
@@ -150,6 +150,9 @@ def _to_execroot_path(ctx, file):
150150

151151
return file.path
152152

153+
def _join(*elements):
154+
return "/".join([f for f in elements if f])
155+
153156
def _nodejs_binary_impl(ctx):
154157
node_modules_manifest = write_node_modules_manifest(ctx, link_workspace_root = ctx.attr.link_workspace_root)
155158
node_modules_depsets = []
@@ -250,7 +253,12 @@ fi
250253
expanded_args = [expand_location_into_runfiles(ctx, a, ctx.attr.data) for a in expanded_args]
251254

252255
# Next expand predefined variables & custom variables
253-
expanded_args = [ctx.expand_make_variables("templated_args", e, {}) for e in expanded_args]
256+
rule_dir = _join(ctx.bin_dir.path, ctx.label.workspace_root, ctx.label.package)
257+
additional_substitutions = {
258+
"@D": rule_dir,
259+
"RULEDIR": rule_dir,
260+
}
261+
expanded_args = [ctx.expand_make_variables("templated_args", e, additional_substitutions) for e in expanded_args]
254262

255263
substitutions = {
256264
# TODO: Split up results of multifile expansions into separate args and qoute them with

packages/typescript/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ pkg_npm(
100100
":npm_version_check",
101101
"//packages/typescript/internal:BUILD",
102102
"//packages/typescript/internal:ts_project_options_validator.js",
103+
"//packages/typescript/internal/worker",
103104
] + select({
104105
# FIXME: fix stardoc on Windows; //packages/typescript:index.md generation fails with:
105106
# ERROR: D:/b/62unjjin/external/npm_bazel_typescript/BUILD.bazel:36:1: Couldn't build file

packages/typescript/internal/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ filegroup(
6464
"ts_config.bzl",
6565
"ts_project.bzl",
6666
"//packages/typescript/internal/devserver:package_contents",
67+
"//packages/typescript/internal/worker:package_contents",
6768
],
6869
visibility = ["//packages/typescript:__subpackages__"],
6970
)

packages/typescript/internal/ts_project.bzl

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
load("@build_bazel_rules_nodejs//:providers.bzl", "DeclarationInfo", "NpmPackageInfo", "declaration_info", "js_module_info", "run_node")
44
load("@build_bazel_rules_nodejs//internal/linker:link_node_modules.bzl", "module_mappings_aspect")
5+
load("@build_bazel_rules_nodejs//internal/node:node.bzl", "nodejs_binary")
56
load(":ts_config.bzl", "TsConfigInfo", "write_tsconfig")
67

78
_ValidOptionsInfo = provider()
@@ -13,6 +14,20 @@ _DEFAULT_TSC = (
1314
"//typescript/bin:tsc"
1415
)
1516

17+
_DEFAULT_TSC_BIN = (
18+
# BEGIN-INTERNAL
19+
"@npm" +
20+
# END-INTERNAL
21+
"//:node_modules/typescript/bin/tsc"
22+
)
23+
24+
_DEFAULT_TYPESCRIPT_MODULE = (
25+
# BEGIN-INTERNAL
26+
"@npm" +
27+
# END-INTERNAL
28+
"//typescript"
29+
)
30+
1631
_ATTRS = {
1732
"args": attr.string_list(),
1833
"declaration_dir": attr.string(),
@@ -33,7 +48,14 @@ _ATTRS = {
3348
# if you swap out the `compiler` attribute (like with ngtsc)
3449
# that compiler might allow more sources than tsc does.
3550
"srcs": attr.label_list(allow_files = True, mandatory = True),
36-
"tsc": attr.label(default = Label(_DEFAULT_TSC), executable = True, cfg = "host"),
51+
"supports_workers": attr.bool(
52+
doc = """Experimental! Use only with caution.
53+
54+
Allows you to enable the Bazel Worker strategy for this project.
55+
This requires that the tsc binary support it.""",
56+
default = False,
57+
),
58+
"tsc": attr.label(default = Label(_DEFAULT_TSC), executable = True, cfg = "target"),
3759
"tsconfig": attr.label(mandatory = True, allow_single_file = [".json"]),
3860
}
3961

@@ -56,6 +78,16 @@ def _join(*elements):
5678

5779
def _ts_project_impl(ctx):
5880
arguments = ctx.actions.args()
81+
execution_requirements = {}
82+
progress_prefix = "Compiling TypeScript project"
83+
84+
if ctx.attr.supports_workers:
85+
# Set to use a multiline param-file for worker mode
86+
arguments.use_param_file("@%s", use_always = True)
87+
arguments.set_param_file_format("multiline")
88+
execution_requirements["supports-workers"] = "1"
89+
execution_requirements["worker-key-mnemonic"] = "TsProject"
90+
progress_prefix = "Compiling TypeScript project (worker mode)"
5991

6092
generated_srcs = False
6193
for src in ctx.files.srcs:
@@ -162,7 +194,9 @@ def _ts_project_impl(ctx):
162194
arguments = [arguments],
163195
outputs = outputs,
164196
executable = "tsc",
165-
progress_message = "Compiling TypeScript project %s [tsc -p %s]" % (
197+
execution_requirements = execution_requirements,
198+
progress_message = "%s %s [tsc -p %s]" % (
199+
progress_prefix,
166200
ctx.label,
167201
ctx.file.tsconfig.short_path,
168202
),
@@ -287,7 +321,10 @@ def ts_project_macro(
287321
emit_declaration_only = False,
288322
ts_build_info_file = None,
289323
tsc = None,
324+
worker_tsc_bin = _DEFAULT_TSC_BIN,
325+
worker_typescript_module = _DEFAULT_TYPESCRIPT_MODULE,
290326
validate = True,
327+
supports_workers = False,
291328
declaration_dir = None,
292329
out_dir = None,
293330
root_dir = None,
@@ -453,8 +490,28 @@ def ts_project_macro(
453490
For example, `tsc = "@my_deps//typescript/bin:tsc"`
454491
Or you can pass a custom compiler binary instead.
455492
493+
worker_tsc_bin: Label of the TypeScript compiler binary to run when running in worker mode.
494+
495+
For example, `tsc = "@my_deps//node_modules/typescript/bin/tsc"`
496+
Or you can pass a custom compiler binary instead.
497+
498+
worker_typescript_module: Label of the package containing all data deps of worker_tsc_bin.
499+
500+
For example, `tsc = "@my_deps//typescript"`
501+
456502
validate: boolean; whether to check that the tsconfig settings match the attributes.
457503
504+
supports_workers: Experimental! Use only with caution.
505+
506+
Allows you to enable the Bazel Persistent Workers strategy for this project.
507+
See https://docs.bazel.build/versions/master/persistent-workers.html
508+
509+
This requires that the tsc binary support a `--watch` option.
510+
511+
NOTE: this does not work on Windows yet.
512+
We will silently fallback to non-worker mode on Windows regardless of the value of this attribute.
513+
Follow https://github.com/bazelbuild/rules_nodejs/issues/2277 for progress on this feature.
514+
458515
root_dir: a string specifying a subdirectory under the input package which should be consider the
459516
root directory of all the input files.
460517
Equivalent to the TypeScript --rootDir option.
@@ -559,6 +616,38 @@ def ts_project_macro(
559616
)
560617
extra_deps.append("_validate_%s_options" % name)
561618

619+
if supports_workers:
620+
tsc_worker = "%s_worker" % name
621+
protobufjs = (
622+
# BEGIN-INTERNAL
623+
"@npm" +
624+
# END-INTERNAL
625+
"//protobufjs"
626+
)
627+
nodejs_binary(
628+
name = tsc_worker,
629+
data = [
630+
Label("//packages/typescript/internal/worker:worker"),
631+
Label(worker_tsc_bin),
632+
Label(worker_typescript_module),
633+
Label(protobufjs),
634+
tsconfig,
635+
],
636+
entry_point = Label("//packages/typescript/internal/worker:worker_adapter"),
637+
templated_args = [
638+
"--nobazel_patch_module_resolver",
639+
"$(execpath {})".format(Label(worker_tsc_bin)),
640+
"--project",
641+
"$(execpath {})".format(tsconfig),
642+
# FIXME: should take out_dir into account
643+
"--outDir",
644+
"$(RULEDIR)",
645+
# FIXME: what about other settings like declaration_dir, root_dir, etc
646+
],
647+
)
648+
649+
tsc = ":" + tsc_worker
650+
562651
typings_out_dir = declaration_dir if declaration_dir else out_dir
563652
tsbuildinfo_path = ts_build_info_file if ts_build_info_file else name + ".tsbuildinfo"
564653

@@ -583,5 +672,9 @@ def ts_project_macro(
583672
buildinfo_out = tsbuildinfo_path if composite or incremental else None,
584673
tsc = tsc,
585674
link_workspace_root = link_workspace_root,
675+
supports_workers = select({
676+
"@bazel_tools//src/conditions:host_windows": False,
677+
"//conditions:default": supports_workers,
678+
}),
586679
**kwargs
587680
)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# BEGIN-INTERNAL
2+
3+
load("//internal/common:copy_to_bin.bzl", "copy_to_bin")
4+
load("//third_party/github.com/bazelbuild/bazel-skylib:rules/copy_file.bzl", "copy_file")
5+
6+
# Copy the proto file to a matching third_party/... nested directory
7+
# so the runtime require() statements still work
8+
_worker_proto_dir = "third_party/github.com/bazelbuild/bazel/src/main/protobuf"
9+
10+
genrule(
11+
name = "copy_worker_js",
12+
srcs = ["//packages/worker:npm_package"],
13+
outs = ["worker.js"],
14+
cmd = "cp $(execpath //packages/worker:npm_package)/index.js $@",
15+
visibility = ["//visibility:public"],
16+
)
17+
18+
copy_file(
19+
name = "copy_worker_proto",
20+
src = "@build_bazel_rules_typescript//%s:worker_protocol.proto" % _worker_proto_dir,
21+
out = "%s/worker_protocol.proto" % _worker_proto_dir,
22+
visibility = ["//visibility:public"],
23+
)
24+
25+
copy_to_bin(
26+
name = "worker_adapter",
27+
srcs = [
28+
"worker_adapter.js",
29+
],
30+
visibility = ["//visibility:public"],
31+
)
32+
33+
filegroup(
34+
name = "package_contents",
35+
srcs = [
36+
"BUILD.bazel",
37+
],
38+
visibility = ["//packages/typescript:__subpackages__"],
39+
)
40+
41+
# END-INTERNAL
42+
43+
exports_files([
44+
"worker_adapter.js",
45+
])
46+
47+
filegroup(
48+
name = "worker",
49+
srcs = [
50+
"third_party/github.com/bazelbuild/bazel/src/main/protobuf/worker_protocol.proto",
51+
"worker.js",
52+
"worker_adapter.js",
53+
],
54+
visibility = ["//visibility:public"],
55+
)
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/**
2+
* @fileoverview wrapper program around the TypeScript compiler, tsc
3+
*
4+
* It intercepts the Bazel Persistent Worker protocol, using it to remote-control tsc running as a
5+
* child process. In between builds, the tsc process is stopped (akin to ctrl-z in a shell) and then
6+
* resumed (akin to `fg`) when the inputs have changed.
7+
*
8+
* See https://medium.com/@mmorearty/how-to-create-a-persistent-worker-for-bazel-7738bba2cabb
9+
* for more background (note, that is documenting a different implementation)
10+
*/
11+
const child_process = require('child_process');
12+
const MNEMONIC = 'TsProject';
13+
const worker = require('./worker');
14+
15+
const workerArg = process.argv.indexOf('--persistent_worker')
16+
if (workerArg > 0) {
17+
process.argv.splice(workerArg, 1, '--watch')
18+
19+
if (process.platform !== 'linux' && process.platform !== 'darwin') {
20+
throw new Error(`Worker mode is only supported on linux and darwin, not ${process.platform}.
21+
See https://github.com/bazelbuild/rules_nodejs/issues/2277`);
22+
}
23+
}
24+
25+
const [tscBin, ...tscArgs] = process.argv.slice(2);
26+
27+
const child = child_process.spawn(
28+
tscBin,
29+
tscArgs,
30+
{stdio: 'pipe'},
31+
);
32+
function awaitOneBuild() {
33+
child.kill('SIGCONT')
34+
35+
let buffer = [];
36+
return new Promise((res) => {
37+
function awaitBuild(s) {
38+
buffer.push(s);
39+
40+
if (s.includes('Watching for file changes.')) {
41+
child.kill('SIGSTOP')
42+
43+
const success = s.includes('Found 0 errors.');
44+
res(success);
45+
46+
child.stdout.removeListener('data', awaitBuild);
47+
48+
if (!success) {
49+
console.error(
50+
`\nError output from tsc worker:\n\n ${
51+
buffer.slice(1).map(s => s.toString()).join('').replace(/\n/g, '\n ')}`,
52+
)
53+
}
54+
55+
buffer = [];
56+
}
57+
};
58+
child.stdout.on('data', awaitBuild);
59+
});
60+
}
61+
62+
async function main() {
63+
// Bazel will pass a special argument to the program when it's running us as a worker
64+
if (workerArg > 0) {
65+
worker.log(`Running ${MNEMONIC} as a Bazel worker`);
66+
67+
worker.runWorkerLoop(awaitOneBuild);
68+
} else {
69+
// Running standalone so stdout is available as usual
70+
console.log(`Running ${MNEMONIC} as a standalone process`);
71+
console.error(
72+
`Started a new process to perform this action. Your build might be misconfigured, try
73+
--strategy=${MNEMONIC}=worker`);
74+
75+
const stdoutbuffer = [];
76+
child.stdout.on('data', data => stdoutbuffer.push(data));
77+
78+
const stderrbuffer = [];
79+
child.stderr.on('data', data => stderrbuffer.push(data));
80+
81+
child.on('exit', code => {
82+
if (code !== 0) {
83+
console.error(
84+
`\nstdout from tsc:\n\n ${
85+
stdoutbuffer.map(s => s.toString()).join('').replace(/\n/g, '\n ')}`,
86+
)
87+
console.error(
88+
`\nstderr from tsc:\n\n ${
89+
stderrbuffer.map(s => s.toString()).join('').replace(/\n/g, '\n ')}`,
90+
)
91+
}
92+
process.exit(code)
93+
});
94+
}
95+
}
96+
97+
if (require.main === module) {
98+
main();
99+
}

0 commit comments

Comments
 (0)