import * as types from "../shared/types"; import * as common from "../shared/common"; import child_process = require('child_process'); import crypto = require('crypto'); import path = require('path'); import fs = require('fs'); import os = require('os'); import tty = require('tty'); declare const ESBUILD_VERSION: string; // This file is used for both the "esbuild" package and the "esbuild-wasm" // package. "WASM" will be true for "esbuild-wasm" and false for "esbuild". declare const WASM: boolean; let worker_threads: typeof import('worker_threads') | undefined; // This optimization is opt-in for now because it could break if node has bugs // with "worker_threads", and node has had such bugs in the past. // // TODO: Determine under which conditions this is safe to enable, and then // replace this check with a check for those conditions. if (process.env.ESBUILD_WORKER_THREADS) { // Don't crash if the "worker_threads" library isn't present try { worker_threads = require('worker_threads'); } catch { } } let esbuildCommandAndArgs = (): [string, string[]] => { // This feature was added to give external code a way to modify the binary // path without modifying the code itself. Do not remove this because // external code relies on this. if (process.env.ESBUILD_BINARY_PATH) { return [path.resolve(process.env.ESBUILD_BINARY_PATH), []]; } if (WASM) { return ['node', [path.join(__dirname, '..', 'bin', 'esbuild')]]; } if (process.platform === 'win32') { return [path.join(__dirname, '..', 'esbuild.exe'), []]; } // Yarn 2 is deliberately incompatible with binary modules because the // developers of Yarn 2 don't think they should be used. See this thread for // details: https://github.com/yarnpkg/berry/issues/882. // // As a compatibility hack we replace the binary with a wrapper script only // for Yarn 2. The wrapper script is avoided for other platforms because // running the binary directly without going through node first is faster. // However, this will make using the JavaScript API with Yarn 2 unnecessarily // slow because the wrapper means running the binary will now start another // nested node process just to call "spawnSync" and run the actual binary. // // To work around this workaround, we query for the place the binary is moved // to if the original location is replaced by our Yarn 2 compatibility hack. // If it exists, we can infer that we are running within Yarn 2 and the // JavaScript API should invoke the binary here instead to avoid a slowdown. // Calling the binary directly can be over 6x faster than calling the wrapper // script instead. let pathForYarn2 = path.join(__dirname, '..', 'esbuild'); if (fs.existsSync(pathForYarn2)) { return [pathForYarn2, []]; } return [path.join(__dirname, '..', 'bin', 'esbuild'), []]; }; // Return true if stderr is a TTY let isTTY = () => tty.isatty(2); let fsSync: common.StreamFS = { readFile(tempFile, callback) { try { let contents = fs.readFileSync(tempFile, 'utf8'); try { fs.unlinkSync(tempFile); } catch { } callback(null, contents); } catch (err) { callback(err, null); } }, writeFile(contents, callback) { try { let tempFile = randomFileName(); fs.writeFileSync(tempFile, contents); callback(tempFile); } catch { callback(null); } }, }; let fsAsync: common.StreamFS = { readFile(tempFile, callback) { try { fs.readFile(tempFile, 'utf8', (err, contents) => { try { fs.unlink(tempFile, () => callback(err, contents)); } catch { callback(err, contents); } }); } catch (err) { callback(err, null); } }, writeFile(contents, callback) { try { let tempFile = randomFileName(); fs.writeFile(tempFile, contents, err => err !== null ? callback(null) : callback(tempFile)); } catch { callback(null); } }, }; export let version = ESBUILD_VERSION; export let build: typeof types.build = (options: types.BuildOptions): Promise => ensureServiceIsRunning().build(options); export let serve: typeof types.serve = (serveOptions, buildOptions) => ensureServiceIsRunning().serve(serveOptions, buildOptions); export let transform: typeof types.transform = (input, options) => ensureServiceIsRunning().transform(input, options); export let formatMessages: typeof types.formatMessages = (messages, options) => ensureServiceIsRunning().formatMessages(messages, options); export let buildSync: typeof types.buildSync = (options: types.BuildOptions): any => { // Try using a long-lived worker thread to avoid repeated start-up overhead if (worker_threads) { if (!workerThreadService) workerThreadService = startWorkerThreadService(worker_threads); return workerThreadService.buildSync(options); } let result: types.BuildResult; runServiceSync(service => service.buildOrServe({ callName: 'buildSync', refs: null, serveOptions: null, options, isTTY: isTTY(), defaultWD, callback: (err, res) => { if (err) throw err; result = res as types.BuildResult }, })); return result!; }; export let transformSync: typeof types.transformSync = (input, options) => { // Try using a long-lived worker thread to avoid repeated start-up overhead if (worker_threads) { if (!workerThreadService) workerThreadService = startWorkerThreadService(worker_threads); return workerThreadService.transformSync(input, options); } let result: types.TransformResult; runServiceSync(service => service.transform({ callName: 'transformSync', refs: null, input, options: options || {}, isTTY: isTTY(), fs: fsSync, callback: (err, res) => { if (err) throw err; result = res! }, })); return result!; }; export let formatMessagesSync: typeof types.formatMessagesSync = (messages, options) => { // Try using a long-lived worker thread to avoid repeated start-up overhead if (worker_threads) { if (!workerThreadService) workerThreadService = startWorkerThreadService(worker_threads); return workerThreadService.formatMessagesSync(messages, options); } let result: string[]; runServiceSync(service => service.formatMessages({ callName: 'formatMessagesSync', refs: null, messages, options, callback: (err, res) => { if (err) throw err; result = res! }, })); return result!; }; let initializeWasCalled = false; export let initialize: typeof types.initialize = options => { options = common.validateInitializeOptions(options || {}); if (options.wasmURL) throw new Error(`The "wasmURL" option only works in the browser`) if (options.worker) throw new Error(`The "worker" option only works in the browser`) if (initializeWasCalled) throw new Error('Cannot call "initialize" more than once') ensureServiceIsRunning() initializeWasCalled = true return Promise.resolve(); } interface Service { build: typeof types.build; serve: typeof types.serve; transform: typeof types.transform; formatMessages: typeof types.formatMessages; } let defaultWD = process.cwd(); let longLivedService: Service | undefined; let ensureServiceIsRunning = (): Service => { if (longLivedService) return longLivedService; let [command, args] = esbuildCommandAndArgs(); let child = child_process.spawn(command, args.concat(`--service=${ESBUILD_VERSION}`, '--ping'), { windowsHide: true, stdio: ['pipe', 'pipe', 'inherit'], cwd: defaultWD, }); let { readFromStdout, afterClose, service } = common.createChannel({ writeToStdin(bytes) { child.stdin.write(bytes); }, readFileSync: fs.readFileSync, isSync: false, isBrowser: false, }); const stdin: typeof child.stdin & { unref?(): void } = child.stdin; const stdout: typeof child.stdout & { unref?(): void } = child.stdout; stdout.on('data', readFromStdout); stdout.on('end', afterClose); let refCount = 0; child.unref(); if (stdin.unref) { stdin.unref(); } if (stdout.unref) { stdout.unref(); } const refs: common.Refs = { ref() { if (++refCount === 1) child.ref(); }, unref() { if (--refCount === 0) child.unref(); }, } longLivedService = { build: (options: types.BuildOptions): Promise => { return new Promise((resolve, reject) => { service.buildOrServe({ callName: 'build', refs, serveOptions: null, options, isTTY: isTTY(), defaultWD, callback: (err, res) => err ? reject(err) : resolve(res as types.BuildResult), }) }) }, serve: (serveOptions, buildOptions) => { if (serveOptions === null || typeof serveOptions !== 'object') throw new Error('The first argument must be an object') return new Promise((resolve, reject) => service.buildOrServe({ callName: 'serve', refs, serveOptions, options: buildOptions, isTTY: isTTY(), defaultWD, callback: (err, res) => err ? reject(err) : resolve(res as types.ServeResult), })) }, transform: (input, options) => { return new Promise((resolve, reject) => service.transform({ callName: 'transform', refs, input, options: options || {}, isTTY: isTTY(), fs: fsAsync, callback: (err, res) => err ? reject(err) : resolve(res!), })); }, formatMessages: (messages, options) => { return new Promise((resolve, reject) => service.formatMessages({ callName: 'formatMessages', refs, messages, options, callback: (err, res) => err ? reject(err) : resolve(res!), })); }, }; return longLivedService; } let runServiceSync = (callback: (service: common.StreamService) => void): void => { let [command, args] = esbuildCommandAndArgs(); let stdin = new Uint8Array(); let { readFromStdout, afterClose, service } = common.createChannel({ writeToStdin(bytes) { if (stdin.length !== 0) throw new Error('Must run at most one command'); stdin = bytes; }, isSync: true, isBrowser: false, }); callback(service); let stdout = child_process.execFileSync(command, args.concat(`--service=${ESBUILD_VERSION}`), { cwd: defaultWD, windowsHide: true, input: stdin, // We don't know how large the output could be. If it's too large, the // command will fail with ENOBUFS. Reserve 16mb for now since that feels // like it should be enough. Also allow overriding this with an environment // variable. maxBuffer: +process.env.ESBUILD_MAX_BUFFER! || 16 * 1024 * 1024, }); readFromStdout(stdout); afterClose(); }; let randomFileName = () => { return path.join(os.tmpdir(), `esbuild-${crypto.randomBytes(32).toString('hex')}`); }; interface MainToWorkerMessage { sharedBuffer: SharedArrayBuffer; id: number; command: string; args: any[]; } interface WorkerThreadService { buildSync(options: types.BuildOptions): types.BuildResult; transformSync: typeof types.transformSync; formatMessagesSync: typeof types.formatMessagesSync; } let workerThreadService: WorkerThreadService | null = null; let startWorkerThreadService = (worker_threads: typeof import('worker_threads')): WorkerThreadService => { let { port1: mainPort, port2: workerPort } = new worker_threads.MessageChannel(); let worker = new worker_threads.Worker(__filename, { workerData: { workerPort, defaultWD }, transferList: [workerPort], // From node's documentation: https://nodejs.org/api/worker_threads.html // // Take care when launching worker threads from preload scripts (scripts loaded // and run using the `-r` command line flag). Unless the `execArgv` option is // explicitly set, new Worker threads automatically inherit the command line flags // from the running process and will preload the same preload scripts as the main // thread. If the preload script unconditionally launches a worker thread, every // thread spawned will spawn another until the application crashes. // execArgv: [], }); let nextID = 0; let wasStopped = false; // This forbids options which would cause structured clone errors let fakeBuildError = (text: string) => { let error: any = new Error(`Build failed with 1 error:\nerror: ${text}`); let errors: types.Message[] = [{ text, location: null, notes: [], detail: void 0 }]; error.errors = errors; error.warnings = []; return error; }; let validateBuildSyncOptions = (options: types.BuildOptions | undefined): void => { if (!options) return let plugins = options.plugins let incremental = options.incremental if (plugins && plugins.length > 0) throw fakeBuildError(`Cannot use plugins in synchronous API calls`); if (incremental) throw fakeBuildError(`Cannot use "incremental" with a synchronous build`); }; // MessagePort doesn't copy the properties of Error objects. We still want // error objects to have extra properties such as "warnings" so implement the // property copying manually. let applyProperties = (object: any, properties: Record): void => { for (let key in properties) { object[key] = properties[key]; } }; let runCallSync = (command: string, args: any[]): any => { if (wasStopped) throw new Error('The service was stopped'); let id = nextID++; // Make a fresh shared buffer for every request. That way we can't have a // race where a notification from the previous call overlaps with this call. let sharedBuffer = new SharedArrayBuffer(8); let sharedBufferView = new Int32Array(sharedBuffer); // Send the message to the worker. Note that the worker could potentially // complete the request before this thread returns from this call. let msg: MainToWorkerMessage = { sharedBuffer, id, command, args }; worker.postMessage(msg); // If the value hasn't changed (i.e. the request hasn't been completed, // wait until the worker thread notifies us that the request is complete). // // Otherwise, if the value has changed, the request has already been // completed. Don't wait in that case because the notification may never // arrive if it has already been sent. let status = Atomics.wait(sharedBufferView, 0, 0); if (status !== 'ok' && status !== 'not-equal') throw new Error('Internal error: Atomics.wait() failed: ' + status); let { message: { id: id2, resolve, reject, properties } } = worker_threads!.receiveMessageOnPort(mainPort)!; if (id !== id2) throw new Error(`Internal error: Expected id ${id} but got id ${id2}`); if (reject) { applyProperties(reject, properties); throw reject; } return resolve; }; // Calling unref() on a worker will allow the thread to exit if it's the last // only active handle in the event system. This means node will still exit // when there are no more event handlers from the main thread. So there's no // need to have a "stop()" function. worker.unref(); return { buildSync(options) { validateBuildSyncOptions(options); return runCallSync('build', [options]); }, transformSync(input, options) { return runCallSync('transform', [input, options]); }, formatMessagesSync(messages, options) { return runCallSync('formatMessages', [messages, options]); }, }; }; let startSyncServiceWorker = () => { let workerPort: import('worker_threads').MessagePort = worker_threads!.workerData.workerPort; let parentPort = worker_threads!.parentPort!; let service = ensureServiceIsRunning(); // Take the default working directory from the main thread because we want it // to be consistent. This will be the working directory that was current at // the time the "esbuild" package was first imported. defaultWD = worker_threads!.workerData.defaultWD; // MessagePort doesn't copy the properties of Error objects. We still want // error objects to have extra properties such as "warnings" so implement the // property copying manually. let extractProperties = (object: any): Record => { let properties: Record = {}; if (object && typeof object === 'object') { for (let key in object) { properties[key] = object[key]; } } return properties; }; parentPort.on('message', (msg: MainToWorkerMessage) => { (async () => { let { sharedBuffer, id, command, args } = msg; let sharedBufferView = new Int32Array(sharedBuffer); try { if (command === 'build') { workerPort.postMessage({ id, resolve: await service.build(args[0]) }); } else if (command === 'transform') { workerPort.postMessage({ id, resolve: await service.transform(args[0], args[1]) }); } else if (command === 'formatMessages') { workerPort.postMessage({ id, resolve: await service.formatMessages(args[0], args[1]) }); } else { throw new Error(`Invalid command: ${command}`); } } catch (reject) { workerPort.postMessage({ id, reject, properties: extractProperties(reject) }); } // The message has already been posted by this point, so it should be // safe to wake the main thread. The main thread should always get the // message we sent above. // First, change the shared value. That way if the main thread attempts // to wait for us after this point, the wait will fail because the shared // value has changed. Atomics.add(sharedBufferView, 0, 1); // Then, wake the main thread. This handles the case where the main // thread was already waiting for us before the shared value was changed. Atomics.notify(sharedBufferView, 0, Infinity); })(); }); }; // If we're in the worker thread, start the worker code if (worker_threads && !worker_threads.isMainThread) { startSyncServiceWorker(); }