Files
esbuild/lib/stdio_protocol.ts
2020-09-11 10:58:21 -07:00

236 lines
5.7 KiB
TypeScript

// The JavaScript API communicates with the Go child process over stdin/stdout
// using this protocol. It's a very simple binary protocol that uses primitives
// and nested arrays and maps. It's basically JSON with UTF-8 encoding and an
// additional byte array primitive. You must send a response after receiving a
// request because the other end is blocking on the response coming back.
import * as types from "./types";
export interface BuildRequest {
command: 'build';
flags: string[];
write: boolean;
stdin: string | null;
resolveDir: string | null;
}
export interface BuildResponse {
errors: types.Message[];
warnings: types.Message[];
outputFiles: types.OutputFile[];
}
export interface TransformRequest {
command: 'transform';
flags: string[];
input: string;
inputFS: boolean;
}
export interface TransformResponse {
errors: types.Message[];
warnings: types.Message[];
js: string;
jsFS: boolean;
jsSourceMap: string;
jsSourceMapFS: boolean;
}
////////////////////////////////////////////////////////////////////////////////
export interface Packet {
id: number;
isRequest: boolean;
value: Value;
}
export type Value =
| null
| boolean
| number
| string
| Uint8Array
| Value[]
| { [key: string]: Value }
export function encodePacket(packet: Packet): Uint8Array {
let visit = (value: Value) => {
if (value === null) {
bb.write8(0);
} else if (typeof value === 'boolean') {
bb.write8(1);
bb.write8(+value);
} else if (typeof value === 'number') {
bb.write8(2);
bb.write32(value | 0);
} else if (typeof value === 'string') {
bb.write8(3);
bb.write(encodeUTF8(value));
} else if (value instanceof Uint8Array) {
bb.write8(4);
bb.write(value);
} else if (value instanceof Array) {
bb.write8(5);
bb.write32(value.length);
for (let item of value) {
visit(item);
}
} else {
let keys = Object.keys(value);
bb.write8(6);
bb.write32(keys.length);
for (let key of keys) {
bb.write(encodeUTF8(key));
visit(value[key]);
}
}
};
let bb = new ByteBuffer;
bb.write32(0); // Reserve space for the length
bb.write32((packet.id << 1) | +!packet.isRequest);
visit(packet.value);
writeUInt32LE(bb.buf, bb.len - 4, 0); // Patch the length in
return bb.buf.subarray(0, bb.len);
}
export function decodePacket(bytes: Uint8Array): Packet {
let visit = (): Value => {
switch (bb.read8()) {
case 0: // null
return null;
case 1: // boolean
return !!bb.read8();
case 2: // number
return bb.read32();
case 3: // string
return decodeUTF8(bb.read());
case 4: // Uint8Array
return bb.read();
case 5: { // Value[]
let count = bb.read32();
let value: Value[] = [];
for (let i = 0; i < count; i++) {
value.push(visit());
}
return value;
}
case 6: { // { [key: string]: Value }
let count = bb.read32();
let value: { [key: string]: Value } = {};
for (let i = 0; i < count; i++) {
value[decodeUTF8(bb.read())] = visit();
}
return value;
}
default:
throw new Error('Invalid packet');
}
};
let bb = new ByteBuffer(bytes);
let id = bb.read32();
let isRequest = (id & 1) === 0;
id >>>= 1;
let value = visit();
if (bb.ptr !== bytes.length) {
throw new Error('Invalid packet');
}
return { id, isRequest, value };
}
class ByteBuffer {
len = 0;
ptr = 0;
constructor(public buf = new Uint8Array(1024)) {
}
private _write(delta: number): number {
if (this.len + delta > this.buf.length) {
let clone = new Uint8Array((this.len + delta) * 2);
clone.set(this.buf);
this.buf = clone;
}
this.len += delta;
return this.len - delta;
}
write8(value: number): void {
let offset = this._write(1);
this.buf[offset] = value;
}
write32(value: number): void {
let offset = this._write(4);
writeUInt32LE(this.buf, value, offset);
}
write(bytes: Uint8Array): void {
let offset = this._write(4 + bytes.length);
writeUInt32LE(this.buf, bytes.length, offset);
this.buf.set(bytes, offset + 4);
}
private _read(delta: number): number {
if (this.ptr + delta > this.buf.length) {
throw new Error('Invalid packet');
}
this.ptr += delta;
return this.ptr - delta;
}
read8(): number {
return this.buf[this._read(1)];
}
read32(): number {
return readUInt32LE(this.buf, this._read(4));
}
read(): Uint8Array {
let length = this.read32();
let bytes = new Uint8Array(length);
let ptr = this._read(bytes.length);
bytes.set(this.buf.subarray(ptr, ptr + length));
return bytes;
}
}
export let encodeUTF8: (text: string) => Uint8Array
export let decodeUTF8: (bytes: Uint8Array) => string
// For the browser and node 12.x
if (typeof TextEncoder !== 'undefined' && typeof TextDecoder !== 'undefined') {
let encoder = new TextEncoder();
let decoder = new TextDecoder();
encodeUTF8 = text => encoder.encode(text);
decodeUTF8 = bytes => decoder.decode(bytes);
}
// For node 10.x
else if (typeof Buffer !== 'undefined') {
encodeUTF8 = text => Buffer.from(text);
decodeUTF8 = bytes => Buffer.from(bytes).toString();
}
else {
throw new Error('No UTF-8 codec found');
}
export function readUInt32LE(buffer: Uint8Array, offset: number): number {
return buffer[offset++] |
(buffer[offset++] << 8) |
(buffer[offset++] << 16) |
(buffer[offset++] << 24);
}
function writeUInt32LE(buffer: Uint8Array, value: number, offset: number): void {
buffer[offset++] = value;
buffer[offset++] = value >> 8;
buffer[offset++] = value >> 16;
buffer[offset++] = value >> 24;
}