├── .editorconfig ├── .gitignore ├── .vscode └── launch.json ├── LICENSE.txt ├── README.md ├── bin └── bazeltsc ├── build.sh ├── example ├── .gitignore ├── BUILD ├── WORKSPACE ├── package.json └── x.ts ├── package-lock.json ├── package.json ├── proto ├── README.md └── worker_protocol.proto ├── src └── bazeltsc.ts ├── tsc.bzl ├── tsconfig.json ├── tslint.json └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.ts] 2 | indent_style = space 3 | indent_size = 4 4 | 5 | [*.json] 6 | indent_style = space 7 | indent_size = 2 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /lib/ 3 | /dist/ 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceRoot}/target/bazel-tsc.js", 12 | "args": [ "--outFile", "x.js", "x.ts", "y.ts" ], 13 | "outFiles": ["${workspaceRoot}/target/*"] 14 | }, 15 | { 16 | "type": "node", 17 | "request": "launch", 18 | "name": "Launch with --debug", 19 | "program": "${workspaceRoot}/target/bazel-tsc.js", 20 | "args": [ "--debug" ], 21 | "outFiles": ["${workspaceRoot}/target/*"], 22 | "console": "integratedTerminal" 23 | }, 24 | { 25 | "type": "node", 26 | "request": "launch", 27 | "name": "Launch with --persistent_worker", 28 | "program": "${workspaceRoot}/target/bazel-tsc.js", 29 | "args": [ "--persistent_worker" ], 30 | "outFiles": ["${workspaceRoot}/target/*"], 31 | "console": "integratedTerminal" 32 | }, 33 | { 34 | "type": "node", 35 | "request": "attach", 36 | "name": "Attach to Port", 37 | "address": "localhost", 38 | "port": 5858, 39 | "outFiles": ["${workspaceRoot}/target/*"] 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Asana 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Persistent TypeScript compiler 2 | 3 | This is a TypeScript compiler that can be used as a Bazel "persistent worker." 4 | If it is launched with `--persistent_worker`, then it will run in a loop, 5 | reading compilation arguments (in protobuf format) from stdin; doing a compile; 6 | and then writing the results (also in protobuf format) to stdout. (The format 7 | is defined by Bazel.) 8 | 9 | For Bazel projects that do a lot of TypeScript compilation, this has two 10 | performance benefits: 11 | 12 | 1. It avoids compiler startup time. 13 | 2. It avoids unnecessarily re-parsing source files. Specifically, the runtime 14 | file, lib.d.ts, will only be read once; any any other .ts or .d.ts files 15 | will also only be read once. 16 | 17 | In our internal usage at Asana, using bazeltsc has led to roughly a 2x to 4x 18 | speedup in TypeScript compilation (the numbers are affected by a variety of 19 | factors). 20 | 21 | ## Installation 22 | 23 | You will need to get bazeltsc into a place where Bazel can find it. One way to 24 | do this is by using Bazel's 25 | [`rules_nodejs`](https://github.com/bazelbuild/rules_nodejs); but you can do it 26 | any way you like. 27 | 28 | Instructions if you are using `rules_nodejs`: 29 | 30 | * Follow the [`rules_nodejs`](https://github.com/bazelbuild/rules_nodejs) 31 | setup instructions. 32 | 33 | * Add `bazeltsc` to your `package.json`: 34 | 35 | npm install --save-dev bazeltsc 36 | 37 | * Run the Bazel equivalent of `npm install`: 38 | 39 | bazel run @nodejs//:npm install 40 | 41 | * Copy `tsc.bzl` from `node_modules/bazeltsc/tsc.bzl` into your repo 42 | somewhere. (We intend to clean this up as soon as we figure out how to get 43 | Bazel to be able to find `tsc.bzl` directly from inside `node_modules`.) 44 | 45 | * Use the example from the `example` directory -- especially the `WORKSPACE` 46 | and `BUILD` file -- to finish setting things up. 47 | 48 | * It is essential that when you run Bazel, you invoke it with 49 | `--strategy=TsCompile=worker`. This is what tells Bazel to use `bazeltsc` 50 | as a persistent worker instead of as a regular tool that is invoked once 51 | per compilation. 52 | 53 | You will probably want to add that to a `.bazelrc` file in the root directory 54 | of your project, so that you don't have to specify it on the command line: 55 | 56 | # This will be used every time someone does `bazel build ...`: 57 | build --strategy=TsCompile=worker 58 | 59 | ## Experimenting with bazeltsc 60 | 61 | Normally, you just let Bazel launch bazeltsc. But it is helpful to understand 62 | how bazeltsc is launched by Bazel, and how you can experiment with it yourself. 63 | 64 | bazeltsc can run in any of three modes: 65 | 66 | * **As a bazel persistent worker.** Bazel will launch it with this command line: 67 | 68 | bazeltsc --persistent_worker 69 | 70 | At that point, bazeltsc is reading protobuf-formatted compilation requests 71 | from stdin; compiling; and returning protobuf-formatted results on stdout. 72 | 73 | * **As a thin wrapper around tsc.** Examples: 74 | 75 | bazeltsc --outDir target foo.ts bar.ts 76 | bazeltsc @argfile 77 | 78 | This provides no functionality beyond what tsc itself provides; it is 79 | supported in case, for whatever reason, there are times when you want to 80 | tell Bazel not to use persistent workers. 81 | 82 | * **In "debug" mode.** If you want to experiment interactively with bazeltsc, 83 | run it like this: 84 | 85 | bazeltsc --debug 86 | 87 | Then, at the `>` prompt, enter a full tsc command line. This will let you 88 | see the speed difference between an initial compilation and a subsequent 89 | one. 90 | 91 | For example: 92 | 93 | bazeltsc --debug 94 | > x.ts 95 | Compilation took 890ms. Exit code: 0. 96 | > x.ts 97 | Compilation took 351ms. Exit code: 0. 98 | -------------------------------------------------------------------------------- /bin/bazeltsc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require("../dist/bazeltsc.js"); 3 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Run this with: "npm run build" 4 | 5 | set -o errexit -o pipefail 6 | 7 | if ! $(echo $PATH | tr : '\n' | grep '\/node_modules\/\.bin$' >/dev/null); then 8 | echo 'run this script via "npm run build"' 9 | exit 1 10 | fi 11 | 12 | rm -rf lib 13 | mkdir -p lib 14 | 15 | # input: proto/worker_protocol.proto 16 | # output: lib/worker_protocol_pb.{js,d.ts} 17 | protoc \ 18 | --plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts \ 19 | --js_out=import_style=commonjs,binary:lib \ 20 | --ts_out=service=true:lib \ 21 | -Iproto \ 22 | worker_protocol.proto 23 | 24 | # output: lib/bazeltsc.js 25 | tsc 26 | 27 | # output: dist/bazeltsc.js (final, single-file bundle) 28 | rm -rf dist 29 | mkdir dist 30 | webpack 31 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | bazel-* 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /example/BUILD: -------------------------------------------------------------------------------- 1 | package(default_visibility = ["//visibility:public"]) 2 | 3 | load("@build_bazel_rules_nodejs//:defs.bzl", "nodejs_binary") 4 | load(":tsc.bzl", "tsc") 5 | 6 | nodejs_binary( 7 | name = "bazeltsc", 8 | entry_point = "bazeltsc/bin/bazeltsc", 9 | ) 10 | 11 | # outputs are x.js, x.js.map, x.d.ts 12 | tsc( 13 | name = "x", 14 | srcs = ["x.ts"] 15 | ) 16 | -------------------------------------------------------------------------------- /example/WORKSPACE: -------------------------------------------------------------------------------- 1 | load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") 2 | 3 | git_repository( 4 | name = "build_bazel_rules_nodejs", 5 | remote = "https://github.com/bazelbuild/rules_nodejs.git", 6 | tag = "0.1.0", # check for the latest tag when you install 7 | ) 8 | 9 | load("@build_bazel_rules_nodejs//:defs.bzl", "node_repositories") 10 | 11 | # NOTE: this rule installs nodejs, npm, and yarn, but does NOT install 12 | # your npm dependencies. You must still run the package manager. 13 | node_repositories(package_json = ["//:package.json"]) 14 | 15 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "btsctest", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "Mike Morearty (https://github.com/mmorearty)", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "bazeltsc": "1.0.0", 14 | "typescript": "^2.6.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /example/x.ts: -------------------------------------------------------------------------------- 1 | export class X { 2 | foo(n: number) { console.log(`This is X.foo ${n}`); } 3 | } 4 | 5 | new X().foo(7); 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bazeltsc", 3 | "version": "2.0.0", 4 | "description": "A Bazel persistent worker for compiling TypeScript", 5 | "keywords": [ 6 | "compiler", 7 | "tsc", 8 | "javasript" 9 | ], 10 | "engines": { 11 | "npm": ">=4.0" 12 | }, 13 | "bin": { 14 | "bazeltsc": "bin/bazeltsc" 15 | }, 16 | "files": [ 17 | "bin/*", 18 | "dist/*", 19 | "tsc.bzl" 20 | ], 21 | "repository": "Asana/bazel-tsc", 22 | "scripts": { 23 | "build": "./build.sh", 24 | "prepare": "npm run build", 25 | "clean": "rm -rf node_modules lib dist", 26 | "test": "echo \"Error: no test specified\" && exit 1" 27 | }, 28 | "author": "Mike Morearty (https://github.com/mmorearty)", 29 | "license": "MIT", 30 | "peerDependencies": { 31 | "typescript": ">= 2.0.2" 32 | }, 33 | "devDependencies": { 34 | "@types/google-protobuf": "^3.2.7", 35 | "@types/minimist": "^1.2.0", 36 | "@types/node": "^10.5.5", 37 | "google-protobuf": "^3.6.1", 38 | "ts-protoc-gen": "^0.7.6", 39 | "tslint": "^5.11.0", 40 | "typescript": "^3.0.1", 41 | "webpack": "^4.16.4", 42 | "webpack-command": "^0.4.1" 43 | }, 44 | "dependencies": { 45 | "minimist": "^1.2.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /proto/README.md: -------------------------------------------------------------------------------- 1 | `worker_protocol_pb.js` was generated from Bazel's 2 | `src/main/protobuf/worker_protocol.proto` by following these steps: 3 | 4 | # Get the file (change `0.5.4` to a different tag if desired): 5 | BAZEL_VERSION=0.5.4 6 | curl -OL https://raw.githubusercontent.com/bazelbuild/bazel/$BAZEL_VERSION/src/main/protobuf/worker_protocol.proto 7 | 8 | # Get protoc and run it 9 | # Mac: brew install protobuf 10 | # Linux: See https://github.com/google/protobuf/#protocol-compiler-installation 11 | protoc --js_out=import_style=commonjs,binary:. worker_protocol.proto 12 | -------------------------------------------------------------------------------- /proto/worker_protocol.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Bazel Authors. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package blaze.worker; 18 | 19 | option java_package = "com.google.devtools.build.lib.worker"; 20 | 21 | // An input file. 22 | message Input { 23 | // The path in the file system where to read this input artifact from. This is 24 | // either a path relative to the execution root (the worker process is 25 | // launched with the working directory set to the execution root), or an 26 | // absolute path. 27 | string path = 1; 28 | 29 | // A hash-value of the contents. The format of the contents is unspecified and 30 | // the digest should be treated as an opaque token. 31 | bytes digest = 2; 32 | } 33 | 34 | // This represents a single work unit that Blaze sends to the worker. 35 | message WorkRequest { 36 | repeated string arguments = 1; 37 | 38 | // The inputs that the worker is allowed to read during execution of this 39 | // request. 40 | repeated Input inputs = 2; 41 | } 42 | 43 | // The worker sends this message to Blaze when it finished its work on the WorkRequest message. 44 | message WorkResponse { 45 | int32 exit_code = 1; 46 | 47 | // This is printed to the user after the WorkResponse has been received and is supposed to contain 48 | // compiler warnings / errors etc. - thus we'll use a string type here, which gives us UTF-8 49 | // encoding. 50 | string output = 2; 51 | } 52 | -------------------------------------------------------------------------------- /src/bazeltsc.ts: -------------------------------------------------------------------------------- 1 | // bazeltsc: Bazel TypeScript Compiler. 2 | 3 | // built-in node packages 4 | import * as path from "path"; 5 | import * as readline from "readline"; 6 | 7 | // imported npm packages 8 | import * as ts from "typescript"; 9 | import * as protobuf from "google-protobuf"; 10 | import * as minimist from "minimist"; 11 | 12 | // imports of our own code 13 | import * as worker from "../lib/worker_protocol_pb"; 14 | 15 | interface Settings extends minimist.ParsedArgs { 16 | max_compiles: number; // e.g. --max_compiles=10; 0 means no limit 17 | max_idle_seconds: number; // e.g. --max_idle_seconds=5; 0 means no limit 18 | persistent_worker?: boolean; // Bazel passes --persistent_worker 19 | debug?: boolean; // --debug lets you run this interactively 20 | } 21 | 22 | let settings: Settings = { 23 | max_compiles: 10, 24 | max_idle_seconds: 5, 25 | _: [] 26 | }; 27 | 28 | let parsedCommandLine: ts.ParsedCommandLine; 29 | 30 | const languageServiceHost: ts.LanguageServiceHost = { 31 | getCompilationSettings: (): ts.CompilerOptions => parsedCommandLine.options, 32 | getNewLine: () => ts.sys.newLine, 33 | getScriptFileNames: (): string[] => parsedCommandLine.fileNames, 34 | getScriptVersion: (fileName: string): string => { 35 | // If the file's size or modified-timestamp changed, it's a different version. 36 | return ts.sys.getFileSize(fileName) + ":" + ts.sys.getModifiedTime(fileName).getTime(); 37 | }, 38 | getScriptSnapshot: (fileName: string): ts.IScriptSnapshot | undefined => { 39 | if (!ts.sys.fileExists(fileName)) { 40 | return undefined; 41 | } 42 | let text = ts.sys.readFile(fileName); 43 | return { 44 | getText: (start: number, end: number) => { 45 | if (start === 0 && end === text.length) { // optimization 46 | return text; 47 | } else { 48 | return text.slice(start, end); 49 | } 50 | }, 51 | getLength: () => text.length, 52 | getChangeRange: (oldSnapshot: ts.IScriptSnapshot): ts.TextChangeRange | undefined => { 53 | const oldText = oldSnapshot.getText(0, oldSnapshot.getLength()); 54 | 55 | // Find the offset of the first char that differs between oldText and text 56 | let firstDiff = 0; 57 | while (firstDiff < oldText.length && 58 | firstDiff < text.length && 59 | text[firstDiff] === oldText[firstDiff]) { 60 | firstDiff++; 61 | } 62 | 63 | // Find the offset of the last char that differs between oldText and text 64 | let oldIndex = oldText.length; 65 | let newIndex = text.length; 66 | while (oldIndex > firstDiff && 67 | newIndex > firstDiff && 68 | oldText[oldIndex - 1] === text[newIndex - 1]) { 69 | oldIndex--; 70 | newIndex--; 71 | } 72 | 73 | return { 74 | span: { 75 | start: firstDiff, 76 | length: oldIndex - firstDiff 77 | }, 78 | newLength: newIndex - firstDiff 79 | }; 80 | }, 81 | dispose: () => { text = ""; } 82 | }; 83 | }, 84 | getCurrentDirectory: ts.sys.getCurrentDirectory, 85 | getDefaultLibFileName: ts.getDefaultLibFilePath 86 | }; 87 | 88 | const formatDiagnosticsHost: ts.FormatDiagnosticsHost = { 89 | getCurrentDirectory: ts.sys.getCurrentDirectory, 90 | getCanonicalFileName: (fileName: string) => fileName, 91 | getNewLine: () => ts.sys.newLine 92 | }; 93 | 94 | class LanguageServiceProvider { 95 | private _languageService: ts.LanguageService; 96 | private _count = 0; 97 | private _idleTimer: NodeJS.Timer; 98 | 99 | // Every N compiles, this will release the current TypeScript LanguageService, create a new 100 | // one, and do a gc(). N defaults to 10, and is overridden with --max_compiles=N 101 | acquire() { 102 | this._clearIdleTimeout(); 103 | if (!this._languageService) { 104 | this._languageService = ts.createLanguageService(languageServiceHost); 105 | this._count = 0; 106 | } 107 | 108 | ++this._count; 109 | return this._languageService; 110 | } 111 | 112 | release() { 113 | if (this._languageService) { 114 | if (this._count >= settings.max_compiles) { 115 | // We've hit our limit on how many compiles we will do with a single 116 | // LanguageService. So free up the current one and do a gc(). 117 | this._freeMemory(); 118 | } else { 119 | // If Bazel doesn't send any more work requests for a while, we will 120 | // free memory. 121 | this._setIdleTimeout(); 122 | } 123 | } 124 | } 125 | 126 | private _setIdleTimeout() { 127 | if (settings.max_idle_seconds) { 128 | this._idleTimer = setTimeout(this._onIdle, settings.max_idle_seconds * 1000); 129 | } 130 | } 131 | 132 | private _clearIdleTimeout() { 133 | if (this._idleTimer) { 134 | clearTimeout(this._idleTimer); 135 | this._idleTimer = null; 136 | } 137 | } 138 | 139 | private _onIdle = () => { 140 | this._idleTimer = null; 141 | this._freeMemory(); 142 | }; 143 | 144 | private _freeMemory() { 145 | this._languageService = null; 146 | if (global.gc) { 147 | global.gc(); 148 | } 149 | } 150 | } 151 | 152 | const languageServiceProvider = new LanguageServiceProvider(); 153 | 154 | function fileNotFoundDiagnostic(filename: string): ts.Diagnostic { 155 | return { 156 | file: undefined, 157 | start: undefined, 158 | length: undefined, 159 | messageText: `File '${filename}' not found.`, // ugh, hard-coded 160 | category: ts.DiagnosticCategory.Error, 161 | code: 6053 // ugh, hard-coded 162 | }; 163 | } 164 | 165 | // Returns an array of zero or more diagnostic messages, one for each file that does not exist. 166 | // I would really prefer to let the TypeScript compiler take care of this, but I couldn't find a way. 167 | function ensureRootFilesExist(filenames: string[]): ts.Diagnostic[] { 168 | return filenames 169 | .filter(filename => !ts.sys.fileExists(filename)) 170 | .map(filename => fileNotFoundDiagnostic(filename)); 171 | } 172 | 173 | // We need to mimic the behavior of tsc: Read arguments from the command line, but also, if none 174 | // were specified or if a project was specified with "--project" / "-p", then read tsconfig.json. 175 | function parseCommandLine(args: string[]): ts.ParsedCommandLine { 176 | let pcl = ts.parseCommandLine(args); 177 | 178 | // If there are no command line arguments, or if the user specified "--project" / "-p", then 179 | // we need to read tsconfig.json 180 | const configFileOrDirectory = (args.length > 0) 181 | ? pcl.options.project 182 | : "."; 183 | 184 | if (configFileOrDirectory) { 185 | if (pcl.fileNames.length > 0) { 186 | pcl.errors.push({ 187 | file: undefined, 188 | start: undefined, 189 | length: undefined, 190 | messageText: "Option 'project' cannot be mixed with source files on a command line.", 191 | category: ts.DiagnosticCategory.Error, 192 | code: 5042 // ugh, hard-coded 193 | }); 194 | return pcl; 195 | } 196 | 197 | const configFileName: string = ts.sys.directoryExists(configFileOrDirectory) 198 | ? path.join(configFileOrDirectory, "tsconfig.json") 199 | : configFileOrDirectory; 200 | 201 | if (!ts.sys.fileExists(configFileName)) { 202 | pcl.errors.push( fileNotFoundDiagnostic(configFileName) ); 203 | return pcl; 204 | } 205 | 206 | const parseConfigFileHost: ts.ParseConfigFileHost = { 207 | useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames, 208 | readDirectory: ts.sys.readDirectory, 209 | fileExists: ts.sys.fileExists, 210 | readFile: ts.sys.readFile, 211 | getCurrentDirectory: ts.sys.getCurrentDirectory, 212 | onUnRecoverableConfigFileDiagnostic: diagnostic => { 213 | pcl.errors.push(diagnostic); 214 | } 215 | }; 216 | 217 | // Read and parse tsconfig.json, merging it with any other args from command line 218 | pcl = ts.getParsedCommandLineOfConfigFile( 219 | configFileName, 220 | pcl.options, 221 | parseConfigFileHost 222 | ); 223 | } 224 | 225 | return pcl; 226 | } 227 | 228 | function compile(args: string[]): { exitCode: number, output: string } { 229 | let exitCode = ts.ExitStatus.DiagnosticsPresent_OutputsSkipped; 230 | let output = ""; 231 | 232 | try { 233 | let emitSkipped = true; 234 | const diagnostics: ts.Diagnostic[] = []; 235 | 236 | parsedCommandLine = parseCommandLine(args); 237 | 238 | if (parsedCommandLine.errors.length > 0) { 239 | diagnostics.push(...parsedCommandLine.errors); 240 | } else if (parsedCommandLine.options.version) { 241 | output += `Version ${ts.version}\n`; 242 | } else { 243 | diagnostics.push(...ensureRootFilesExist(parsedCommandLine.fileNames)); 244 | if (diagnostics.length === 0) { 245 | let languageService: ts.LanguageService; 246 | try { 247 | languageService = languageServiceProvider.acquire(); 248 | // This call to languageService.getProgram() will clear out any modified SourceFiles 249 | // from the compiler's cache. 250 | const program = languageService.getProgram(); 251 | diagnostics.push(...ts.getPreEmitDiagnostics(program)); 252 | if (diagnostics.length === 0) { 253 | // We would like to use the TypeScript library's built-in writeFile function; 254 | // the only way to get access to it is to call createCompilerHost(). 255 | const compilerHost = ts.createCompilerHost(parsedCommandLine.options); 256 | 257 | // Write the compiled files to disk! 258 | const emitOutput = program.emit(undefined /* all files */, compilerHost.writeFile); 259 | 260 | diagnostics.push(...emitOutput.diagnostics); 261 | emitSkipped = emitOutput.emitSkipped; 262 | } 263 | } finally { 264 | languageServiceProvider.release(); 265 | } 266 | } 267 | } 268 | 269 | output += ts.formatDiagnostics(diagnostics, formatDiagnosticsHost); 270 | 271 | if (emitSkipped) { 272 | exitCode = ts.ExitStatus.DiagnosticsPresent_OutputsSkipped; 273 | } else if (diagnostics.length > 0) { 274 | exitCode = ts.ExitStatus.DiagnosticsPresent_OutputsGenerated; 275 | } else { 276 | exitCode = ts.ExitStatus.Success; 277 | } 278 | } catch (e) { 279 | exitCode = ts.ExitStatus.DiagnosticsPresent_OutputsSkipped; 280 | output = "" + e.stack; 281 | } 282 | return { exitCode, output }; 283 | } 284 | 285 | // Reads an unsigned varint32 from a protobuf-formatted array 286 | function readUnsignedVarint32(a: { [index: number]: number }): { 287 | value: number, // the value of the varint32 that was read 288 | length: number // the number of bytes that the encoded varint32 took 289 | } { 290 | let b: number; 291 | let result = 0; 292 | let offset = 0; 293 | 294 | // Each byte has 7 bits of data, plus a high bit which indicates whether 295 | // the value continues over into the next byte. A 32-bit value will never 296 | // have more than 5 bytes of data (because 5 * 7 is obviously more than 297 | // enough bits). 298 | for (let i = 0; i < 5; i++) { 299 | b = a[offset++]; 300 | result |= (b & 0x7F) << (7 * i); 301 | if (!(b & 0x80)) { 302 | break; 303 | } 304 | } 305 | 306 | return { value: result, length: offset }; 307 | } 308 | 309 | function persistentWorker(exit: (exitCode: number) => void) { 310 | let data: Buffer = null; 311 | let dataOffset = 0; 312 | 313 | process.stdin.on("data", (chunk: Buffer) => { 314 | if (data === null) { 315 | // This is the first chunk of data for a new WorkRequest. Each incoming 316 | // WorkRequest is preceded by its length, encoded as a Protobuf varint32. 317 | // (Bazel calls Protobuf's writeDelimitedTo -- the Java source is here: 318 | // http://goo.gl/4udNmR). 319 | const varint = readUnsignedVarint32(chunk); 320 | const messageSize = varint.value; 321 | if (messageSize <= chunk.length - varint.length) { 322 | // `chunk` contains the entire message, so just point into it 323 | data = chunk.slice(varint.length, varint.length + varint.value); 324 | dataOffset = data.length; 325 | } else { 326 | // `chunk` contains only the first part of the message, so 327 | // allocate a new Buffer which will hold the entire message 328 | data = Buffer.allocUnsafe(messageSize); 329 | dataOffset = chunk.copy(data, 0, varint.length); 330 | } 331 | } else { 332 | // This is an additional chunk of data for a WorkRequest whose first 333 | // part already came in earlier; just append it 334 | dataOffset += chunk.copy(data, dataOffset); 335 | } 336 | 337 | // Keep reading chunks of data until we have read the entire WorkRequest 338 | if (dataOffset < data.length) { 339 | return; 340 | } 341 | 342 | // Turn the buffer into a Bazel WorkRequest 343 | const reader = new protobuf.BinaryReader(new Uint8Array(data)); 344 | const workRequest = new worker.WorkRequest(); 345 | worker.WorkRequest.deserializeBinaryFromReader(workRequest, reader); 346 | 347 | data = null; 348 | dataOffset = 0; 349 | 350 | // Run the compiler 351 | const args = workRequest.getArgumentsList(); 352 | const { exitCode, output } = compile(args); 353 | 354 | // Turn the result into a Bazel WorkResponse, and send it protobuf-encoded to stdout 355 | const workResponse = new worker.WorkResponse(); 356 | workResponse.setExitCode(exitCode); 357 | workResponse.setOutput(output); 358 | 359 | const workResponseBytes: Uint8Array = workResponse.serializeBinary(); 360 | 361 | // Bazel wants the response to be preceded by a varint-encoded length 362 | const writer = new protobuf.BinaryWriter(); 363 | // TODO this is not cool, encoder_ is an undocumented internal property. But I haven't 364 | // found another way to do what I want here. 365 | const encoder_ = (writer as any).encoder_; 366 | encoder_.writeUnsignedVarint32(workResponseBytes.length); 367 | const lengthArray: any = encoder_.end(); // array 368 | 369 | const buffer: Buffer = new Buffer(lengthArray.length + workResponseBytes.length); 370 | buffer.set(lengthArray); // the varint-encoded length... 371 | buffer.set(workResponseBytes, lengthArray.length); // ...followed by the WorkResponse 372 | process.stdout.write(new Buffer(buffer)); 373 | }); 374 | 375 | process.stdin.on("end", () => { 376 | exit(0); 377 | }); 378 | } 379 | 380 | function main(args: string[], exit: (exitCode: number) => void) { 381 | settings = { 382 | ...settings, 383 | ...minimist(args) 384 | }; 385 | 386 | // This is the flag that Bazel passes when it wants us to remain in memory as a persistent worker, 387 | // communicating with Bazel via protobuf over stdin/stdout. 388 | if (settings["persistent_worker"]) { 389 | persistentWorker(exit); 390 | } else if (settings["debug"]) { 391 | // Read regular text (not protobuf) from stdin; print to stdout 392 | const rl = readline.createInterface({ 393 | input: process.stdin, 394 | output: process.stdout 395 | }); 396 | rl.prompt(); 397 | 398 | rl.on("line", (input: string) => { 399 | const startTime = process.hrtime(); 400 | 401 | const cmdArgs = input.split(" "); 402 | const { exitCode, output } = compile(cmdArgs); 403 | 404 | const elapsedTime = process.hrtime(startTime); 405 | const elapsedMillis = Math.floor((elapsedTime[0] * 1e9 + elapsedTime[1]) / 1e6); 406 | let message = `Compilation took ${elapsedMillis}ms. Exit code: ${exitCode}.`; 407 | if (output) { 408 | message += ` Compiler output:\n${output}`; 409 | } else { 410 | message += "\n"; 411 | } 412 | process.stdout.write(message); 413 | rl.prompt(); 414 | }); 415 | rl.on("close", () => { 416 | exit(0); 417 | }); 418 | } else { // not --persistent_worker nor --debug; just a regular compile 419 | const { exitCode, output } = compile(args); 420 | if (output) { 421 | process.stdout.write(output); 422 | } 423 | exit(exitCode); 424 | } 425 | } 426 | 427 | main(ts.sys.args, ts.sys.exit); 428 | -------------------------------------------------------------------------------- /tsc.bzl: -------------------------------------------------------------------------------- 1 | def _tsc_impl(ctx): 2 | # Generate the "@"-file containing the command-line args for the unit of work. 3 | tsc_arguments = [ 4 | ## Modify these args as desired for your project! 5 | "--outFile", ctx.outputs.js.path, 6 | "--module", "amd", 7 | "--declaration", # generate .d.ts file 8 | "--sourceMap", # generate .js.map file 9 | ] + [ 10 | src.path for src in ctx.files.srcs 11 | ] 12 | argfile = ctx.new_file(ctx.configuration.bin_dir, ctx.label.name + ".params") 13 | ctx.file_action( 14 | output = argfile, 15 | content = "".join([arg + "\n" for arg in tsc_arguments]) 16 | ) 17 | ctx.action( 18 | executable = ctx.executable._bazeltsc, 19 | arguments = [ 20 | "@" + argfile.path # this must be the last argument 21 | ], 22 | inputs = ctx.files.srcs + [argfile], 23 | outputs = [ctx.outputs.js, ctx.outputs.dts, ctx.outputs.map], 24 | mnemonic = "TsCompile", 25 | progress_message = "Compile TypeScript {}".format(ctx.label), 26 | execution_requirements = { "supports-workers": "1" }, 27 | ) 28 | 29 | tsc = rule( 30 | implementation = _tsc_impl, 31 | attrs = { 32 | "srcs": attr.label_list( 33 | mandatory = True, 34 | allow_files = True 35 | ), 36 | "_bazeltsc": attr.label( 37 | default = Label("//:bazeltsc"), 38 | executable = True, 39 | cfg = "host" 40 | ), 41 | }, 42 | outputs = { 43 | "js": "%{name}.js", 44 | "map": "%{name}.js.map", 45 | "dts": "%{name}.d.ts", 46 | } 47 | ) 48 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "src", 4 | "outDir": "lib", 5 | "types": ["node"], 6 | "allowJs": true, 7 | "noImplicitAny": true, 8 | "noImplicitReturns": true, 9 | "noImplicitThis": true, 10 | "noUnusedLocals": true 11 | }, 12 | "include": [ 13 | "src/**.ts", 14 | "src/**.js" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "ordered-imports": false, 9 | "object-literal-sort-keys": false, 10 | "trailing-comma": false, 11 | "arrow-parens": false, 12 | "no-bitwise": false 13 | }, 14 | "rulesDirectory": [] 15 | } 16 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | module.exports = { 4 | entry: './lib/bazeltsc.js', 5 | output: { 6 | filename: 'bazeltsc.js' 7 | }, 8 | target: 'node', 9 | externals: { 10 | // This handles require("typescript") from a file that is inside Bazel's 11 | // sandbox. 12 | // 13 | // This is complicated. We want typescript to be a "peer dependency" -- not 14 | // bundled, but rather, located dynamically at runtime (in order to allow 15 | // people who are using bazeltsc to use it with any version of TypeScript). 16 | // But if we just use a regular import or require() from this file, then we 17 | // will be potentially bypassing Bazel's sandboxing, by starting at this 18 | // file's location and searching up from there for node_modules/typescript 19 | // directory. Instead, we want to start at Bazel's execution_root (see 20 | // `bazel info execution_root`). 21 | // 22 | // The safest way we have found to do that is to dynamically write a short 23 | // JavaScript file to the process.cwd() directory (which is the 24 | // execution_root), and let _that_ file call require(). 25 | 'typescript': `(function() { 26 | const fs = require("fs"); 27 | const path = require("path"); 28 | const crypto = require("crypto"); 29 | const pseudorand = crypto.randomBytes(4).readUInt32LE(0); 30 | const typescriptProxy = path.join(process.cwd(), "typescript_proxy_" + pseudorand + ".js"); 31 | fs.writeFileSync(typescriptProxy, 'module.exports = require("typescript");'); 32 | const typescript = require(typescriptProxy); 33 | fs.unlinkSync(typescriptProxy); 34 | return typescript; 35 | })()` 36 | } 37 | } 38 | --------------------------------------------------------------------------------