mirror of
https://github.com/hanzoai/esbuild.git
synced 2026-09-24 16:58:50 +00:00
1857 lines
76 KiB
TypeScript
1857 lines
76 KiB
TypeScript
import type * as types from "./types"
|
|
import * as protocol from "./stdio_protocol"
|
|
|
|
declare const ESBUILD_VERSION: string
|
|
|
|
const quote: (x: string) => string = JSON.stringify
|
|
|
|
const buildLogLevelDefault = 'warning'
|
|
const transformLogLevelDefault = 'silent'
|
|
|
|
function validateAndJoinStringArray(values: string[], what: string): string {
|
|
const toJoin: string[] = []
|
|
for (const value of values) {
|
|
validateStringValue(value, what)
|
|
if (value.indexOf(',') >= 0) throw new Error(`Invalid ${what}: ${value}`)
|
|
toJoin.push(value)
|
|
}
|
|
return toJoin.join(',')
|
|
}
|
|
|
|
let canBeAnything = () => null
|
|
|
|
let mustBeBoolean = (value: boolean | undefined): string | null =>
|
|
typeof value === 'boolean' ? null : 'a boolean'
|
|
|
|
let mustBeString = (value: string | undefined): string | null =>
|
|
typeof value === 'string' ? null : 'a string'
|
|
|
|
let mustBeRegExp = (value: RegExp | undefined): string | null =>
|
|
value instanceof RegExp ? null : 'a RegExp object'
|
|
|
|
let mustBeInteger = (value: number | undefined): string | null =>
|
|
typeof value === 'number' && value === (value | 0) ? null : 'an integer'
|
|
|
|
let mustBeValidPortNumber = (value: number | undefined): string | null =>
|
|
typeof value === 'number' && value === (value | 0) && value >= 0 && value <= 0xFFFF ? null : 'a valid port number'
|
|
|
|
let mustBeFunction = (value: Function | undefined): string | null =>
|
|
typeof value === 'function' ? null : 'a function'
|
|
|
|
let mustBeArray = <T>(value: T[] | undefined): string | null =>
|
|
Array.isArray(value) ? null : 'an array'
|
|
|
|
let mustBeArrayOfStrings = (value: string[] | undefined): string | null =>
|
|
Array.isArray(value) && value.every(x => typeof x === 'string') ? null : 'an array of strings'
|
|
|
|
let mustBeObject = (value: Object | undefined): string | null =>
|
|
typeof value === 'object' && value !== null && !Array.isArray(value) ? null : 'an object'
|
|
|
|
let mustBeEntryPoints = (value: types.BuildOptions['entryPoints']): string | null =>
|
|
typeof value === 'object' && value !== null ? null : 'an array or an object'
|
|
|
|
let mustBeWebAssemblyModule = (value: WebAssembly.Module | undefined): string | null =>
|
|
value instanceof WebAssembly.Module ? null : 'a WebAssembly.Module'
|
|
|
|
let mustBeObjectOrNull = (value: Object | null | undefined): string | null =>
|
|
typeof value === 'object' && !Array.isArray(value) ? null : 'an object or null'
|
|
|
|
let mustBeStringOrBoolean = (value: string | boolean | undefined): string | null =>
|
|
typeof value === 'string' || typeof value === 'boolean' ? null : 'a string or a boolean'
|
|
|
|
let mustBeStringOrObject = (value: string | Object | undefined): string | null =>
|
|
typeof value === 'string' || typeof value === 'object' && value !== null && !Array.isArray(value) ? null : 'a string or an object'
|
|
|
|
let mustBeStringOrArrayOfStrings = (value: string | string[] | undefined): string | null =>
|
|
typeof value === 'string' || (Array.isArray(value) && value.every(x => typeof x === 'string')) ? null : 'a string or an array of strings'
|
|
|
|
let mustBeStringOrUint8Array = (value: string | Uint8Array | undefined): string | null =>
|
|
typeof value === 'string' || value instanceof Uint8Array ? null : 'a string or a Uint8Array'
|
|
|
|
let mustBeStringOrURL = (value: string | URL | undefined): string | null =>
|
|
typeof value === 'string' || value instanceof URL ? null : 'a string or a URL'
|
|
|
|
type OptionKeys = { [key: string]: boolean }
|
|
|
|
function getFlag<T, K extends (keyof T & string)>(object: T, keys: OptionKeys, key: K, mustBeFn: (value: T[K]) => string | null): T[K] | undefined {
|
|
let value = object[key]
|
|
keys[key + ''] = true
|
|
if (value === undefined) return undefined
|
|
let mustBe = mustBeFn(value)
|
|
if (mustBe !== null) throw new Error(`${quote(key)} must be ${mustBe}`)
|
|
return value
|
|
}
|
|
|
|
function checkForInvalidFlags(object: Object, keys: OptionKeys, where: string): void {
|
|
for (let key in object) {
|
|
if (!(key in keys)) {
|
|
throw new Error(`Invalid option ${where}: ${quote(key)}`)
|
|
}
|
|
}
|
|
}
|
|
|
|
export function validateInitializeOptions(options: types.InitializeOptions): types.InitializeOptions {
|
|
let keys: OptionKeys = Object.create(null)
|
|
let wasmURL = getFlag(options, keys, 'wasmURL', mustBeStringOrURL)
|
|
let wasmModule = getFlag(options, keys, 'wasmModule', mustBeWebAssemblyModule)
|
|
let worker = getFlag(options, keys, 'worker', mustBeBoolean)
|
|
checkForInvalidFlags(options, keys, 'in initialize() call')
|
|
return {
|
|
wasmURL,
|
|
wasmModule,
|
|
worker,
|
|
}
|
|
}
|
|
|
|
type MangleCache = Record<string, string | false>
|
|
|
|
function validateMangleCache(mangleCache: MangleCache | undefined): MangleCache | undefined {
|
|
let validated: MangleCache | undefined
|
|
if (mangleCache !== undefined) {
|
|
validated = Object.create(null) as MangleCache
|
|
for (let key in mangleCache) {
|
|
let value = mangleCache[key]
|
|
if (typeof value === 'string' || value === false) {
|
|
validated[key] = value
|
|
} else {
|
|
throw new Error(`Expected ${quote(key)} in mangle cache to map to either a string or false`)
|
|
}
|
|
}
|
|
}
|
|
return validated
|
|
}
|
|
|
|
type CommonOptions = types.BuildOptions | types.TransformOptions
|
|
|
|
function pushLogFlags(flags: string[], options: CommonOptions, keys: OptionKeys, isTTY: boolean, logLevelDefault: types.LogLevel): void {
|
|
let color = getFlag(options, keys, 'color', mustBeBoolean)
|
|
let logLevel = getFlag(options, keys, 'logLevel', mustBeString)
|
|
let logLimit = getFlag(options, keys, 'logLimit', mustBeInteger)
|
|
|
|
if (color !== void 0) flags.push(`--color=${color}`)
|
|
else if (isTTY) flags.push(`--color=true`); // This is needed to fix "execFileSync" which buffers stderr
|
|
flags.push(`--log-level=${logLevel || logLevelDefault}`)
|
|
flags.push(`--log-limit=${logLimit || 0}`)
|
|
}
|
|
|
|
function validateStringValue(value: unknown, what: string, key?: string): string {
|
|
if (typeof value !== 'string') {
|
|
throw new Error(`Expected value for ${what}${key !== void 0 ? ' ' + quote(key) : ''} to be a string, got ${typeof value} instead`)
|
|
}
|
|
return value
|
|
}
|
|
|
|
function pushCommonFlags(flags: string[], options: CommonOptions, keys: OptionKeys): void {
|
|
let legalComments = getFlag(options, keys, 'legalComments', mustBeString)
|
|
let sourceRoot = getFlag(options, keys, 'sourceRoot', mustBeString)
|
|
let sourcesContent = getFlag(options, keys, 'sourcesContent', mustBeBoolean)
|
|
let target = getFlag(options, keys, 'target', mustBeStringOrArrayOfStrings)
|
|
let format = getFlag(options, keys, 'format', mustBeString)
|
|
let globalName = getFlag(options, keys, 'globalName', mustBeString)
|
|
let mangleProps = getFlag(options, keys, 'mangleProps', mustBeRegExp)
|
|
let reserveProps = getFlag(options, keys, 'reserveProps', mustBeRegExp)
|
|
let mangleQuoted = getFlag(options, keys, 'mangleQuoted', mustBeBoolean)
|
|
let minify = getFlag(options, keys, 'minify', mustBeBoolean)
|
|
let minifySyntax = getFlag(options, keys, 'minifySyntax', mustBeBoolean)
|
|
let minifyWhitespace = getFlag(options, keys, 'minifyWhitespace', mustBeBoolean)
|
|
let minifyIdentifiers = getFlag(options, keys, 'minifyIdentifiers', mustBeBoolean)
|
|
let lineLimit = getFlag(options, keys, 'lineLimit', mustBeInteger)
|
|
let drop = getFlag(options, keys, 'drop', mustBeArrayOfStrings)
|
|
let dropLabels = getFlag(options, keys, 'dropLabels', mustBeArrayOfStrings)
|
|
let charset = getFlag(options, keys, 'charset', mustBeString)
|
|
let treeShaking = getFlag(options, keys, 'treeShaking', mustBeBoolean)
|
|
let ignoreAnnotations = getFlag(options, keys, 'ignoreAnnotations', mustBeBoolean)
|
|
let jsx = getFlag(options, keys, 'jsx', mustBeString)
|
|
let jsxFactory = getFlag(options, keys, 'jsxFactory', mustBeString)
|
|
let jsxFragment = getFlag(options, keys, 'jsxFragment', mustBeString)
|
|
let jsxImportSource = getFlag(options, keys, 'jsxImportSource', mustBeString)
|
|
let jsxDev = getFlag(options, keys, 'jsxDev', mustBeBoolean)
|
|
let jsxSideEffects = getFlag(options, keys, 'jsxSideEffects', mustBeBoolean)
|
|
let define = getFlag(options, keys, 'define', mustBeObject)
|
|
let logOverride = getFlag(options, keys, 'logOverride', mustBeObject)
|
|
let supported = getFlag(options, keys, 'supported', mustBeObject)
|
|
let pure = getFlag(options, keys, 'pure', mustBeArrayOfStrings)
|
|
let keepNames = getFlag(options, keys, 'keepNames', mustBeBoolean)
|
|
let platform = getFlag(options, keys, 'platform', mustBeString)
|
|
let tsconfigRaw = getFlag(options, keys, 'tsconfigRaw', mustBeStringOrObject)
|
|
|
|
if (legalComments) flags.push(`--legal-comments=${legalComments}`)
|
|
if (sourceRoot !== void 0) flags.push(`--source-root=${sourceRoot}`)
|
|
if (sourcesContent !== void 0) flags.push(`--sources-content=${sourcesContent}`)
|
|
if (target) flags.push(`--target=${validateAndJoinStringArray(Array.isArray(target) ? target : [target], 'target')}`)
|
|
if (format) flags.push(`--format=${format}`)
|
|
if (globalName) flags.push(`--global-name=${globalName}`)
|
|
if (platform) flags.push(`--platform=${platform}`)
|
|
if (tsconfigRaw) flags.push(`--tsconfig-raw=${typeof tsconfigRaw === 'string' ? tsconfigRaw : JSON.stringify(tsconfigRaw)}`)
|
|
|
|
if (minify) flags.push('--minify')
|
|
if (minifySyntax) flags.push('--minify-syntax')
|
|
if (minifyWhitespace) flags.push('--minify-whitespace')
|
|
if (minifyIdentifiers) flags.push('--minify-identifiers')
|
|
if (lineLimit) flags.push(`--line-limit=${lineLimit}`)
|
|
if (charset) flags.push(`--charset=${charset}`)
|
|
if (treeShaking !== void 0) flags.push(`--tree-shaking=${treeShaking}`)
|
|
if (ignoreAnnotations) flags.push(`--ignore-annotations`)
|
|
if (drop) for (let what of drop) flags.push(`--drop:${validateStringValue(what, 'drop')}`)
|
|
if (dropLabels) flags.push(`--drop-labels=${validateAndJoinStringArray(dropLabels, 'drop label')}`)
|
|
if (mangleProps) flags.push(`--mangle-props=${jsRegExpToGoRegExp(mangleProps)}`)
|
|
if (reserveProps) flags.push(`--reserve-props=${jsRegExpToGoRegExp(reserveProps)}`)
|
|
if (mangleQuoted !== void 0) flags.push(`--mangle-quoted=${mangleQuoted}`)
|
|
|
|
if (jsx) flags.push(`--jsx=${jsx}`)
|
|
if (jsxFactory) flags.push(`--jsx-factory=${jsxFactory}`)
|
|
if (jsxFragment) flags.push(`--jsx-fragment=${jsxFragment}`)
|
|
if (jsxImportSource) flags.push(`--jsx-import-source=${jsxImportSource}`)
|
|
if (jsxDev) flags.push(`--jsx-dev`)
|
|
if (jsxSideEffects) flags.push(`--jsx-side-effects`)
|
|
|
|
if (define) {
|
|
for (let key in define) {
|
|
if (key.indexOf('=') >= 0) throw new Error(`Invalid define: ${key}`)
|
|
flags.push(`--define:${key}=${validateStringValue(define[key], 'define', key)}`)
|
|
}
|
|
}
|
|
if (logOverride) {
|
|
for (let key in logOverride) {
|
|
if (key.indexOf('=') >= 0) throw new Error(`Invalid log override: ${key}`)
|
|
flags.push(`--log-override:${key}=${validateStringValue(logOverride[key], 'log override', key)}`)
|
|
}
|
|
}
|
|
if (supported) {
|
|
for (let key in supported) {
|
|
if (key.indexOf('=') >= 0) throw new Error(`Invalid supported: ${key}`)
|
|
const value = supported[key]
|
|
if (typeof value !== 'boolean') throw new Error(`Expected value for supported ${quote(key)} to be a boolean, got ${typeof value} instead`)
|
|
flags.push(`--supported:${key}=${value}`)
|
|
}
|
|
}
|
|
if (pure) for (let fn of pure) flags.push(`--pure:${validateStringValue(fn, 'pure')}`)
|
|
if (keepNames) flags.push(`--keep-names`)
|
|
}
|
|
|
|
function flagsForBuildOptions(
|
|
callName: string,
|
|
options: types.BuildOptions,
|
|
isTTY: boolean,
|
|
logLevelDefault: types.LogLevel,
|
|
writeDefault: boolean,
|
|
): {
|
|
entries: [string, string][],
|
|
flags: string[],
|
|
write: boolean,
|
|
stdinContents: Uint8Array | null,
|
|
stdinResolveDir: string | null,
|
|
absWorkingDir: string | undefined,
|
|
nodePaths: string[],
|
|
mangleCache: MangleCache | undefined,
|
|
} {
|
|
let flags: string[] = []
|
|
let entries: [string, string][] = []
|
|
let keys: OptionKeys = Object.create(null)
|
|
let stdinContents: Uint8Array | null = null
|
|
let stdinResolveDir: string | null = null
|
|
pushLogFlags(flags, options, keys, isTTY, logLevelDefault)
|
|
pushCommonFlags(flags, options, keys)
|
|
|
|
let sourcemap = getFlag(options, keys, 'sourcemap', mustBeStringOrBoolean)
|
|
let bundle = getFlag(options, keys, 'bundle', mustBeBoolean)
|
|
let splitting = getFlag(options, keys, 'splitting', mustBeBoolean)
|
|
let preserveSymlinks = getFlag(options, keys, 'preserveSymlinks', mustBeBoolean)
|
|
let metafile = getFlag(options, keys, 'metafile', mustBeBoolean)
|
|
let outfile = getFlag(options, keys, 'outfile', mustBeString)
|
|
let outdir = getFlag(options, keys, 'outdir', mustBeString)
|
|
let outbase = getFlag(options, keys, 'outbase', mustBeString)
|
|
let tsconfig = getFlag(options, keys, 'tsconfig', mustBeString)
|
|
let resolveExtensions = getFlag(options, keys, 'resolveExtensions', mustBeArrayOfStrings)
|
|
let nodePathsInput = getFlag(options, keys, 'nodePaths', mustBeArrayOfStrings)
|
|
let mainFields = getFlag(options, keys, 'mainFields', mustBeArrayOfStrings)
|
|
let conditions = getFlag(options, keys, 'conditions', mustBeArrayOfStrings)
|
|
let external = getFlag(options, keys, 'external', mustBeArrayOfStrings)
|
|
let packages = getFlag(options, keys, 'packages', mustBeString)
|
|
let alias = getFlag(options, keys, 'alias', mustBeObject)
|
|
let loader = getFlag(options, keys, 'loader', mustBeObject)
|
|
let outExtension = getFlag(options, keys, 'outExtension', mustBeObject)
|
|
let publicPath = getFlag(options, keys, 'publicPath', mustBeString)
|
|
let entryNames = getFlag(options, keys, 'entryNames', mustBeString)
|
|
let chunkNames = getFlag(options, keys, 'chunkNames', mustBeString)
|
|
let assetNames = getFlag(options, keys, 'assetNames', mustBeString)
|
|
let inject = getFlag(options, keys, 'inject', mustBeArrayOfStrings)
|
|
let banner = getFlag(options, keys, 'banner', mustBeObject)
|
|
let footer = getFlag(options, keys, 'footer', mustBeObject)
|
|
let entryPoints = getFlag(options, keys, 'entryPoints', mustBeEntryPoints)
|
|
let absWorkingDir = getFlag(options, keys, 'absWorkingDir', mustBeString)
|
|
let stdin = getFlag(options, keys, 'stdin', mustBeObject)
|
|
let write = getFlag(options, keys, 'write', mustBeBoolean) ?? writeDefault; // Default to true if not specified
|
|
let allowOverwrite = getFlag(options, keys, 'allowOverwrite', mustBeBoolean)
|
|
let mangleCache = getFlag(options, keys, 'mangleCache', mustBeObject)
|
|
keys.plugins = true; // "plugins" has already been read earlier
|
|
checkForInvalidFlags(options, keys, `in ${callName}() call`)
|
|
|
|
if (sourcemap) flags.push(`--sourcemap${sourcemap === true ? '' : `=${sourcemap}`}`)
|
|
if (bundle) flags.push('--bundle')
|
|
if (allowOverwrite) flags.push('--allow-overwrite')
|
|
if (splitting) flags.push('--splitting')
|
|
if (preserveSymlinks) flags.push('--preserve-symlinks')
|
|
if (metafile) flags.push(`--metafile`)
|
|
if (outfile) flags.push(`--outfile=${outfile}`)
|
|
if (outdir) flags.push(`--outdir=${outdir}`)
|
|
if (outbase) flags.push(`--outbase=${outbase}`)
|
|
if (tsconfig) flags.push(`--tsconfig=${tsconfig}`)
|
|
if (packages) flags.push(`--packages=${packages}`)
|
|
if (resolveExtensions) flags.push(`--resolve-extensions=${validateAndJoinStringArray(resolveExtensions, 'resolve extension')}`)
|
|
if (publicPath) flags.push(`--public-path=${publicPath}`)
|
|
if (entryNames) flags.push(`--entry-names=${entryNames}`)
|
|
if (chunkNames) flags.push(`--chunk-names=${chunkNames}`)
|
|
if (assetNames) flags.push(`--asset-names=${assetNames}`)
|
|
if (mainFields) flags.push(`--main-fields=${validateAndJoinStringArray(mainFields, 'main field')}`)
|
|
if (conditions) flags.push(`--conditions=${validateAndJoinStringArray(conditions, 'condition')}`)
|
|
if (external) for (let name of external) flags.push(`--external:${validateStringValue(name, 'external')}`)
|
|
if (alias) {
|
|
for (let old in alias) {
|
|
if (old.indexOf('=') >= 0) throw new Error(`Invalid package name in alias: ${old}`)
|
|
flags.push(`--alias:${old}=${validateStringValue(alias[old], 'alias', old)}`)
|
|
}
|
|
}
|
|
if (banner) {
|
|
for (let type in banner) {
|
|
if (type.indexOf('=') >= 0) throw new Error(`Invalid banner file type: ${type}`)
|
|
flags.push(`--banner:${type}=${validateStringValue(banner[type], 'banner', type)}`)
|
|
}
|
|
}
|
|
if (footer) {
|
|
for (let type in footer) {
|
|
if (type.indexOf('=') >= 0) throw new Error(`Invalid footer file type: ${type}`)
|
|
flags.push(`--footer:${type}=${validateStringValue(footer[type], 'footer', type)}`)
|
|
}
|
|
}
|
|
if (inject) for (let path of inject) flags.push(`--inject:${validateStringValue(path, 'inject')}`)
|
|
if (loader) {
|
|
for (let ext in loader) {
|
|
if (ext.indexOf('=') >= 0) throw new Error(`Invalid loader extension: ${ext}`)
|
|
flags.push(`--loader:${ext}=${validateStringValue(loader[ext], 'loader', ext)}`)
|
|
}
|
|
}
|
|
if (outExtension) {
|
|
for (let ext in outExtension) {
|
|
if (ext.indexOf('=') >= 0) throw new Error(`Invalid out extension: ${ext}`)
|
|
flags.push(`--out-extension:${ext}=${validateStringValue(outExtension[ext], 'out extension', ext)}`)
|
|
}
|
|
}
|
|
|
|
if (entryPoints) {
|
|
if (Array.isArray(entryPoints)) {
|
|
for (let i = 0, n = entryPoints.length; i < n; i++) {
|
|
let entryPoint = entryPoints[i]
|
|
if (typeof entryPoint === 'object' && entryPoint !== null) {
|
|
let entryPointKeys: OptionKeys = Object.create(null)
|
|
let input = getFlag(entryPoint, entryPointKeys, 'in', mustBeString)
|
|
let output = getFlag(entryPoint, entryPointKeys, 'out', mustBeString)
|
|
checkForInvalidFlags(entryPoint, entryPointKeys, 'in entry point at index ' + i)
|
|
if (input === undefined) throw new Error('Missing property "in" for entry point at index ' + i)
|
|
if (output === undefined) throw new Error('Missing property "out" for entry point at index ' + i)
|
|
entries.push([output, input])
|
|
} else {
|
|
entries.push(['', validateStringValue(entryPoint, 'entry point at index ' + i)])
|
|
}
|
|
}
|
|
} else {
|
|
for (let key in entryPoints) {
|
|
entries.push([key, validateStringValue(entryPoints[key], 'entry point', key)])
|
|
}
|
|
}
|
|
}
|
|
|
|
if (stdin) {
|
|
let stdinKeys: OptionKeys = Object.create(null)
|
|
let contents = getFlag(stdin, stdinKeys, 'contents', mustBeStringOrUint8Array)
|
|
let resolveDir = getFlag(stdin, stdinKeys, 'resolveDir', mustBeString)
|
|
let sourcefile = getFlag(stdin, stdinKeys, 'sourcefile', mustBeString)
|
|
let loader = getFlag(stdin, stdinKeys, 'loader', mustBeString)
|
|
checkForInvalidFlags(stdin, stdinKeys, 'in "stdin" object')
|
|
|
|
if (sourcefile) flags.push(`--sourcefile=${sourcefile}`)
|
|
if (loader) flags.push(`--loader=${loader}`)
|
|
if (resolveDir) stdinResolveDir = resolveDir
|
|
if (typeof contents === 'string') stdinContents = protocol.encodeUTF8(contents)
|
|
else if (contents instanceof Uint8Array) stdinContents = contents
|
|
}
|
|
|
|
let nodePaths: string[] = []
|
|
if (nodePathsInput) {
|
|
for (let value of nodePathsInput) {
|
|
value += ''
|
|
nodePaths.push(value)
|
|
}
|
|
}
|
|
|
|
return {
|
|
entries,
|
|
flags,
|
|
write,
|
|
stdinContents,
|
|
stdinResolveDir,
|
|
absWorkingDir,
|
|
nodePaths,
|
|
mangleCache: validateMangleCache(mangleCache),
|
|
}
|
|
}
|
|
|
|
function flagsForTransformOptions(
|
|
callName: string,
|
|
options: types.TransformOptions,
|
|
isTTY: boolean,
|
|
logLevelDefault: types.LogLevel,
|
|
): {
|
|
flags: string[],
|
|
mangleCache: MangleCache | undefined,
|
|
} {
|
|
let flags: string[] = []
|
|
let keys: OptionKeys = Object.create(null)
|
|
pushLogFlags(flags, options, keys, isTTY, logLevelDefault)
|
|
pushCommonFlags(flags, options, keys)
|
|
|
|
let sourcemap = getFlag(options, keys, 'sourcemap', mustBeStringOrBoolean)
|
|
let sourcefile = getFlag(options, keys, 'sourcefile', mustBeString)
|
|
let loader = getFlag(options, keys, 'loader', mustBeString)
|
|
let banner = getFlag(options, keys, 'banner', mustBeString)
|
|
let footer = getFlag(options, keys, 'footer', mustBeString)
|
|
let mangleCache = getFlag(options, keys, 'mangleCache', mustBeObject)
|
|
checkForInvalidFlags(options, keys, `in ${callName}() call`)
|
|
|
|
if (sourcemap) flags.push(`--sourcemap=${sourcemap === true ? 'external' : sourcemap}`)
|
|
if (sourcefile) flags.push(`--sourcefile=${sourcefile}`)
|
|
if (loader) flags.push(`--loader=${loader}`)
|
|
if (banner) flags.push(`--banner=${banner}`)
|
|
if (footer) flags.push(`--footer=${footer}`)
|
|
|
|
return {
|
|
flags,
|
|
mangleCache: validateMangleCache(mangleCache),
|
|
}
|
|
}
|
|
|
|
export interface StreamIn {
|
|
writeToStdin: (data: Uint8Array) => void
|
|
readFileSync?: (path: string, encoding: 'utf8') => string
|
|
isSync: boolean
|
|
hasFS: boolean
|
|
esbuild: types.PluginBuild['esbuild']
|
|
}
|
|
|
|
export interface StreamOut {
|
|
readFromStdout: (data: Uint8Array) => void
|
|
afterClose: (error: Error | null) => void
|
|
service: StreamService
|
|
}
|
|
|
|
export interface StreamFS {
|
|
writeFile(contents: string | Uint8Array, callback: (path: string | null) => void): void
|
|
readFile(path: string, callback: (err: Error | null, contents: string | null) => void): void
|
|
}
|
|
|
|
export interface Refs {
|
|
ref(): void
|
|
unref(): void
|
|
}
|
|
|
|
export interface StreamService {
|
|
buildOrContext(args: {
|
|
callName: string,
|
|
refs: Refs | null,
|
|
options: types.BuildOptions,
|
|
isTTY: boolean,
|
|
defaultWD: string,
|
|
callback: (err: Error | null, res: types.BuildResult | types.BuildContext | null) => void,
|
|
}): void
|
|
|
|
transform(args: {
|
|
callName: string,
|
|
refs: Refs | null,
|
|
input: string | Uint8Array,
|
|
options: types.TransformOptions,
|
|
isTTY: boolean,
|
|
fs: StreamFS,
|
|
callback: (err: Error | null, res: types.TransformResult | null) => void,
|
|
}): void
|
|
|
|
formatMessages(args: {
|
|
callName: string,
|
|
refs: Refs | null,
|
|
messages: types.PartialMessage[],
|
|
options: types.FormatMessagesOptions,
|
|
callback: (err: Error | null, res: string[] | null) => void,
|
|
}): void
|
|
|
|
analyzeMetafile(args: {
|
|
callName: string,
|
|
refs: Refs | null,
|
|
metafile: string,
|
|
options: types.AnalyzeMetafileOptions | undefined,
|
|
callback: (err: Error | null, res: string | null) => void,
|
|
}): void
|
|
}
|
|
|
|
type CloseData = { didClose: boolean, reason: string }
|
|
type RequestCallback = (id: number, request: any) => Promise<void> | void
|
|
|
|
// This can't use any promises in the main execution flow because it must work
|
|
// for both sync and async code. There is an exception for plugin code because
|
|
// that can't work in sync code anyway.
|
|
export function createChannel(streamIn: StreamIn): StreamOut {
|
|
const requestCallbacksByKey: { [key: number]: { [command: string]: RequestCallback } } = {}
|
|
const closeData: CloseData = { didClose: false, reason: '' }
|
|
let responseCallbacks: { [id: number]: (error: string | null, response: protocol.Value) => void } = {}
|
|
let nextRequestID = 0
|
|
let nextBuildKey = 0
|
|
|
|
// Use a long-lived buffer to store stdout data
|
|
let stdout = new Uint8Array(16 * 1024)
|
|
let stdoutUsed = 0
|
|
let readFromStdout = (chunk: Uint8Array) => {
|
|
// Append the chunk to the stdout buffer, growing it as necessary
|
|
let limit = stdoutUsed + chunk.length
|
|
if (limit > stdout.length) {
|
|
let swap = new Uint8Array(limit * 2)
|
|
swap.set(stdout)
|
|
stdout = swap
|
|
}
|
|
stdout.set(chunk, stdoutUsed)
|
|
stdoutUsed += chunk.length
|
|
|
|
// Process all complete (i.e. not partial) packets
|
|
let offset = 0
|
|
while (offset + 4 <= stdoutUsed) {
|
|
let length = protocol.readUInt32LE(stdout, offset)
|
|
if (offset + 4 + length > stdoutUsed) {
|
|
break
|
|
}
|
|
offset += 4
|
|
handleIncomingPacket(stdout.subarray(offset, offset + length))
|
|
offset += length
|
|
}
|
|
if (offset > 0) {
|
|
stdout.copyWithin(0, offset, stdoutUsed)
|
|
stdoutUsed -= offset
|
|
}
|
|
}
|
|
|
|
let afterClose = (error: Error | null) => {
|
|
// When the process is closed, fail all pending requests
|
|
closeData.didClose = true
|
|
if (error) closeData.reason = ': ' + (error.message || error)
|
|
const text = 'The service was stopped' + closeData.reason
|
|
for (let id in responseCallbacks) {
|
|
responseCallbacks[id](text, null)
|
|
}
|
|
responseCallbacks = {}
|
|
}
|
|
|
|
let sendRequest = <Req, Res>(refs: Refs | null, value: Req, callback: (error: string | null, response: Res | null) => void): void => {
|
|
if (closeData.didClose) return callback('The service is no longer running' + closeData.reason, null)
|
|
let id = nextRequestID++
|
|
responseCallbacks[id] = (error, response) => {
|
|
try {
|
|
callback(error, response as any)
|
|
} finally {
|
|
if (refs) refs.unref() // Do this after the callback so the callback can extend the lifetime if needed
|
|
}
|
|
}
|
|
if (refs) refs.ref()
|
|
streamIn.writeToStdin(protocol.encodePacket({ id, isRequest: true, value: value as any }))
|
|
}
|
|
|
|
let sendResponse = (id: number, value: protocol.Value): void => {
|
|
if (closeData.didClose) throw new Error('The service is no longer running' + closeData.reason)
|
|
streamIn.writeToStdin(protocol.encodePacket({ id, isRequest: false, value }))
|
|
}
|
|
|
|
let handleRequest = async (id: number, request: any) => {
|
|
// Catch exceptions in the code below so they get passed to the caller
|
|
try {
|
|
if (request.command === 'ping') {
|
|
sendResponse(id, {})
|
|
return
|
|
}
|
|
|
|
if (typeof request.key === 'number') {
|
|
const requestCallbacks = requestCallbacksByKey[request.key]
|
|
if (!requestCallbacks) {
|
|
// Ignore invalid commands for old builds that no longer exist.
|
|
// This can happen when "context.cancel" and "context.dispose"
|
|
// is called while esbuild is processing many files in parallel.
|
|
// See https://github.com/evanw/esbuild/issues/3318 for details.
|
|
return
|
|
}
|
|
const callback = requestCallbacks[request.command]
|
|
if (callback) {
|
|
await callback(id, request)
|
|
return
|
|
}
|
|
}
|
|
|
|
throw new Error(`Invalid command: ` + request.command)
|
|
} catch (e) {
|
|
const errors = [extractErrorMessageV8(e, streamIn, null, void 0, '')]
|
|
try {
|
|
sendResponse(id, { errors } as any)
|
|
} catch {
|
|
// This may fail if the esbuild process is no longer running, but
|
|
// that's ok. Catch and swallow this exception so that we don't
|
|
// cause an unhandled promise rejection. Our caller isn't expecting
|
|
// this call to fail and doesn't handle the promise rejection.
|
|
}
|
|
}
|
|
}
|
|
|
|
let isFirstPacket = true
|
|
|
|
let handleIncomingPacket = (bytes: Uint8Array): void => {
|
|
// The first packet is a version check
|
|
if (isFirstPacket) {
|
|
isFirstPacket = false
|
|
|
|
// Validate the binary's version number to make sure esbuild was installed
|
|
// correctly. This check was added because some people have reported
|
|
// errors that appear to indicate an incorrect installation.
|
|
let binaryVersion = String.fromCharCode(...bytes)
|
|
if (binaryVersion !== ESBUILD_VERSION) {
|
|
throw new Error(`Cannot start service: Host version "${ESBUILD_VERSION}" does not match binary version ${quote(binaryVersion)}`)
|
|
}
|
|
return
|
|
}
|
|
|
|
let packet = protocol.decodePacket(bytes) as any
|
|
|
|
if (packet.isRequest) {
|
|
handleRequest(packet.id, packet.value)
|
|
}
|
|
|
|
else {
|
|
let callback = responseCallbacks[packet.id]!
|
|
delete responseCallbacks[packet.id]
|
|
if (packet.value.error) callback(packet.value.error, {})
|
|
else callback(null, packet.value)
|
|
}
|
|
}
|
|
|
|
let buildOrContext: StreamService['buildOrContext'] = ({ callName, refs, options, isTTY, defaultWD, callback }) => {
|
|
let refCount = 0
|
|
const buildKey = nextBuildKey++
|
|
const requestCallbacks: { [command: string]: RequestCallback } = {}
|
|
const buildRefs: Refs = {
|
|
ref() {
|
|
if (++refCount === 1) {
|
|
if (refs) refs.ref()
|
|
}
|
|
},
|
|
unref() {
|
|
if (--refCount === 0) {
|
|
delete requestCallbacksByKey[buildKey]
|
|
if (refs) refs.unref()
|
|
}
|
|
},
|
|
}
|
|
requestCallbacksByKey[buildKey] = requestCallbacks
|
|
|
|
// Guard the whole "build" request with a temporary ref count bump. We
|
|
// don't want the ref count to be bumped above zero and then back down
|
|
// to zero before the callback is called.
|
|
buildRefs.ref()
|
|
buildOrContextImpl(
|
|
callName,
|
|
buildKey,
|
|
sendRequest,
|
|
sendResponse,
|
|
buildRefs,
|
|
streamIn,
|
|
requestCallbacks,
|
|
options,
|
|
isTTY,
|
|
defaultWD,
|
|
(err, res) => {
|
|
// Now that the initial "build" request is done, we can release our
|
|
// temporary ref count bump. Any code that wants to extend the life
|
|
// of the build will have to do so by explicitly retaining a count.
|
|
try {
|
|
callback(err, res)
|
|
} finally {
|
|
buildRefs.unref()
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
let transform: StreamService['transform'] = ({ callName, refs, input, options, isTTY, fs, callback }) => {
|
|
const details = createObjectStash()
|
|
|
|
// Ideally the "transform()" API would be faster than calling "build()"
|
|
// since it doesn't need to touch the file system. However, performance
|
|
// measurements with large files on macOS indicate that sending the data
|
|
// over the stdio pipe can be 2x slower than just using a temporary file.
|
|
//
|
|
// This appears to be an OS limitation. Both the JavaScript and Go code
|
|
// are using large buffers but the pipe only writes data in 8kb chunks.
|
|
// An investigation seems to indicate that this number is hard-coded into
|
|
// the OS source code. Presumably files are faster because the OS uses
|
|
// a larger chunk size, or maybe even reads everything in one syscall.
|
|
//
|
|
// The cross-over size where this starts to be faster is around 1mb on
|
|
// my machine. In that case, this code tries to use a temporary file if
|
|
// possible but falls back to sending the data over the stdio pipe if
|
|
// that doesn't work.
|
|
let start = (inputPath: string | null) => {
|
|
try {
|
|
if (typeof input !== 'string' && !(input instanceof Uint8Array))
|
|
throw new Error('The input to "transform" must be a string or a Uint8Array')
|
|
let {
|
|
flags,
|
|
mangleCache,
|
|
} = flagsForTransformOptions(callName, options, isTTY, transformLogLevelDefault)
|
|
let request: protocol.TransformRequest = {
|
|
command: 'transform',
|
|
flags,
|
|
inputFS: inputPath !== null,
|
|
input: inputPath !== null ? protocol.encodeUTF8(inputPath)
|
|
: typeof input === 'string' ? protocol.encodeUTF8(input)
|
|
: input,
|
|
}
|
|
if (mangleCache) request.mangleCache = mangleCache
|
|
sendRequest<protocol.TransformRequest, protocol.TransformResponse>(refs, request, (error, response) => {
|
|
if (error) return callback(new Error(error), null)
|
|
let errors = replaceDetailsInMessages(response!.errors, details)
|
|
let warnings = replaceDetailsInMessages(response!.warnings, details)
|
|
let outstanding = 1
|
|
let next = () => {
|
|
if (--outstanding === 0) {
|
|
let result: types.TransformResult = {
|
|
warnings,
|
|
code: response!.code,
|
|
map: response!.map,
|
|
mangleCache: undefined,
|
|
legalComments: undefined,
|
|
}
|
|
if ('legalComments' in response!) result.legalComments = response?.legalComments
|
|
if (response!.mangleCache) result.mangleCache = response?.mangleCache
|
|
callback(null, result)
|
|
}
|
|
}
|
|
if (errors.length > 0) return callback(failureErrorWithLog('Transform failed', errors, warnings), null)
|
|
|
|
// Read the JavaScript file from the file system
|
|
if (response!.codeFS) {
|
|
outstanding++
|
|
fs.readFile(response!.code, (err, contents) => {
|
|
if (err !== null) {
|
|
callback(err, null)
|
|
} else {
|
|
response!.code = contents!
|
|
next()
|
|
}
|
|
})
|
|
}
|
|
|
|
// Read the source map file from the file system
|
|
if (response!.mapFS) {
|
|
outstanding++
|
|
fs.readFile(response!.map, (err, contents) => {
|
|
if (err !== null) {
|
|
callback(err, null)
|
|
} else {
|
|
response!.map = contents!
|
|
next()
|
|
}
|
|
})
|
|
}
|
|
|
|
next()
|
|
})
|
|
} catch (e) {
|
|
let flags: string[] = []
|
|
try { pushLogFlags(flags, options, {}, isTTY, transformLogLevelDefault) } catch { }
|
|
const error = extractErrorMessageV8(e, streamIn, details, void 0, '')
|
|
sendRequest(refs, { command: 'error', flags, error }, () => {
|
|
error.detail = details.load(error.detail)
|
|
callback(failureErrorWithLog('Transform failed', [error], []), null)
|
|
})
|
|
}
|
|
}
|
|
if ((typeof input === 'string' || input instanceof Uint8Array) && input.length > 1024 * 1024) {
|
|
let next = start
|
|
start = () => fs.writeFile(input, next)
|
|
}
|
|
start(null)
|
|
}
|
|
|
|
let formatMessages: StreamService['formatMessages'] = ({ callName, refs, messages, options, callback }) => {
|
|
if (!options) throw new Error(`Missing second argument in ${callName}() call`)
|
|
let keys: OptionKeys = {}
|
|
let kind = getFlag(options, keys, 'kind', mustBeString)
|
|
let color = getFlag(options, keys, 'color', mustBeBoolean)
|
|
let terminalWidth = getFlag(options, keys, 'terminalWidth', mustBeInteger)
|
|
checkForInvalidFlags(options, keys, `in ${callName}() call`)
|
|
if (kind === void 0) throw new Error(`Missing "kind" in ${callName}() call`)
|
|
if (kind !== 'error' && kind !== 'warning') throw new Error(`Expected "kind" to be "error" or "warning" in ${callName}() call`)
|
|
let request: protocol.FormatMsgsRequest = {
|
|
command: 'format-msgs',
|
|
messages: sanitizeMessages(messages, 'messages', null, '', terminalWidth),
|
|
isWarning: kind === 'warning',
|
|
}
|
|
if (color !== void 0) request.color = color
|
|
if (terminalWidth !== void 0) request.terminalWidth = terminalWidth
|
|
sendRequest<protocol.FormatMsgsRequest, protocol.FormatMsgsResponse>(refs, request, (error, response) => {
|
|
if (error) return callback(new Error(error), null)
|
|
callback(null, response!.messages)
|
|
})
|
|
}
|
|
|
|
let analyzeMetafile: StreamService['analyzeMetafile'] = ({ callName, refs, metafile, options, callback }) => {
|
|
if (options === void 0) options = {}
|
|
let keys: OptionKeys = {}
|
|
let color = getFlag(options, keys, 'color', mustBeBoolean)
|
|
let verbose = getFlag(options, keys, 'verbose', mustBeBoolean)
|
|
checkForInvalidFlags(options, keys, `in ${callName}() call`)
|
|
let request: protocol.AnalyzeMetafileRequest = {
|
|
command: 'analyze-metafile',
|
|
metafile,
|
|
}
|
|
if (color !== void 0) request.color = color
|
|
if (verbose !== void 0) request.verbose = verbose
|
|
sendRequest<protocol.AnalyzeMetafileRequest, protocol.AnalyzeMetafileResponse>(refs, request, (error, response) => {
|
|
if (error) return callback(new Error(error), null)
|
|
callback(null, response!.result)
|
|
})
|
|
}
|
|
|
|
return {
|
|
readFromStdout,
|
|
afterClose,
|
|
service: {
|
|
buildOrContext,
|
|
transform,
|
|
formatMessages,
|
|
analyzeMetafile,
|
|
},
|
|
}
|
|
}
|
|
|
|
function buildOrContextImpl(
|
|
callName: string,
|
|
buildKey: number,
|
|
sendRequest: <Req, Res>(refs: Refs | null, value: Req, callback: (error: string | null, response: Res | null) => void) => void,
|
|
sendResponse: (id: number, value: protocol.Value) => void,
|
|
refs: Refs,
|
|
streamIn: StreamIn,
|
|
requestCallbacks: { [command: string]: RequestCallback },
|
|
options: types.BuildOptions,
|
|
isTTY: boolean,
|
|
defaultWD: string,
|
|
callback: (err: Error | null, res: types.BuildResult | types.BuildContext | null) => void,
|
|
): void {
|
|
const details = createObjectStash()
|
|
const isContext = callName === 'context'
|
|
|
|
const handleError = (e: any, pluginName: string): void => {
|
|
const flags: string[] = []
|
|
try { pushLogFlags(flags, options, {}, isTTY, buildLogLevelDefault) } catch { }
|
|
const message = extractErrorMessageV8(e, streamIn, details, void 0, pluginName)
|
|
sendRequest(refs, { command: 'error', flags, error: message }, () => {
|
|
message.detail = details.load(message.detail)
|
|
callback(failureErrorWithLog(isContext ? 'Context failed' : 'Build failed', [message], []), null)
|
|
})
|
|
}
|
|
|
|
let plugins: types.Plugin[] | undefined
|
|
if (typeof options === 'object') {
|
|
const value = options.plugins
|
|
if (value !== void 0) {
|
|
if (!Array.isArray(value)) return handleError(new Error(`"plugins" must be an array`), '')
|
|
plugins = value
|
|
}
|
|
}
|
|
|
|
if (plugins && plugins.length > 0) {
|
|
if (streamIn.isSync) return handleError(new Error('Cannot use plugins in synchronous API calls'), '')
|
|
|
|
// Plugins can use async/await because they can't be run with "buildSync"
|
|
handlePlugins(
|
|
buildKey,
|
|
sendRequest,
|
|
sendResponse,
|
|
refs,
|
|
streamIn,
|
|
requestCallbacks,
|
|
options,
|
|
plugins,
|
|
details,
|
|
).then(
|
|
result => {
|
|
if (!result.ok) return handleError(result.error, result.pluginName)
|
|
try {
|
|
buildOrContextContinue(result.requestPlugins, result.runOnEndCallbacks, result.scheduleOnDisposeCallbacks)
|
|
} catch (e) {
|
|
handleError(e, '')
|
|
}
|
|
},
|
|
e => handleError(e, ''),
|
|
)
|
|
return
|
|
}
|
|
|
|
try {
|
|
buildOrContextContinue(null, (result, done) => done([], []), () => { })
|
|
} catch (e) {
|
|
handleError(e, '')
|
|
}
|
|
|
|
// "buildOrContext" cannot be written using async/await due to "buildSync"
|
|
// and must be written in continuation-passing style instead
|
|
function buildOrContextContinue(requestPlugins: protocol.BuildPlugin[] | null, runOnEndCallbacks: RunOnEndCallbacks, scheduleOnDisposeCallbacks: () => void) {
|
|
const writeDefault = streamIn.hasFS
|
|
const {
|
|
entries,
|
|
flags,
|
|
write,
|
|
stdinContents,
|
|
stdinResolveDir,
|
|
absWorkingDir,
|
|
nodePaths,
|
|
mangleCache,
|
|
} = flagsForBuildOptions(callName, options, isTTY, buildLogLevelDefault, writeDefault)
|
|
if (write && !streamIn.hasFS) throw new Error(`The "write" option is unavailable in this environment`)
|
|
|
|
// Construct the request
|
|
const request: protocol.BuildRequest = {
|
|
command: 'build',
|
|
key: buildKey,
|
|
entries,
|
|
flags,
|
|
write,
|
|
stdinContents,
|
|
stdinResolveDir,
|
|
absWorkingDir: absWorkingDir || defaultWD,
|
|
nodePaths,
|
|
context: isContext,
|
|
}
|
|
if (requestPlugins) request.plugins = requestPlugins
|
|
if (mangleCache) request.mangleCache = mangleCache
|
|
|
|
// Factor out response handling so it can be reused for rebuilds
|
|
const buildResponseToResult = (
|
|
response: protocol.BuildResponse | null,
|
|
callback: (error: types.BuildFailure | null, result: types.BuildResult | null, onEndErrors: types.Message[], onEndWarnings: types.Message[]) => void,
|
|
): void => {
|
|
const result: types.BuildResult = {
|
|
errors: replaceDetailsInMessages(response!.errors, details),
|
|
warnings: replaceDetailsInMessages(response!.warnings, details),
|
|
outputFiles: undefined,
|
|
metafile: undefined,
|
|
mangleCache: undefined,
|
|
}
|
|
const originalErrors = result.errors.slice()
|
|
const originalWarnings = result.warnings.slice()
|
|
if (response!.outputFiles) result.outputFiles = response!.outputFiles.map(convertOutputFiles)
|
|
if (response!.metafile) result.metafile = JSON.parse(response!.metafile)
|
|
if (response!.mangleCache) result.mangleCache = response!.mangleCache
|
|
if (response!.writeToStdout !== void 0) console.log(protocol.decodeUTF8(response!.writeToStdout).replace(/\n$/, ''))
|
|
runOnEndCallbacks(result, (onEndErrors, onEndWarnings) => {
|
|
if (originalErrors.length > 0 || onEndErrors.length > 0) {
|
|
const error = failureErrorWithLog('Build failed', originalErrors.concat(onEndErrors), originalWarnings.concat(onEndWarnings))
|
|
return callback(error, null, onEndErrors, onEndWarnings)
|
|
}
|
|
callback(null, result, onEndErrors, onEndWarnings)
|
|
})
|
|
}
|
|
|
|
// In context mode, Go runs the "onEnd" callbacks instead of JavaScript
|
|
let latestResultPromise: Promise<types.BuildResult> | undefined
|
|
let provideLatestResult: ((error: types.BuildFailure | null, result: types.BuildResult | null) => void) | undefined
|
|
if (isContext)
|
|
requestCallbacks['on-end'] = (id, request: protocol.OnEndRequest) =>
|
|
new Promise(resolve => {
|
|
buildResponseToResult(request, (err, result, onEndErrors, onEndWarnings) => {
|
|
const response: protocol.OnEndResponse = {
|
|
errors: onEndErrors,
|
|
warnings: onEndWarnings,
|
|
}
|
|
if (provideLatestResult) provideLatestResult(err, result)
|
|
latestResultPromise = undefined
|
|
provideLatestResult = undefined
|
|
sendResponse(id, response as any)
|
|
resolve()
|
|
})
|
|
})
|
|
|
|
sendRequest<protocol.BuildRequest, protocol.BuildResponse>(refs, request, (error, response) => {
|
|
if (error) return callback(new Error(error), null)
|
|
if (!isContext) {
|
|
return buildResponseToResult(response!, (err, res) => {
|
|
scheduleOnDisposeCallbacks()
|
|
return callback(err, res)
|
|
})
|
|
}
|
|
|
|
// Construct a context object
|
|
if (response!.errors.length > 0) {
|
|
return callback(failureErrorWithLog('Context failed', response!.errors, response!.warnings), null)
|
|
}
|
|
let didDispose = false
|
|
const result: types.BuildContext = {
|
|
rebuild: () => {
|
|
if (!latestResultPromise) latestResultPromise = new Promise((resolve, reject) => {
|
|
let settlePromise: (() => void) | undefined
|
|
provideLatestResult = (err, result) => {
|
|
if (!settlePromise) settlePromise = () => err ? reject(err) : resolve(result!)
|
|
}
|
|
const triggerAnotherBuild = (): void => {
|
|
const request: protocol.RebuildRequest = {
|
|
command: 'rebuild',
|
|
key: buildKey,
|
|
}
|
|
sendRequest<protocol.RebuildRequest, protocol.RebuildResponse>(refs, request, (error, response) => {
|
|
if (error) {
|
|
reject(new Error(error))
|
|
} else if (settlePromise) {
|
|
// It's possible to settle the promise that we returned from
|
|
// this "rebuild()" function earlier than this point. However,
|
|
// at that point the user could call "rebuild()" again which
|
|
// would unexpectedly merge with the same build that's still
|
|
// ongoing. To prevent that, we defer settling the promise
|
|
// until now when we know that the build has finished.
|
|
settlePromise()
|
|
} else {
|
|
// When we call "rebuild()", we call out to the Go "Rebuild()"
|
|
// API over IPC. That may trigger a build, but may also "join"
|
|
// an existing build. At some point the Go code sends us an
|
|
// "on-end" message with the build result to tell us to run
|
|
// our "onEnd" plugins. We capture that build result and return
|
|
// it here.
|
|
//
|
|
// However, there's a potential problem: For performance, the
|
|
// Go code will only send us the result if it's needed, which
|
|
// only happens if there are "onEnd" callbacks or if "rebuild"
|
|
// was called. So there's a race where the following things
|
|
// happen:
|
|
//
|
|
// 1. Go starts a rebuild (e.g. due to watch mode)
|
|
// 2. JS calls "rebuild()"
|
|
// 3. Go ends the build and starts Go's "OnEnd" callback
|
|
// 4. Go's "OnEnd" callback sees no need to send the result
|
|
// 5. JS asks Go to rebuild, which merges with the existing build
|
|
// 6. Go's existing build ends
|
|
// 7. The merged build ends, which wakes up JS and ends up here
|
|
//
|
|
// In that situation we didn't get an "on-end" message since
|
|
// Go thought it wasn't necessary. In that situation, we
|
|
// trigger another rebuild below so that Go will (almost
|
|
// surely) send us an "on-end" message next time. I suspect
|
|
// that this is a very rare case, so the performance impact
|
|
// of building twice shouldn't really matter. It also only
|
|
// happens when "rebuild()" is used with "watch()" and/or
|
|
// "serve()".
|
|
triggerAnotherBuild()
|
|
}
|
|
})
|
|
}
|
|
triggerAnotherBuild()
|
|
})
|
|
return latestResultPromise
|
|
},
|
|
|
|
watch: (options = {}) => new Promise((resolve, reject) => {
|
|
if (!streamIn.hasFS) throw new Error(`Cannot use the "watch" API in this environment`)
|
|
const keys: OptionKeys = {}
|
|
checkForInvalidFlags(options, keys, `in watch() call`)
|
|
const request: protocol.WatchRequest = {
|
|
command: 'watch',
|
|
key: buildKey,
|
|
}
|
|
sendRequest<protocol.WatchRequest, null>(refs, request, error => {
|
|
if (error) reject(new Error(error))
|
|
else resolve(undefined)
|
|
})
|
|
}),
|
|
|
|
serve: (options = {}) => new Promise((resolve, reject) => {
|
|
if (!streamIn.hasFS) throw new Error(`Cannot use the "serve" API in this environment`)
|
|
const keys: OptionKeys = {}
|
|
const port = getFlag(options, keys, 'port', mustBeValidPortNumber)
|
|
const host = getFlag(options, keys, 'host', mustBeString)
|
|
const servedir = getFlag(options, keys, 'servedir', mustBeString)
|
|
const keyfile = getFlag(options, keys, 'keyfile', mustBeString)
|
|
const certfile = getFlag(options, keys, 'certfile', mustBeString)
|
|
const fallback = getFlag(options, keys, 'fallback', mustBeString)
|
|
const cors = getFlag(options, keys, 'cors', mustBeObject)
|
|
const onRequest = getFlag(options, keys, 'onRequest', mustBeFunction)
|
|
checkForInvalidFlags(options, keys, `in serve() call`)
|
|
|
|
const request: protocol.ServeRequest = {
|
|
command: 'serve',
|
|
key: buildKey,
|
|
onRequest: !!onRequest,
|
|
}
|
|
if (port !== void 0) request.port = port
|
|
if (host !== void 0) request.host = host
|
|
if (servedir !== void 0) request.servedir = servedir
|
|
if (keyfile !== void 0) request.keyfile = keyfile
|
|
if (certfile !== void 0) request.certfile = certfile
|
|
if (fallback !== void 0) request.fallback = fallback
|
|
|
|
if (cors) {
|
|
const corsKeys: OptionKeys = {}
|
|
const origin = getFlag(cors, corsKeys, 'origin', mustBeStringOrArrayOfStrings)
|
|
checkForInvalidFlags(cors, corsKeys, `on "cors" object`)
|
|
if (Array.isArray(origin)) request.corsOrigin = origin
|
|
else if (origin !== void 0) request.corsOrigin = [origin]
|
|
}
|
|
|
|
sendRequest<protocol.ServeRequest, protocol.ServeResponse>(refs, request, (error, response) => {
|
|
if (error) return reject(new Error(error))
|
|
if (onRequest) {
|
|
requestCallbacks['serve-request'] = (id, request: protocol.OnServeRequest) => {
|
|
onRequest(request.args)
|
|
sendResponse(id, {})
|
|
}
|
|
}
|
|
resolve(response!)
|
|
})
|
|
}),
|
|
|
|
cancel: () => new Promise(resolve => {
|
|
if (didDispose) return resolve()
|
|
const request: protocol.CancelRequest = {
|
|
command: 'cancel',
|
|
key: buildKey,
|
|
}
|
|
sendRequest<protocol.CancelRequest, null>(refs, request, () => {
|
|
resolve(); // We don't care about errors here
|
|
})
|
|
}),
|
|
|
|
dispose: () => new Promise(resolve => {
|
|
if (didDispose) return resolve()
|
|
didDispose = true // Don't dispose more than once
|
|
const request: protocol.DisposeRequest = {
|
|
command: 'dispose',
|
|
key: buildKey,
|
|
}
|
|
sendRequest<protocol.DisposeRequest, null>(refs, request, () => {
|
|
resolve(); // We don't care about errors here
|
|
scheduleOnDisposeCallbacks()
|
|
|
|
// Only remove the reference here when we know the Go code has seen
|
|
// this "dispose" call. We don't want to remove any registered
|
|
// callbacks before that point because the Go code might still be
|
|
// sending us events. If we remove the reference earlier then we
|
|
// will return errors for those events, which may end up being
|
|
// printed to the terminal where the user can see them, which would
|
|
// be very confusing.
|
|
refs.unref()
|
|
})
|
|
}),
|
|
}
|
|
refs.ref(); // Keep a reference until "dispose" is called
|
|
callback(null, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
type RunOnEndCallbacks = (result: types.BuildResult, done: (errors: types.Message[], warnings: types.Message[]) => void) => void
|
|
|
|
let handlePlugins = async (
|
|
buildKey: number,
|
|
sendRequest: <Req, Res>(refs: Refs | null, value: Req, callback: (error: string | null, response: Res | null) => void) => void,
|
|
sendResponse: (id: number, value: protocol.Value) => void,
|
|
refs: Refs,
|
|
streamIn: StreamIn,
|
|
requestCallbacks: { [command: string]: RequestCallback },
|
|
initialOptions: types.BuildOptions,
|
|
plugins: types.Plugin[],
|
|
details: ObjectStash,
|
|
): Promise<
|
|
| { ok: true, requestPlugins: protocol.BuildPlugin[], runOnEndCallbacks: RunOnEndCallbacks, scheduleOnDisposeCallbacks: () => void }
|
|
| { ok: false, error: any, pluginName: string }
|
|
> => {
|
|
let onStartCallbacks: {
|
|
name: string,
|
|
note: () => types.Note | undefined,
|
|
callback: () =>
|
|
(types.OnStartResult | null | void | Promise<types.OnStartResult | null | void>),
|
|
}[] = []
|
|
|
|
let onEndCallbacks: {
|
|
name: string,
|
|
note: () => types.Note | undefined,
|
|
callback: (result: types.BuildResult) =>
|
|
(types.OnEndResult | null | void | Promise<types.OnEndResult | null | void>),
|
|
}[] = []
|
|
|
|
let onResolveCallbacks: {
|
|
[id: number]: {
|
|
name: string,
|
|
note: () => types.Note | undefined,
|
|
callback: (args: types.OnResolveArgs) =>
|
|
(types.OnResolveResult | null | undefined | Promise<types.OnResolveResult | null | undefined>),
|
|
},
|
|
} = {}
|
|
|
|
let onLoadCallbacks: {
|
|
[id: number]: {
|
|
name: string,
|
|
note: () => types.Note | undefined,
|
|
callback: (args: types.OnLoadArgs) =>
|
|
(types.OnLoadResult | null | undefined | Promise<types.OnLoadResult | null | undefined>),
|
|
},
|
|
} = {}
|
|
|
|
let onDisposeCallbacks: (() => void)[] = []
|
|
let nextCallbackID = 0
|
|
let i = 0
|
|
let requestPlugins: protocol.BuildPlugin[] = []
|
|
let isSetupDone = false
|
|
|
|
// Clone the plugin array to guard against mutation during iteration
|
|
plugins = [...plugins]
|
|
|
|
for (let item of plugins) {
|
|
let keys: OptionKeys = {}
|
|
if (typeof item !== 'object') throw new Error(`Plugin at index ${i} must be an object`)
|
|
const name = getFlag(item, keys, 'name', mustBeString)
|
|
if (typeof name !== 'string' || name === '') throw new Error(`Plugin at index ${i} is missing a name`)
|
|
try {
|
|
let setup = getFlag(item, keys, 'setup', mustBeFunction)
|
|
if (typeof setup !== 'function') throw new Error(`Plugin is missing a setup function`)
|
|
checkForInvalidFlags(item, keys, `on plugin ${quote(name)}`)
|
|
|
|
let plugin: protocol.BuildPlugin = {
|
|
name,
|
|
onStart: false,
|
|
onEnd: false,
|
|
onResolve: [],
|
|
onLoad: [],
|
|
}
|
|
i++
|
|
|
|
let resolve = (path: string, options: types.ResolveOptions = {}): Promise<types.ResolveResult> => {
|
|
if (!isSetupDone) throw new Error('Cannot call "resolve" before plugin setup has completed')
|
|
if (typeof path !== 'string') throw new Error(`The path to resolve must be a string`)
|
|
let keys: OptionKeys = Object.create(null)
|
|
let pluginName = getFlag(options, keys, 'pluginName', mustBeString)
|
|
let importer = getFlag(options, keys, 'importer', mustBeString)
|
|
let namespace = getFlag(options, keys, 'namespace', mustBeString)
|
|
let resolveDir = getFlag(options, keys, 'resolveDir', mustBeString)
|
|
let kind = getFlag(options, keys, 'kind', mustBeString)
|
|
let pluginData = getFlag(options, keys, 'pluginData', canBeAnything)
|
|
let importAttributes = getFlag(options, keys, 'with', mustBeObject)
|
|
checkForInvalidFlags(options, keys, 'in resolve() call')
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const request: protocol.ResolveRequest = {
|
|
command: 'resolve',
|
|
path,
|
|
key: buildKey,
|
|
pluginName: name,
|
|
}
|
|
if (pluginName != null) request.pluginName = pluginName
|
|
if (importer != null) request.importer = importer
|
|
if (namespace != null) request.namespace = namespace
|
|
if (resolveDir != null) request.resolveDir = resolveDir
|
|
if (kind != null) request.kind = kind
|
|
else throw new Error(`Must specify "kind" when calling "resolve"`)
|
|
if (pluginData != null) request.pluginData = details.store(pluginData)
|
|
if (importAttributes != null) request.with = sanitizeStringMap(importAttributes, 'with')
|
|
|
|
sendRequest<protocol.ResolveRequest, protocol.ResolveResponse>(refs, request, (error, response) => {
|
|
if (error !== null) reject(new Error(error))
|
|
else resolve({
|
|
errors: replaceDetailsInMessages(response!.errors, details),
|
|
warnings: replaceDetailsInMessages(response!.warnings, details),
|
|
path: response!.path,
|
|
external: response!.external,
|
|
sideEffects: response!.sideEffects,
|
|
namespace: response!.namespace,
|
|
suffix: response!.suffix,
|
|
pluginData: details.load(response!.pluginData),
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
let promise = setup({
|
|
initialOptions,
|
|
|
|
resolve,
|
|
|
|
onStart(callback) {
|
|
let registeredText = `This error came from the "onStart" callback registered here:`
|
|
let registeredNote = extractCallerV8(new Error(registeredText), streamIn, 'onStart')
|
|
onStartCallbacks.push({ name: name!, callback, note: registeredNote })
|
|
plugin.onStart = true
|
|
},
|
|
|
|
onEnd(callback) {
|
|
let registeredText = `This error came from the "onEnd" callback registered here:`
|
|
let registeredNote = extractCallerV8(new Error(registeredText), streamIn, 'onEnd')
|
|
onEndCallbacks.push({ name: name!, callback, note: registeredNote })
|
|
plugin.onEnd = true
|
|
},
|
|
|
|
onResolve(options, callback) {
|
|
let registeredText = `This error came from the "onResolve" callback registered here:`
|
|
let registeredNote = extractCallerV8(new Error(registeredText), streamIn, 'onResolve')
|
|
let keys: OptionKeys = {}
|
|
let filter = getFlag(options, keys, 'filter', mustBeRegExp)
|
|
let namespace = getFlag(options, keys, 'namespace', mustBeString)
|
|
checkForInvalidFlags(options, keys, `in onResolve() call for plugin ${quote(name)}`)
|
|
if (filter == null) throw new Error(`onResolve() call is missing a filter`)
|
|
let id = nextCallbackID++
|
|
onResolveCallbacks[id] = { name: name!, callback, note: registeredNote }
|
|
plugin.onResolve.push({ id, filter: jsRegExpToGoRegExp(filter), namespace: namespace || '' })
|
|
},
|
|
|
|
onLoad(options, callback) {
|
|
let registeredText = `This error came from the "onLoad" callback registered here:`
|
|
let registeredNote = extractCallerV8(new Error(registeredText), streamIn, 'onLoad')
|
|
let keys: OptionKeys = {}
|
|
let filter = getFlag(options, keys, 'filter', mustBeRegExp)
|
|
let namespace = getFlag(options, keys, 'namespace', mustBeString)
|
|
checkForInvalidFlags(options, keys, `in onLoad() call for plugin ${quote(name)}`)
|
|
if (filter == null) throw new Error(`onLoad() call is missing a filter`)
|
|
let id = nextCallbackID++
|
|
onLoadCallbacks[id] = { name: name!, callback, note: registeredNote }
|
|
plugin.onLoad.push({ id, filter: jsRegExpToGoRegExp(filter), namespace: namespace || '' })
|
|
},
|
|
|
|
onDispose(callback) {
|
|
onDisposeCallbacks.push(callback)
|
|
},
|
|
|
|
esbuild: streamIn.esbuild,
|
|
})
|
|
|
|
// Await a returned promise if there was one. This allows plugins to do
|
|
// some asynchronous setup while still retaining the ability to modify
|
|
// the build options. This deliberately serializes asynchronous plugin
|
|
// setup instead of running them concurrently so that build option
|
|
// modifications are easier to reason about.
|
|
if (promise) await promise
|
|
|
|
requestPlugins.push(plugin)
|
|
} catch (e) {
|
|
return { ok: false, error: e, pluginName: name }
|
|
}
|
|
}
|
|
|
|
requestCallbacks['on-start'] = async (id, request: protocol.OnStartRequest) => {
|
|
// Reset the "pluginData" map before each new build to avoid a memory leak.
|
|
// This is done before each new build begins instead of after each build ends
|
|
// because I believe the current API doesn't restrict when you can call
|
|
// "resolve" and there may be some uses of it that call it around when the
|
|
// build ends, and we don't want to accidentally break those use cases.
|
|
details.clear()
|
|
|
|
let response: protocol.OnStartResponse = { errors: [], warnings: [] }
|
|
await Promise.all(onStartCallbacks.map(async ({ name, callback, note }) => {
|
|
try {
|
|
let result = await callback()
|
|
|
|
if (result != null) {
|
|
if (typeof result !== 'object') throw new Error(`Expected onStart() callback in plugin ${quote(name)} to return an object`)
|
|
let keys: OptionKeys = {}
|
|
let errors = getFlag(result, keys, 'errors', mustBeArray)
|
|
let warnings = getFlag(result, keys, 'warnings', mustBeArray)
|
|
checkForInvalidFlags(result, keys, `from onStart() callback in plugin ${quote(name)}`)
|
|
|
|
if (errors != null) response.errors!.push(...sanitizeMessages(errors, 'errors', details, name, undefined))
|
|
if (warnings != null) response.warnings!.push(...sanitizeMessages(warnings, 'warnings', details, name, undefined))
|
|
}
|
|
} catch (e) {
|
|
response.errors!.push(extractErrorMessageV8(e, streamIn, details, note && note(), name))
|
|
}
|
|
}))
|
|
sendResponse(id, response as any)
|
|
}
|
|
|
|
requestCallbacks['on-resolve'] = async (id, request: protocol.OnResolveRequest) => {
|
|
let response: protocol.OnResolveResponse = {}, name = '', callback, note
|
|
for (let id of request.ids) {
|
|
try {
|
|
({ name, callback, note } = onResolveCallbacks[id])
|
|
let result = await callback({
|
|
path: request.path,
|
|
importer: request.importer,
|
|
namespace: request.namespace,
|
|
resolveDir: request.resolveDir,
|
|
kind: request.kind,
|
|
pluginData: details.load(request.pluginData),
|
|
with: request.with,
|
|
})
|
|
|
|
if (result != null) {
|
|
if (typeof result !== 'object') throw new Error(`Expected onResolve() callback in plugin ${quote(name)} to return an object`)
|
|
let keys: OptionKeys = {}
|
|
let pluginName = getFlag(result, keys, 'pluginName', mustBeString)
|
|
let path = getFlag(result, keys, 'path', mustBeString)
|
|
let namespace = getFlag(result, keys, 'namespace', mustBeString)
|
|
let suffix = getFlag(result, keys, 'suffix', mustBeString)
|
|
let external = getFlag(result, keys, 'external', mustBeBoolean)
|
|
let sideEffects = getFlag(result, keys, 'sideEffects', mustBeBoolean)
|
|
let pluginData = getFlag(result, keys, 'pluginData', canBeAnything)
|
|
let errors = getFlag(result, keys, 'errors', mustBeArray)
|
|
let warnings = getFlag(result, keys, 'warnings', mustBeArray)
|
|
let watchFiles = getFlag(result, keys, 'watchFiles', mustBeArrayOfStrings)
|
|
let watchDirs = getFlag(result, keys, 'watchDirs', mustBeArrayOfStrings)
|
|
checkForInvalidFlags(result, keys, `from onResolve() callback in plugin ${quote(name)}`)
|
|
|
|
response.id = id
|
|
if (pluginName != null) response.pluginName = pluginName
|
|
if (path != null) response.path = path
|
|
if (namespace != null) response.namespace = namespace
|
|
if (suffix != null) response.suffix = suffix
|
|
if (external != null) response.external = external
|
|
if (sideEffects != null) response.sideEffects = sideEffects
|
|
if (pluginData != null) response.pluginData = details.store(pluginData)
|
|
if (errors != null) response.errors = sanitizeMessages(errors, 'errors', details, name, undefined)
|
|
if (warnings != null) response.warnings = sanitizeMessages(warnings, 'warnings', details, name, undefined)
|
|
if (watchFiles != null) response.watchFiles = sanitizeStringArray(watchFiles, 'watchFiles')
|
|
if (watchDirs != null) response.watchDirs = sanitizeStringArray(watchDirs, 'watchDirs')
|
|
break
|
|
}
|
|
} catch (e) {
|
|
response = { id, errors: [extractErrorMessageV8(e, streamIn, details, note && note(), name)] }
|
|
break
|
|
}
|
|
}
|
|
sendResponse(id, response as any)
|
|
}
|
|
|
|
requestCallbacks['on-load'] = async (id, request: protocol.OnLoadRequest) => {
|
|
let response: protocol.OnLoadResponse = {}, name = '', callback, note
|
|
for (let id of request.ids) {
|
|
try {
|
|
({ name, callback, note } = onLoadCallbacks[id])
|
|
let result = await callback({
|
|
path: request.path,
|
|
namespace: request.namespace,
|
|
suffix: request.suffix,
|
|
pluginData: details.load(request.pluginData),
|
|
with: request.with,
|
|
})
|
|
|
|
if (result != null) {
|
|
if (typeof result !== 'object') throw new Error(`Expected onLoad() callback in plugin ${quote(name)} to return an object`)
|
|
let keys: OptionKeys = {}
|
|
let pluginName = getFlag(result, keys, 'pluginName', mustBeString)
|
|
let contents = getFlag(result, keys, 'contents', mustBeStringOrUint8Array)
|
|
let resolveDir = getFlag(result, keys, 'resolveDir', mustBeString)
|
|
let pluginData = getFlag(result, keys, 'pluginData', canBeAnything)
|
|
let loader = getFlag(result, keys, 'loader', mustBeString)
|
|
let errors = getFlag(result, keys, 'errors', mustBeArray)
|
|
let warnings = getFlag(result, keys, 'warnings', mustBeArray)
|
|
let watchFiles = getFlag(result, keys, 'watchFiles', mustBeArrayOfStrings)
|
|
let watchDirs = getFlag(result, keys, 'watchDirs', mustBeArrayOfStrings)
|
|
checkForInvalidFlags(result, keys, `from onLoad() callback in plugin ${quote(name)}`)
|
|
|
|
response.id = id
|
|
if (pluginName != null) response.pluginName = pluginName
|
|
if (contents instanceof Uint8Array) response.contents = contents
|
|
else if (contents != null) response.contents = protocol.encodeUTF8(contents)
|
|
if (resolveDir != null) response.resolveDir = resolveDir
|
|
if (pluginData != null) response.pluginData = details.store(pluginData)
|
|
if (loader != null) response.loader = loader
|
|
if (errors != null) response.errors = sanitizeMessages(errors, 'errors', details, name, undefined)
|
|
if (warnings != null) response.warnings = sanitizeMessages(warnings, 'warnings', details, name, undefined)
|
|
if (watchFiles != null) response.watchFiles = sanitizeStringArray(watchFiles, 'watchFiles')
|
|
if (watchDirs != null) response.watchDirs = sanitizeStringArray(watchDirs, 'watchDirs')
|
|
break
|
|
}
|
|
} catch (e) {
|
|
response = { id, errors: [extractErrorMessageV8(e, streamIn, details, note && note(), name)] }
|
|
break
|
|
}
|
|
}
|
|
sendResponse(id, response as any)
|
|
}
|
|
|
|
let runOnEndCallbacks: RunOnEndCallbacks = (result, done) => done([], [])
|
|
|
|
if (onEndCallbacks.length > 0) {
|
|
runOnEndCallbacks = (result, done) => {
|
|
(async () => {
|
|
const onEndErrors: types.Message[] = []
|
|
const onEndWarnings: types.Message[] = []
|
|
|
|
for (const { name, callback, note } of onEndCallbacks) {
|
|
let newErrors: types.Message[] | undefined
|
|
let newWarnings: types.Message[] | undefined
|
|
|
|
try {
|
|
const value = await callback(result)
|
|
|
|
if (value != null) {
|
|
if (typeof value !== 'object') throw new Error(`Expected onEnd() callback in plugin ${quote(name)} to return an object`)
|
|
let keys: OptionKeys = {}
|
|
let errors = getFlag(value, keys, 'errors', mustBeArray)
|
|
let warnings = getFlag(value, keys, 'warnings', mustBeArray)
|
|
checkForInvalidFlags(value, keys, `from onEnd() callback in plugin ${quote(name)}`)
|
|
|
|
if (errors != null) newErrors = sanitizeMessages(errors, 'errors', details, name, undefined)
|
|
if (warnings != null) newWarnings = sanitizeMessages(warnings, 'warnings', details, name, undefined)
|
|
}
|
|
} catch (e) {
|
|
newErrors = [extractErrorMessageV8(e, streamIn, details, note && note(), name)]
|
|
}
|
|
|
|
// Try adding the errors and warnings to the result object, but
|
|
// continue if something goes wrong. If error-reporting has errors
|
|
// then nothing can help us...
|
|
if (newErrors) {
|
|
onEndErrors.push(...newErrors)
|
|
try {
|
|
result.errors.push(...newErrors)
|
|
} catch {
|
|
}
|
|
}
|
|
if (newWarnings) {
|
|
onEndWarnings.push(...newWarnings)
|
|
try {
|
|
result.warnings.push(...newWarnings)
|
|
} catch {
|
|
}
|
|
}
|
|
}
|
|
|
|
done(onEndErrors, onEndWarnings)
|
|
})()
|
|
}
|
|
}
|
|
|
|
let scheduleOnDisposeCallbacks = (): void => {
|
|
// Run each "onDispose" callback with its own call stack
|
|
for (const cb of onDisposeCallbacks) {
|
|
setTimeout(() => cb(), 0)
|
|
}
|
|
}
|
|
|
|
isSetupDone = true
|
|
return {
|
|
ok: true,
|
|
requestPlugins,
|
|
runOnEndCallbacks,
|
|
scheduleOnDisposeCallbacks,
|
|
}
|
|
}
|
|
|
|
// This stores JavaScript objects on the JavaScript side and temporarily
|
|
// substitutes them with an integer that can be passed through the Go side
|
|
// and back. That way we can associate JavaScript objects with Go objects
|
|
// even if the JavaScript objects aren't serializable. And we also avoid
|
|
// the overhead of serializing large JavaScript objects.
|
|
interface ObjectStash {
|
|
clear(): void
|
|
load(id: number): any
|
|
store(value: any): number
|
|
}
|
|
|
|
function createObjectStash(): ObjectStash {
|
|
const map = new Map<number, any>()
|
|
let nextID = 0
|
|
return {
|
|
clear() {
|
|
map.clear()
|
|
},
|
|
load(id) {
|
|
return map.get(id)
|
|
},
|
|
store(value) {
|
|
if (value === void 0) return -1
|
|
const id = nextID++
|
|
map.set(id, value)
|
|
return id
|
|
},
|
|
}
|
|
}
|
|
|
|
function extractCallerV8(e: Error, streamIn: StreamIn, ident: string): () => types.Note | undefined {
|
|
let note: types.Note | undefined
|
|
let tried = false
|
|
return () => {
|
|
if (tried) return note
|
|
tried = true
|
|
try {
|
|
let lines = (e.stack + '').split('\n')
|
|
lines.splice(1, 1)
|
|
let location = parseStackLinesV8(streamIn, lines, ident)
|
|
if (location) {
|
|
note = { text: e.message, location }
|
|
return note
|
|
}
|
|
} catch {
|
|
}
|
|
}
|
|
}
|
|
|
|
function extractErrorMessageV8(e: any, streamIn: StreamIn, stash: ObjectStash | null, note: types.Note | undefined, pluginName: string): types.Message {
|
|
let text = 'Internal error'
|
|
let location: types.Location | null = null
|
|
|
|
try {
|
|
text = ((e && e.message) || e) + ''
|
|
} catch {
|
|
}
|
|
|
|
// Optionally attempt to extract the file from the stack trace, works in V8/node
|
|
try {
|
|
location = parseStackLinesV8(streamIn, (e.stack + '').split('\n'), '')
|
|
} catch {
|
|
}
|
|
|
|
return { id: '', pluginName, text, location, notes: note ? [note] : [], detail: stash ? stash.store(e) : -1 }
|
|
}
|
|
|
|
function parseStackLinesV8(streamIn: StreamIn, lines: string[], ident: string): types.Location | null {
|
|
let at = ' at '
|
|
|
|
// Check to see if this looks like a V8 stack trace
|
|
if (streamIn.readFileSync && !lines[0].startsWith(at) && lines[1].startsWith(at)) {
|
|
for (let i = 1; i < lines.length; i++) {
|
|
let line = lines[i]
|
|
if (!line.startsWith(at)) continue
|
|
line = line.slice(at.length)
|
|
while (true) {
|
|
// Unwrap a function name
|
|
let match = /^(?:new |async )?\S+ \((.*)\)$/.exec(line)
|
|
if (match) {
|
|
line = match[1]
|
|
continue
|
|
}
|
|
|
|
// Unwrap an eval wrapper
|
|
match = /^eval at \S+ \((.*)\)(?:, \S+:\d+:\d+)?$/.exec(line)
|
|
if (match) {
|
|
line = match[1]
|
|
continue
|
|
}
|
|
|
|
// Match on the file location
|
|
match = /^(\S+):(\d+):(\d+)$/.exec(line)
|
|
if (match) {
|
|
let contents
|
|
try {
|
|
contents = streamIn.readFileSync(match[1], 'utf8')
|
|
} catch {
|
|
break
|
|
}
|
|
let lineText = contents.split(/\r\n|\r|\n|\u2028|\u2029/)[+match[2] - 1] || ''
|
|
let column = +match[3] - 1
|
|
let length = lineText.slice(column, column + ident.length) === ident ? ident.length : 0
|
|
return {
|
|
file: match[1],
|
|
namespace: 'file',
|
|
line: +match[2],
|
|
column: protocol.encodeUTF8(lineText.slice(0, column)).length,
|
|
length: protocol.encodeUTF8(lineText.slice(column, column + length)).length,
|
|
lineText: lineText + '\n' + lines.slice(1).join('\n'),
|
|
suggestion: '',
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
function failureErrorWithLog(text: string, errors: types.Message[], warnings: types.Message[]): types.BuildFailure {
|
|
let limit = 5
|
|
text += errors.length < 1 ? '' : ` with ${errors.length} error${errors.length < 2 ? '' : 's'}:` +
|
|
errors.slice(0, limit + 1).map((e, i) => {
|
|
if (i === limit) return '\n...'
|
|
if (!e.location) return `\nerror: ${e.text}`
|
|
let { file, line, column } = e.location
|
|
let pluginText = e.pluginName ? `[plugin: ${e.pluginName}] ` : ''
|
|
return `\n${file}:${line}:${column}: ERROR: ${pluginText}${e.text}`
|
|
}).join('')
|
|
let error: any = new Error(text)
|
|
|
|
// Use a getter instead of a plain property so that when the error is thrown
|
|
// without being caught and the node process exits, the error objects aren't
|
|
// printed. The error objects are pretty big and not helpful because a) esbuild
|
|
// already prints errors to stderr by default and b) the error summary already
|
|
// has a more helpful abbreviated form of the error messages.
|
|
for (const [key, value] of [['errors', errors], ['warnings', warnings]] as const) {
|
|
Object.defineProperty(error, key, {
|
|
configurable: true,
|
|
enumerable: true,
|
|
get: () => value,
|
|
set: value => Object.defineProperty(error, key, {
|
|
configurable: true,
|
|
enumerable: true,
|
|
value,
|
|
}),
|
|
})
|
|
}
|
|
|
|
return error
|
|
}
|
|
|
|
function replaceDetailsInMessages(messages: types.Message[], stash: ObjectStash): types.Message[] {
|
|
for (const message of messages) {
|
|
message.detail = stash.load(message.detail)
|
|
}
|
|
return messages
|
|
}
|
|
|
|
function sanitizeLocation(location: types.PartialMessage['location'], where: string, terminalWidth: number | undefined): types.Message['location'] {
|
|
if (location == null) return null
|
|
|
|
let keys: OptionKeys = {}
|
|
let file = getFlag(location, keys, 'file', mustBeString)
|
|
let namespace = getFlag(location, keys, 'namespace', mustBeString)
|
|
let line = getFlag(location, keys, 'line', mustBeInteger)
|
|
let column = getFlag(location, keys, 'column', mustBeInteger)
|
|
let length = getFlag(location, keys, 'length', mustBeInteger)
|
|
let lineText = getFlag(location, keys, 'lineText', mustBeString)
|
|
let suggestion = getFlag(location, keys, 'suggestion', mustBeString)
|
|
checkForInvalidFlags(location, keys, where)
|
|
|
|
// Performance hack: Some people pass enormous minified files as the line
|
|
// text with a column near the beginning of the line and then complain
|
|
// when this function is slow. The slowness comes from serializing a huge
|
|
// string. But the vast majority of that string is unnecessary. Try to
|
|
// detect when this is the case and trim the string before serialization
|
|
// to avoid the performance hit. See: https://github.com/evanw/esbuild/issues/3467
|
|
if (lineText) {
|
|
// Try to conservatively guess the maximum amount of relevant text
|
|
const relevantASCII = lineText.slice(0,
|
|
(column && column > 0 ? column : 0) +
|
|
(length && length > 0 ? length : 0) +
|
|
(terminalWidth && terminalWidth > 0 ? terminalWidth : 80))
|
|
|
|
// Make sure it's ASCII (so the byte-oriented column and length values
|
|
// are correct) and that there are no newlines (so that our logging code
|
|
// doesn't look at the end of the string)
|
|
if (!/[\x7F-\uFFFF]/.test(relevantASCII) && !/\n/.test(lineText)) {
|
|
lineText = relevantASCII
|
|
}
|
|
}
|
|
|
|
// Note: We could technically make this even faster by maintaining two copies
|
|
// of this code, one in Go and one in TypeScript. But I'm not going to do that.
|
|
// The point of this function is to call into the real Go code to get what it
|
|
// does. If someone wants a JS version, they can port it themselves.
|
|
|
|
return {
|
|
file: file || '',
|
|
namespace: namespace || '',
|
|
line: line || 0,
|
|
column: column || 0,
|
|
length: length || 0,
|
|
lineText: lineText || '',
|
|
suggestion: suggestion || '',
|
|
}
|
|
}
|
|
|
|
function sanitizeMessages(
|
|
messages: types.PartialMessage[],
|
|
property: string,
|
|
stash: ObjectStash | null,
|
|
fallbackPluginName: string,
|
|
terminalWidth: number | undefined,
|
|
): types.Message[] {
|
|
let messagesClone: types.Message[] = []
|
|
let index = 0
|
|
|
|
for (const message of messages) {
|
|
let keys: OptionKeys = {}
|
|
let id = getFlag(message, keys, 'id', mustBeString)
|
|
let pluginName = getFlag(message, keys, 'pluginName', mustBeString)
|
|
let text = getFlag(message, keys, 'text', mustBeString)
|
|
let location = getFlag(message, keys, 'location', mustBeObjectOrNull)
|
|
let notes = getFlag(message, keys, 'notes', mustBeArray)
|
|
let detail = getFlag(message, keys, 'detail', canBeAnything)
|
|
let where = `in element ${index} of "${property}"`
|
|
checkForInvalidFlags(message, keys, where)
|
|
|
|
let notesClone: types.Note[] = []
|
|
if (notes) {
|
|
for (const note of notes) {
|
|
let noteKeys: OptionKeys = {}
|
|
let noteText = getFlag(note, noteKeys, 'text', mustBeString)
|
|
let noteLocation = getFlag(note, noteKeys, 'location', mustBeObjectOrNull)
|
|
checkForInvalidFlags(note, noteKeys, where)
|
|
notesClone.push({
|
|
text: noteText || '',
|
|
location: sanitizeLocation(noteLocation, where, terminalWidth),
|
|
})
|
|
}
|
|
}
|
|
|
|
messagesClone.push({
|
|
id: id || '',
|
|
pluginName: pluginName || fallbackPluginName,
|
|
text: text || '',
|
|
location: sanitizeLocation(location, where, terminalWidth),
|
|
notes: notesClone,
|
|
detail: stash ? stash.store(detail) : -1,
|
|
})
|
|
index++
|
|
}
|
|
|
|
return messagesClone
|
|
}
|
|
|
|
function sanitizeStringArray(values: any[], property: string): string[] {
|
|
const result: string[] = []
|
|
for (const value of values) {
|
|
if (typeof value !== 'string') throw new Error(`${quote(property)} must be an array of strings`)
|
|
result.push(value)
|
|
}
|
|
return result
|
|
}
|
|
|
|
function sanitizeStringMap(map: Record<string, any>, property: string): Record<string, string> {
|
|
const result: Record<string, string> = Object.create(null)
|
|
for (const key in map) {
|
|
const value = map[key]
|
|
if (typeof value !== 'string') throw new Error(`key ${quote(key)} in object ${quote(property)} must be a string`)
|
|
result[key] = value
|
|
}
|
|
return result
|
|
}
|
|
|
|
function convertOutputFiles({ path, contents, hash }: protocol.BuildOutputFile): types.OutputFile {
|
|
// The text is lazily-generated for performance reasons. If no one asks for
|
|
// it, then it never needs to be generated.
|
|
let text: string | null = null
|
|
return {
|
|
path,
|
|
contents,
|
|
hash,
|
|
get text() {
|
|
// People want to be able to set "contents" and have esbuild automatically
|
|
// derive "text" for them, so grab the contents off of this object instead
|
|
// of using our original value.
|
|
const binary = this.contents
|
|
|
|
// This deliberately doesn't do bidirectional derivation because that could
|
|
// result in the inefficiency. For example, if we did do this and then you
|
|
// set "contents" and "text" and then asked for "contents", the second
|
|
// setter for "text" will have erased our cached "contents" value so we'd
|
|
// need to regenerate it again. Instead, "contents" is unambiguously the
|
|
// primary value and "text" is unambiguously the derived value.
|
|
if (text === null || binary !== contents) {
|
|
contents = binary
|
|
text = protocol.decodeUTF8(binary)
|
|
}
|
|
return text
|
|
},
|
|
}
|
|
}
|
|
|
|
function jsRegExpToGoRegExp(regexp: RegExp): string {
|
|
let result = regexp.source
|
|
if (regexp.flags) result = `(?${regexp.flags})${result}`
|
|
return result
|
|
}
|