@bazel/typescript
Version:
TypeScript rules for Bazel
201 lines (200 loc) • 9.84 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __asyncValues = (this && this.__asyncValues) || function (o) {
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
var m = o[Symbol.asyncIterator], i;
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
};
Object.defineProperty(exports, "__esModule", { value: true });
const path = require("path");
const protobufjs = require("protobufjs");
// Equivalent of running node with --expose-gc
// but easier to write tooling since we don't need to inject that arg to
// nodejs_binary
if (typeof global.gc !== 'function') {
// tslint:disable-next-line:no-require-imports
require('v8').setFlagsFromString('--expose_gc');
// tslint:disable-next-line:no-require-imports
global.gc = require('vm').runInNewContext('gc');
}
/**
* Whether to print debug messages (to console.error) from the debug function
* below.
*/
exports.DEBUG = false;
/** Maybe print a debug message (depending on a flag defaulting to false). */
function debug(...args) {
if (exports.DEBUG)
console.error.call(console, ...args);
}
exports.debug = debug;
/**
* Write a message to stderr, which appears in the bazel log and is visible to
* the end user.
*/
function log(...args) {
console.error.call(console, ...args);
}
exports.log = log;
/**
* runAsWorker returns true if the given arguments indicate the process should
* run as a persistent worker.
*/
function runAsWorker(args) {
return args.indexOf('--persistent_worker') !== -1;
}
exports.runAsWorker = runAsWorker;
/**
* loadWorkerPb finds and loads the protocol buffer definition for bazel's
* worker protocol using protobufjs. In protobufjs, this means it's a reflection
* object that also contains properties for the individual messages.
*/
function loadWorkerPb() {
const protoPath = './third_party/github.com/bazelbuild/bazel/src/main/protobuf/worker_protocol.proto';
// Use node module resolution so we can find the .proto file in any of the
// root dirs
let protofile;
try {
// Look for the .proto file relative in its @bazel/typescript npm package
// location
protofile = require.resolve(protoPath);
}
catch (e) {
}
if (!protofile) {
// If not found above, look for the .proto file in its rules_typescript
// workspace location
// This extra lookup should never happen in google3. It's only needed for
// local development in the rules_typescript repo.
const runfiles = process.env['BAZEL_NODE_RUNFILES_HELPER'];
if (runfiles) {
protofile = require(runfiles).resolve('@bazel/worker/third_party/github.com/bazelbuild/bazel/src/main/protobuf/worker_protocol.proto');
}
if (!protofile) {
throw new Error(`cannot find worker_protocol.proto at ${protoPath} or in Runfiles`);
}
}
const protoNamespace = protobufjs.loadSync(protofile);
if (!protoNamespace) {
throw new Error('Cannot find ' + path.resolve(protoPath));
}
const workerpb = protoNamespace.lookup('blaze.worker');
if (!workerpb) {
throw new Error(`Cannot find namespace blaze.worker`);
}
return workerpb;
}
/**
* workerpb contains the runtime representation of the worker protocol buffer,
* including accessor for the defined messages.
*/
const workerpb = loadWorkerPb();
/**
* runWorkerLoop handles the interacton between bazel workers and the
* TypeScript compiler. It reads compilation requests from stdin, unmarshals the
* data, and dispatches into `runOneBuild` for the actual compilation to happen.
*
* The compilation handler is parameterized so that this code can be used by
* different compiler entry points (currently TypeScript compilation, Angular
* compilation, and the contrib vulcanize worker).
*
* It's also exposed publicly as an npm package:
* https://www.npmjs.com/package/@bazel/worker
*/
function runWorkerLoop(runOneBuild) {
var e_1, _a;
return __awaiter(this, void 0, void 0, function* () {
// Hook all output to stderr and write it to a buffer, then include
// that buffer's in the worker protcol proto's textual output. This
// means you can log via console.error() and it will appear to the
// user as expected.
let consoleOutput = '';
process.stderr.write =
(chunk, ...otherArgs) => {
consoleOutput += chunk.toString();
return true;
};
// Accumulator for asynchronously read input.
// protobufjs uses node's Buffer, but has its own reader abstraction on top of
// it (for browser compatiblity). It ignores Buffer's builtin start and
// offset, which means the handling code below cannot use Buffer in a
// meaningful way (such as cycling data through it). The handler below reads
// any data available on stdin, concatenating it into this buffer. It then
// attempts to read a delimited Message from it. If a message is incomplete,
// it exits and waits for more input. If a message has been read, it strips
// its data of this buffer.
let buf = Buffer.alloc(0);
try {
stdinLoop: for (var _b = __asyncValues(process.stdin), _c; _c = yield _b.next(), !_c.done;) {
const chunk = _c.value;
buf = Buffer.concat([buf, chunk]);
try {
const reader = new protobufjs.Reader(buf);
// Read all requests that have accumulated in the buffer.
while (reader.len - reader.pos > 0) {
const messageStart = reader.len;
const msgLength = reader.uint32();
// chunk might be an incomplete read from stdin. If there are not enough
// bytes for the next full message, wait for more input.
if ((reader.len - reader.pos) < msgLength)
continue stdinLoop;
const req = workerpb.WorkRequest.decode(reader, msgLength);
// Once a message has been read, remove it from buf so that if we pause
// to read more input, this message will not be processed again.
buf = buf.slice(messageStart);
debug('=== Handling new build request');
const args = req.arguments;
const inputs = {};
for (const input of req.inputs) {
inputs[input.path] = input.digest.toString('hex');
}
debug('Compiling with:\n\t' + args.join('\n\t'));
process.stdout.write(workerpb.WorkResponse.encodeDelimited({
exitCode: (yield runOneBuild(args, inputs)) ? 0 : 1,
output: consoleOutput,
}).finish());
// Reset accumulated log output now that it has been printed.
consoleOutput = '';
// Force a garbage collection pass. This keeps our memory usage
// consistent across multiple compilations, and allows the file
// cache to use the current memory usage as a guideline for expiring
// data. Note: this is intentionally not within runOneBuild(), as
// we want to gc only after all its locals have gone out of scope.
global.gc();
}
// All messages have been handled, make sure the invariant holds and
// Buffer is empty once all messages have been read.
if (buf.length > 0) {
throw new Error('buffer not empty after reading all messages');
}
}
catch (e) {
log('Compilation failed', e.stack);
process.stdout.write(workerpb.WorkResponse
.encodeDelimited({ exitCode: 1, output: consoleOutput })
.finish());
// Clear buffer so the next build won't read an incomplete request.
buf = Buffer.alloc(0);
}
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (_c && !_c.done && (_a = _b.return)) yield _a.call(_b);
}
finally { if (e_1) throw e_1.error; }
}
});
}
exports.runWorkerLoop = runWorkerLoop;