import type * as types from "../shared/types"
import * as common from "../shared/common"
import * as ourselves from "./node"
import { ESBUILD_BINARY_PATH, generateBinPath } from "./node-platform"
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
declare const WASM: boolean
let worker_threads: typeof import('worker_threads') | undefined
if (process.env.ESBUILD_WORKER_THREADS !== '0') {
try {
worker_threads = require('worker_threads')
} catch {
}
let [major, minor] = process.versions.node.split('.')
if (
+major < 12 || (+major === 12 && +minor < 17)
|| (+major === 13 && +minor < 13)
) {
worker_threads = void 0
}
}
let isInternalWorkerThread = worker_threads?.workerData?.esbuildVersion === ESBUILD_VERSION
let esbuildCommandAndArgs = (): [string, string[]] => {
if ((!ESBUILD_BINARY_PATH || WASM) && (path.basename(__filename) !== 'main.js' || path.basename(__dirname) !== 'lib')) {
throw new Error(
`The esbuild JavaScript API cannot be bundled. Please mark the "esbuild" ` +
`package as external so it's not included in the bundle.\n` +
`\n` +
`More information: The file containing the code for esbuild's JavaScript ` +
`API (${__filename}) does not appear to be inside the esbuild package on ` +
`the file system, which usually means that the esbuild package was bundled ` +
`into another file. This is problematic because the API needs to run a ` +
`binary executable inside the esbuild package which is located using a ` +
`relative path from the API code to the executable. If the esbuild package ` +
`is bundled, the relative path will be incorrect and the executable won't ` +
`be found.`)
}
if (WASM) {
return ['node', [path.join(__dirname, '..', 'bin', 'esbuild')]]
} else {
const { binPath, isWASM } = generateBinPath()
if (isWASM) {
return ['node', [binPath]]
} else {
return [binPath, []]
}
}
}
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: any) {
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: any) {
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) =>
ensureServiceIsRunning().build(options)
export let context: typeof types.context = (buildOptions: types.BuildOptions) =>
ensureServiceIsRunning().context(buildOptions)
export let transform: typeof types.transform = (input: string | Uint8Array, options?: types.TransformOptions) =>
ensureServiceIsRunning().transform(input, options)
export let formatMessages: typeof types.formatMessages = (messages, options) =>
ensureServiceIsRunning().formatMessages(messages, options)
export let analyzeMetafile: typeof types.analyzeMetafile = (messages, options) =>
ensureServiceIsRunning().analyzeMetafile(messages, options)
export let buildSync: typeof types.buildSync = (options: types.BuildOptions) => {
if (worker_threads && !isInternalWorkerThread) {
if (!workerThreadService) workerThreadService = startWorkerThreadService(worker_threads)
return workerThreadService.buildSync(options)
}
let result: types.BuildResult
runServiceSync(service => service.buildOrContext({
callName: 'buildSync',
refs: 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: string | Uint8Array, options?: types.TransformOptions) => {
if (worker_threads && !isInternalWorkerThread) {
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) => {
if (worker_threads && !isInternalWorkerThread) {
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!
}
export let analyzeMetafileSync: typeof types.analyzeMetafileSync = (metafile, options) => {
if (worker_threads && !isInternalWorkerThread) {
if (!workerThreadService) workerThreadService = startWorkerThreadService(worker_threads)
return workerThreadService.analyzeMetafileSync(metafile, options)
}
let result: string
runServiceSync(service => service.analyzeMetafile({
callName: 'analyzeMetafileSync',
refs: null,
metafile: typeof metafile === 'string' ? metafile : JSON.stringify(metafile),
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.wasmModule) throw new Error(`The "wasmModule" 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
context: typeof types.context
transform: typeof types.transform
formatMessages: typeof types.formatMessages
analyzeMetafile: typeof types.analyzeMetafile
}
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, err => {
if (err) afterClose(err)
})
},
readFileSync: fs.readFileSync,
isSync: false,
hasFS: true,
esbuild: ourselves,
})
child.stdin.on('error', afterClose)
child.on('error', afterClose)
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) =>
new Promise<types.BuildResult>((resolve, reject) => {
service.buildOrContext({
callName: 'build',
refs,
options,
isTTY: isTTY(),
defaultWD,
callback: (err, res) => err ? reject(err) : resolve(res as types.BuildResult),
})
}),
context: (options: types.BuildOptions) =>
new Promise<types.BuildContext>((resolve, reject) =>
service.buildOrContext({
callName: 'context',
refs,
options,
isTTY: isTTY(),
defaultWD,
callback: (err, res) => err ? reject(err) : resolve(res as types.BuildContext),
})),
transform: (input: string | Uint8Array, options?: types.TransformOptions) =>
new Promise<types.TransformResult>((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) =>
new Promise((resolve, reject) =>
service.formatMessages({
callName: 'formatMessages',
refs,
messages,
options,
callback: (err, res) => err ? reject(err) : resolve(res!),
})),
analyzeMetafile: (metafile, options) =>
new Promise((resolve, reject) =>
service.analyzeMetafile({
callName: 'analyzeMetafile',
refs,
metafile: typeof metafile === 'string' ? metafile : JSON.stringify(metafile),
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,
hasFS: true,
esbuild: ourselves,
})
callback(service)
let stdout = child_process.execFileSync(command, args.concat(`--service=${ESBUILD_VERSION}`), {
cwd: defaultWD,
windowsHide: true,
input: stdin,
maxBuffer: +process.env.ESBUILD_MAX_BUFFER! || 16 * 1024 * 1024,
})
readFromStdout(stdout)
afterClose(null)
}
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(input: string | Uint8Array, options?: types.TransformOptions): types.TransformResult
formatMessagesSync: typeof types.formatMessagesSync
analyzeMetafileSync: typeof types.analyzeMetafileSync
}
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, esbuildVersion: ESBUILD_VERSION },
transferList: [workerPort],
execArgv: [],
})
let nextID = 0
let fakeBuildError = (text: string) => {
let error: any = new Error(`Build failed with 1 error:\nerror: ${text}`)
let errors: types.Message[] = [{ id: '', pluginName: '', 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
if (plugins && plugins.length > 0) throw fakeBuildError(`Cannot use plugins in synchronous API calls`)
}
let applyProperties = (object: any, properties: Record<string, any>): void => {
for (let key in properties) {
object[key] = properties[key]
}
}
let runCallSync = (command: string, args: any[]): any => {
let id = nextID++
let sharedBuffer = new SharedArrayBuffer(8)
let sharedBufferView = new Int32Array(sharedBuffer)
let msg: MainToWorkerMessage = { sharedBuffer, id, command, args }
worker.postMessage(msg)
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
}
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])
},
analyzeMetafileSync(metafile, options) {
return runCallSync('analyzeMetafile', [metafile, options])
},
}
}
let startSyncServiceWorker = () => {
let workerPort: import('worker_threads').MessagePort = worker_threads!.workerData.workerPort
let parentPort = worker_threads!.parentPort!
let extractProperties = (object: any): Record<string, any> => {
let properties: Record<string, any> = {}
if (object && typeof object === 'object') {
for (let key in object) {
properties[key] = object[key]
}
}
return properties
}
try {
let service = ensureServiceIsRunning()
defaultWD = worker_threads!.workerData.defaultWD
parentPort.on('message', (msg: MainToWorkerMessage) => {
(async () => {
let { sharedBuffer, id, command, args } = msg
let sharedBufferView = new Int32Array(sharedBuffer)
try {
switch (command) {
case 'build':
workerPort.postMessage({ id, resolve: await service.build(args[0]) })
break
case 'transform':
workerPort.postMessage({ id, resolve: await service.transform(args[0], args[1]) })
break
case 'formatMessages':
workerPort.postMessage({ id, resolve: await service.formatMessages(args[0], args[1]) })
break
case 'analyzeMetafile':
workerPort.postMessage({ id, resolve: await service.analyzeMetafile(args[0], args[1]) })
break
default:
throw new Error(`Invalid command: ${command}`)
}
} catch (reject) {
workerPort.postMessage({ id, reject, properties: extractProperties(reject) })
}
Atomics.add(sharedBufferView, 0, 1)
Atomics.notify(sharedBufferView, 0, Infinity)
})()
})
}
catch (reject) {
parentPort.on('message', (msg: MainToWorkerMessage) => {
let { sharedBuffer, id } = msg
let sharedBufferView = new Int32Array(sharedBuffer)
workerPort.postMessage({ id, reject, properties: extractProperties(reject) })
Atomics.add(sharedBufferView, 0, 1)
Atomics.notify(sharedBufferView, 0, Infinity)
})
}
}
if (isInternalWorkerThread) {
startSyncServiceWorker()
}
export default ourselves