1import * as types from "./types"; 2import * as protocol from "./stdio_protocol"; 3 4declare const ESBUILD_VERSION: string; 5 6function validateTarget(target: string): string { 7 target += '' 8 if (target.indexOf(',') >= 0) throw new Error(`Invalid target: ${target}`) 9 return target 10} 11 12let canBeAnything = () => null; 13 14let mustBeBoolean = (value: boolean | undefined): string | null => 15 typeof value === 'boolean' ? null : 'a boolean'; 16 17let mustBeBooleanOrObject = (value: Object | boolean | undefined): string | null => 18 typeof value === 'boolean' || (typeof value === 'object' && !Array.isArray(value)) ? null : 'a boolean or an object'; 19 20let mustBeString = (value: string | undefined): string | null => 21 typeof value === 'string' ? null : 'a string'; 22 23let mustBeRegExp = (value: RegExp | undefined): string | null => 24 value instanceof RegExp ? null : 'a RegExp object'; 25 26let mustBeInteger = (value: number | undefined): string | null => 27 typeof value === 'number' && value === (value | 0) ? null : 'an integer'; 28 29let mustBeFunction = (value: Function | undefined): string | null => 30 typeof value === 'function' ? null : 'a function'; 31 32let mustBeArray = <T>(value: T[] | undefined): string | null => 33 Array.isArray(value) ? null : 'an array'; 34 35let mustBeObject = (value: Object | undefined): string | null => 36 typeof value === 'object' && value !== null && !Array.isArray(value) ? null : 'an object'; 37 38let mustBeArrayOrRecord = <T extends string>(value: T[] | Record<T, T> | undefined): string | null => 39 typeof value === 'object' && value !== null ? null : 'an array or an object'; 40 41let mustBeObjectOrNull = (value: Object | null | undefined): string | null => 42 typeof value === 'object' && !Array.isArray(value) ? null : 'an object or null'; 43 44let mustBeStringOrBoolean = (value: string | boolean | undefined): string | null => 45 typeof value === 'string' || typeof value === 'boolean' ? null : 'a string or a boolean'; 46 47let mustBeStringOrObject = (value: string | Object | undefined): string | null => 48 typeof value === 'string' || typeof value === 'object' && value !== null && !Array.isArray(value) ? null : 'a string or an object'; 49 50let mustBeStringOrArray = (value: string | string[] | undefined): string | null => 51 typeof value === 'string' || Array.isArray(value) ? null : 'a string or an array'; 52 53let mustBeStringOrUint8Array = (value: string | Uint8Array | undefined): string | null => 54 typeof value === 'string' || value instanceof Uint8Array ? null : 'a string or a Uint8Array'; 55 56type OptionKeys = { [key: string]: boolean }; 57 58function getFlag<T, K extends keyof T>(object: T, keys: OptionKeys, key: K, mustBeFn: (value: T[K]) => string | null): T[K] | undefined { 59 let value = object[key]; 60 keys[key + ''] = true; 61 if (value === undefined) return undefined; 62 let mustBe = mustBeFn(value); 63 if (mustBe !== null) throw new Error(`"${key}" must be ${mustBe}`); 64 return value; 65} 66 67function checkForInvalidFlags(object: Object, keys: OptionKeys, where: string): void { 68 for (let key in object) { 69 if (!(key in keys)) { 70 throw new Error(`Invalid option ${where}: "${key}"`); 71 } 72 } 73} 74 75export function validateInitializeOptions(options: types.InitializeOptions): types.InitializeOptions { 76 let keys: OptionKeys = Object.create(null); 77 let wasmURL = getFlag(options, keys, 'wasmURL', mustBeString); 78 let worker = getFlag(options, keys, 'worker', mustBeBoolean); 79 checkForInvalidFlags(options, keys, 'in startService() call'); 80 return { 81 wasmURL, 82 worker, 83 }; 84} 85 86type CommonOptions = types.BuildOptions | types.TransformOptions; 87 88function pushLogFlags(flags: string[], options: CommonOptions, keys: OptionKeys, isTTY: boolean, logLevelDefault: types.LogLevel): void { 89 let color = getFlag(options, keys, 'color', mustBeBoolean); 90 let logLevel = getFlag(options, keys, 'logLevel', mustBeString); 91 let logLimit = getFlag(options, keys, 'logLimit', mustBeInteger); 92 93 if (color !== void 0) flags.push(`--color=${color}`); 94 else if (isTTY) flags.push(`--color=true`); // This is needed to fix "execFileSync" which buffers stderr 95 flags.push(`--log-level=${logLevel || logLevelDefault}`); 96 flags.push(`--log-limit=${logLimit || 0}`); 97} 98 99function pushCommonFlags(flags: string[], options: CommonOptions, keys: OptionKeys): void { 100 let legalComments = getFlag(options, keys, 'legalComments', mustBeString); 101 let sourceRoot = getFlag(options, keys, 'sourceRoot', mustBeString); 102 let sourcesContent = getFlag(options, keys, 'sourcesContent', mustBeBoolean); 103 let target = getFlag(options, keys, 'target', mustBeStringOrArray); 104 let format = getFlag(options, keys, 'format', mustBeString); 105 let globalName = getFlag(options, keys, 'globalName', mustBeString); 106 let minify = getFlag(options, keys, 'minify', mustBeBoolean); 107 let minifySyntax = getFlag(options, keys, 'minifySyntax', mustBeBoolean); 108 let minifyWhitespace = getFlag(options, keys, 'minifyWhitespace', mustBeBoolean); 109 let minifyIdentifiers = getFlag(options, keys, 'minifyIdentifiers', mustBeBoolean); 110 let charset = getFlag(options, keys, 'charset', mustBeString); 111 let treeShaking = getFlag(options, keys, 'treeShaking', mustBeStringOrBoolean); 112 let jsx = getFlag(options, keys, 'jsx', mustBeString); 113 let jsxFactory = getFlag(options, keys, 'jsxFactory', mustBeString); 114 let jsxFragment = getFlag(options, keys, 'jsxFragment', mustBeString); 115 let define = getFlag(options, keys, 'define', mustBeObject); 116 let pure = getFlag(options, keys, 'pure', mustBeArray); 117 let keepNames = getFlag(options, keys, 'keepNames', mustBeBoolean); 118 119 if (legalComments) flags.push(`--legal-comments=${legalComments}`); 120 if (sourceRoot !== void 0) flags.push(`--source-root=${sourceRoot}`); 121 if (sourcesContent !== void 0) flags.push(`--sources-content=${sourcesContent}`); 122 if (target) { 123 if (Array.isArray(target)) flags.push(`--target=${Array.from(target).map(validateTarget).join(',')}`) 124 else flags.push(`--target=${validateTarget(target)}`) 125 } 126 if (format) flags.push(`--format=${format}`); 127 if (globalName) flags.push(`--global-name=${globalName}`); 128 129 if (minify) flags.push('--minify'); 130 if (minifySyntax) flags.push('--minify-syntax'); 131 if (minifyWhitespace) flags.push('--minify-whitespace'); 132 if (minifyIdentifiers) flags.push('--minify-identifiers'); 133 if (charset) flags.push(`--charset=${charset}`); 134 if (treeShaking !== void 0 && treeShaking !== true) flags.push(`--tree-shaking=${treeShaking}`); 135 136 if (jsx) flags.push(`--jsx=${jsx}`); 137 if (jsxFactory) flags.push(`--jsx-factory=${jsxFactory}`); 138 if (jsxFragment) flags.push(`--jsx-fragment=${jsxFragment}`); 139 140 if (define) { 141 for (let key in define) { 142 if (key.indexOf('=') >= 0) throw new Error(`Invalid define: ${key}`); 143 flags.push(`--define:${key}=${define[key]}`); 144 } 145 } 146 if (pure) for (let fn of pure) flags.push(`--pure:${fn}`); 147 if (keepNames) flags.push(`--keep-names`); 148} 149 150function flagsForBuildOptions( 151 callName: string, 152 options: types.BuildOptions, 153 isTTY: boolean, 154 logLevelDefault: types.LogLevel, 155 writeDefault: boolean, 156): { 157 entries: [string, string][], 158 flags: string[], 159 write: boolean, 160 stdinContents: string | null, 161 stdinResolveDir: string | null, 162 absWorkingDir: string | undefined, 163 incremental: boolean, 164 nodePaths: string[], 165 watch: types.WatchMode | null, 166} { 167 let flags: string[] = []; 168 let entries: [string, string][] = []; 169 let keys: OptionKeys = Object.create(null); 170 let stdinContents: string | null = null; 171 let stdinResolveDir: string | null = null; 172 let watchMode: types.WatchMode | null = null; 173 pushLogFlags(flags, options, keys, isTTY, logLevelDefault); 174 pushCommonFlags(flags, options, keys); 175 176 let sourcemap = getFlag(options, keys, 'sourcemap', mustBeStringOrBoolean); 177 let bundle = getFlag(options, keys, 'bundle', mustBeBoolean); 178 let watch = getFlag(options, keys, 'watch', mustBeBooleanOrObject); 179 let splitting = getFlag(options, keys, 'splitting', mustBeBoolean); 180 let preserveSymlinks = getFlag(options, keys, 'preserveSymlinks', mustBeBoolean); 181 let metafile = getFlag(options, keys, 'metafile', mustBeBoolean); 182 let outfile = getFlag(options, keys, 'outfile', mustBeString); 183 let outdir = getFlag(options, keys, 'outdir', mustBeString); 184 let outbase = getFlag(options, keys, 'outbase', mustBeString); 185 let platform = getFlag(options, keys, 'platform', mustBeString); 186 let tsconfig = getFlag(options, keys, 'tsconfig', mustBeString); 187 let resolveExtensions = getFlag(options, keys, 'resolveExtensions', mustBeArray); 188 let nodePathsInput = getFlag(options, keys, 'nodePaths', mustBeArray); 189 let mainFields = getFlag(options, keys, 'mainFields', mustBeArray); 190 let conditions = getFlag(options, keys, 'conditions', mustBeArray); 191 let external = getFlag(options, keys, 'external', mustBeArray); 192 let loader = getFlag(options, keys, 'loader', mustBeObject); 193 let outExtension = getFlag(options, keys, 'outExtension', mustBeObject); 194 let publicPath = getFlag(options, keys, 'publicPath', mustBeString); 195 let entryNames = getFlag(options, keys, 'entryNames', mustBeString); 196 let chunkNames = getFlag(options, keys, 'chunkNames', mustBeString); 197 let assetNames = getFlag(options, keys, 'assetNames', mustBeString); 198 let inject = getFlag(options, keys, 'inject', mustBeArray); 199 let banner = getFlag(options, keys, 'banner', mustBeObject); 200 let footer = getFlag(options, keys, 'footer', mustBeObject); 201 let entryPoints = getFlag(options, keys, 'entryPoints', mustBeArrayOrRecord); 202 let absWorkingDir = getFlag(options, keys, 'absWorkingDir', mustBeString); 203 let stdin = getFlag(options, keys, 'stdin', mustBeObject); 204 let write = getFlag(options, keys, 'write', mustBeBoolean) ?? writeDefault; // Default to true if not specified 205 let allowOverwrite = getFlag(options, keys, 'allowOverwrite', mustBeBoolean); 206 let incremental = getFlag(options, keys, 'incremental', mustBeBoolean) === true; 207 keys.plugins = true; // "plugins" has already been read earlier 208 checkForInvalidFlags(options, keys, `in ${callName}() call`); 209 210 if (sourcemap) flags.push(`--sourcemap${sourcemap === true ? '' : `=${sourcemap}`}`); 211 if (bundle) flags.push('--bundle'); 212 if (allowOverwrite) flags.push('--allow-overwrite'); 213 if (watch) { 214 flags.push('--watch'); 215 if (typeof watch === 'boolean') { 216 watchMode = {}; 217 } else { 218 let watchKeys: OptionKeys = Object.create(null); 219 let onRebuild = getFlag(watch, watchKeys, 'onRebuild', mustBeFunction); 220 checkForInvalidFlags(watch, watchKeys, `on "watch" in ${callName}() call`); 221 watchMode = { onRebuild }; 222 } 223 } 224 if (splitting) flags.push('--splitting'); 225 if (preserveSymlinks) flags.push('--preserve-symlinks'); 226 if (metafile) flags.push(`--metafile`); 227 if (outfile) flags.push(`--outfile=${outfile}`); 228 if (outdir) flags.push(`--outdir=${outdir}`); 229 if (outbase) flags.push(`--outbase=${outbase}`); 230 if (platform) flags.push(`--platform=${platform}`); 231 if (tsconfig) flags.push(`--tsconfig=${tsconfig}`); 232 if (resolveExtensions) { 233 let values: string[] = []; 234 for (let value of resolveExtensions) { 235 value += ''; 236 if (value.indexOf(',') >= 0) throw new Error(`Invalid resolve extension: ${value}`); 237 values.push(value); 238 } 239 flags.push(`--resolve-extensions=${values.join(',')}`); 240 } 241 if (publicPath) flags.push(`--public-path=${publicPath}`); 242 if (entryNames) flags.push(`--entry-names=${entryNames}`); 243 if (chunkNames) flags.push(`--chunk-names=${chunkNames}`); 244 if (assetNames) flags.push(`--asset-names=${assetNames}`); 245 if (mainFields) { 246 let values: string[] = []; 247 for (let value of mainFields) { 248 value += ''; 249 if (value.indexOf(',') >= 0) throw new Error(`Invalid main field: ${value}`); 250 values.push(value); 251 } 252 flags.push(`--main-fields=${values.join(',')}`); 253 } 254 if (conditions) { 255 let values: string[] = []; 256 for (let value of conditions) { 257 value += ''; 258 if (value.indexOf(',') >= 0) throw new Error(`Invalid condition: ${value}`); 259 values.push(value); 260 } 261 flags.push(`--conditions=${values.join(',')}`); 262 } 263 if (external) for (let name of external) flags.push(`--external:${name}`); 264 if (banner) { 265 for (let type in banner) { 266 if (type.indexOf('=') >= 0) throw new Error(`Invalid banner file type: ${type}`); 267 flags.push(`--banner:${type}=${banner[type]}`); 268 } 269 } 270 if (footer) { 271 for (let type in footer) { 272 if (type.indexOf('=') >= 0) throw new Error(`Invalid footer file type: ${type}`); 273 flags.push(`--footer:${type}=${footer[type]}`); 274 } 275 } 276 if (inject) for (let path of inject) flags.push(`--inject:${path}`); 277 if (loader) { 278 for (let ext in loader) { 279 if (ext.indexOf('=') >= 0) throw new Error(`Invalid loader extension: ${ext}`); 280 flags.push(`--loader:${ext}=${loader[ext]}`); 281 } 282 } 283 if (outExtension) { 284 for (let ext in outExtension) { 285 if (ext.indexOf('=') >= 0) throw new Error(`Invalid out extension: ${ext}`); 286 flags.push(`--out-extension:${ext}=${outExtension[ext]}`); 287 } 288 } 289 290 if (entryPoints) { 291 if (Array.isArray(entryPoints)) { 292 for (let entryPoint of entryPoints) { 293 entries.push(['', entryPoint + '']); 294 } 295 } else { 296 for (let [key, value] of Object.entries(entryPoints)) { 297 entries.push([key + '', value + '']); 298 } 299 } 300 } 301 302 if (stdin) { 303 let stdinKeys: OptionKeys = Object.create(null); 304 let contents = getFlag(stdin, stdinKeys, 'contents', mustBeString); 305 let resolveDir = getFlag(stdin, stdinKeys, 'resolveDir', mustBeString); 306 let sourcefile = getFlag(stdin, stdinKeys, 'sourcefile', mustBeString); 307 let loader = getFlag(stdin, stdinKeys, 'loader', mustBeString); 308 checkForInvalidFlags(stdin, stdinKeys, 'in "stdin" object'); 309 310 if (sourcefile) flags.push(`--sourcefile=${sourcefile}`); 311 if (loader) flags.push(`--loader=${loader}`); 312 if (resolveDir) stdinResolveDir = resolveDir + ''; 313 stdinContents = contents ? contents + '' : ''; 314 } 315 316 let nodePaths: string[] = []; 317 if (nodePathsInput) { 318 for (let value of nodePathsInput) { 319 value += ''; 320 nodePaths.push(value); 321 } 322 } 323 324 return { 325 entries, 326 flags, 327 write, 328 stdinContents, 329 stdinResolveDir, 330 absWorkingDir, 331 incremental, 332 nodePaths, 333 watch: watchMode, 334 }; 335} 336 337function flagsForTransformOptions( 338 callName: string, 339 options: types.TransformOptions, 340 isTTY: boolean, 341 logLevelDefault: types.LogLevel, 342): string[] { 343 let flags: string[] = []; 344 let keys: OptionKeys = Object.create(null); 345 pushLogFlags(flags, options, keys, isTTY, logLevelDefault); 346 pushCommonFlags(flags, options, keys); 347 348 let sourcemap = getFlag(options, keys, 'sourcemap', mustBeStringOrBoolean); 349 let tsconfigRaw = getFlag(options, keys, 'tsconfigRaw', mustBeStringOrObject); 350 let sourcefile = getFlag(options, keys, 'sourcefile', mustBeString); 351 let loader = getFlag(options, keys, 'loader', mustBeString); 352 let banner = getFlag(options, keys, 'banner', mustBeString); 353 let footer = getFlag(options, keys, 'footer', mustBeString); 354 checkForInvalidFlags(options, keys, `in ${callName}() call`); 355 356 if (sourcemap) flags.push(`--sourcemap=${sourcemap === true ? 'external' : sourcemap}`); 357 if (tsconfigRaw) flags.push(`--tsconfig-raw=${typeof tsconfigRaw === 'string' ? tsconfigRaw : JSON.stringify(tsconfigRaw)}`); 358 if (sourcefile) flags.push(`--sourcefile=${sourcefile}`); 359 if (loader) flags.push(`--loader=${loader}`); 360 if (banner) flags.push(`--banner=${banner}`); 361 if (footer) flags.push(`--footer=${footer}`); 362 363 return flags; 364} 365 366export interface StreamIn { 367 writeToStdin: (data: Uint8Array) => void; 368 readFileSync?: (path: string, encoding: 'utf8') => string; 369 isSync: boolean; 370 isBrowser: boolean; 371} 372 373export interface StreamOut { 374 readFromStdout: (data: Uint8Array) => void; 375 afterClose: () => void; 376 service: StreamService; 377} 378 379export interface StreamFS { 380 writeFile(contents: string, callback: (path: string | null) => void): void; 381 readFile(path: string, callback: (err: Error | null, contents: string | null) => void): void; 382} 383 384export interface Refs { 385 ref(): void; 386 unref(): void; 387} 388 389export interface StreamService { 390 buildOrServe(args: { 391 callName: string, 392 refs: Refs | null, 393 serveOptions: types.ServeOptions | null, 394 options: types.BuildOptions, 395 isTTY: boolean, 396 defaultWD: string, 397 callback: (err: Error | null, res: types.BuildResult | types.ServeResult | null) => void, 398 }): void; 399 400 transform(args: { 401 callName: string, 402 refs: Refs | null, 403 input: string, 404 options: types.TransformOptions, 405 isTTY: boolean, 406 fs: StreamFS, 407 callback: (err: Error | null, res: types.TransformResult | null) => void, 408 }): void; 409 410 formatMessages(args: { 411 callName: string, 412 refs: Refs | null, 413 messages: types.PartialMessage[], 414 options: types.FormatMessagesOptions, 415 callback: (err: Error | null, res: string[] | null) => void, 416 }): void; 417} 418 419// This can't use any promises in the main execution flow because it must work 420// for both sync and async code. There is an exception for plugin code because 421// that can't work in sync code anyway. 422export function createChannel(streamIn: StreamIn): StreamOut { 423 type PluginCallback = (request: protocol.OnStartRequest | protocol.OnResolveRequest | protocol.OnLoadRequest) => 424 Promise<protocol.OnStartResponse | protocol.OnResolveResponse | protocol.OnLoadResponse>; 425 426 type WatchCallback = (error: Error | null, response: any) => void; 427 428 interface ServeCallbacks { 429 onRequest: types.ServeOptions['onRequest']; 430 onWait: (error: string | null) => void; 431 } 432 433 let responseCallbacks = new Map<number, (error: string | null, response: protocol.Value) => void>(); 434 let pluginCallbacks = new Map<number, PluginCallback>(); 435 let watchCallbacks = new Map<number, WatchCallback>(); 436 let serveCallbacks = new Map<number, ServeCallbacks>(); 437 let nextServeID = 0; 438 let isClosed = false; 439 let nextRequestID = 0; 440 let nextBuildKey = 0; 441 442 // Use a long-lived buffer to store stdout data 443 let stdout = new Uint8Array(16 * 1024); 444 let stdoutUsed = 0; 445 let readFromStdout = (chunk: Uint8Array) => { 446 // Append the chunk to the stdout buffer, growing it as necessary 447 let limit = stdoutUsed + chunk.length; 448 if (limit > stdout.length) { 449 let swap = new Uint8Array(limit * 2); 450 swap.set(stdout); 451 stdout = swap; 452 } 453 stdout.set(chunk, stdoutUsed); 454 stdoutUsed += chunk.length; 455 456 // Process all complete (i.e. not partial) packets 457 let offset = 0; 458 while (offset + 4 <= stdoutUsed) { 459 let length = protocol.readUInt32LE(stdout, offset); 460 if (offset + 4 + length > stdoutUsed) { 461 break; 462 } 463 offset += 4; 464 handleIncomingPacket(stdout.subarray(offset, offset + length)); 465 offset += length; 466 } 467 if (offset > 0) { 468 stdout.copyWithin(0, offset, stdoutUsed); 469 stdoutUsed -= offset; 470 } 471 }; 472 473 let afterClose = () => { 474 // When the process is closed, fail all pending requests 475 isClosed = true; 476 for (let callback of responseCallbacks.values()) { 477 callback('The service was stopped', null); 478 } 479 responseCallbacks.clear(); 480 for (let callbacks of serveCallbacks.values()) { 481 callbacks.onWait('The service was stopped'); 482 } 483 serveCallbacks.clear(); 484 for (let callback of watchCallbacks.values()) { 485 try { 486 callback(new Error('The service was stopped'), null); 487 } catch (e) { 488 console.error(e) 489 } 490 } 491 watchCallbacks.clear(); 492 }; 493 494 let sendRequest = <Req, Res>(refs: Refs | null, value: Req, callback: (error: string | null, response: Res | null) => void): void => { 495 if (isClosed) return callback('The service is no longer running', null); 496 let id = nextRequestID++; 497 responseCallbacks.set(id, (error, response) => { 498 try { 499 callback(error, response as any); 500 } finally { 501 if (refs) refs.unref() // Do this after the callback so the callback can extend the lifetime if needed 502 } 503 }); 504 if (refs) refs.ref() 505 streamIn.writeToStdin(protocol.encodePacket({ id, isRequest: true, value: value as any })); 506 }; 507 508 let sendResponse = (id: number, value: protocol.Value): void => { 509 if (isClosed) throw new Error('The service is no longer running'); 510 streamIn.writeToStdin(protocol.encodePacket({ id, isRequest: false, value })); 511 }; 512 513 type RequestType = 514 | protocol.PingRequest 515 | protocol.OnStartRequest 516 | protocol.OnResolveRequest 517 | protocol.OnLoadRequest 518 | protocol.OnRequestRequest 519 | protocol.OnWaitRequest 520 | protocol.OnWatchRebuildRequest 521 522 let handleRequest = async (id: number, request: RequestType) => { 523 // Catch exceptions in the code below so they get passed to the caller 524 try { 525 switch (request.command) { 526 case 'ping': { 527 sendResponse(id, {}); 528 break; 529 } 530 531 case 'start': { 532 let callback = pluginCallbacks.get(request.key); 533 if (!callback) sendResponse(id, {}); 534 else sendResponse(id, await callback!(request) as any); 535 break; 536 } 537 538 case 'resolve': { 539 let callback = pluginCallbacks.get(request.key); 540 if (!callback) sendResponse(id, {}); 541 else sendResponse(id, await callback!(request) as any); 542 break; 543 } 544 545 case 'load': { 546 let callback = pluginCallbacks.get(request.key); 547 if (!callback) sendResponse(id, {}); 548 else sendResponse(id, await callback!(request) as any); 549 break; 550 } 551 552 case 'serve-request': { 553 let callbacks = serveCallbacks.get(request.serveID); 554 if (callbacks && callbacks.onRequest) callbacks.onRequest(request.args); 555 sendResponse(id, {}); 556 break; 557 } 558 559 case 'serve-wait': { 560 let callbacks = serveCallbacks.get(request.serveID); 561 if (callbacks) callbacks.onWait(request.error); 562 sendResponse(id, {}); 563 break; 564 } 565 566 case 'watch-rebuild': { 567 let callback = watchCallbacks.get(request.watchID); 568 try { 569 if (callback) callback(null, request.args); 570 } catch (err) { 571 console.error(err); 572 } 573 sendResponse(id, {}); 574 break; 575 } 576 577 default: 578 throw new Error(`Invalid command: ` + (request as any)!.command); 579 } 580 } catch (e) { 581 sendResponse(id, { errors: [extractErrorMessageV8(e, streamIn, null, void 0, '')] } as any); 582 } 583 }; 584 585 let isFirstPacket = true; 586 587 let handleIncomingPacket = (bytes: Uint8Array): void => { 588 // The first packet is a version check 589 if (isFirstPacket) { 590 isFirstPacket = false; 591 592 // Validate the binary's version number to make sure esbuild was installed 593 // correctly. This check was added because some people have reported 594 // errors that appear to indicate an incorrect installation. 595 let binaryVersion = String.fromCharCode(...bytes); 596 if (binaryVersion !== ESBUILD_VERSION) { 597 throw new Error(`Cannot start service: Host version "${ESBUILD_VERSION}" does not match binary version ${JSON.stringify(binaryVersion)}`); 598 } 599 return; 600 } 601 602 let packet = protocol.decodePacket(bytes) as any; 603 604 if (packet.isRequest) { 605 handleRequest(packet.id, packet.value); 606 } 607 608 else { 609 let callback = responseCallbacks.get(packet.id)!; 610 responseCallbacks.delete(packet.id); 611 if (packet.value.error) callback(packet.value.error, {}); 612 else callback(null, packet.value); 613 } 614 }; 615 616 type RunOnEndCallbacks = (result: types.BuildResult, logPluginError: LogPluginErrorCallback, done: () => void) => void; 617 type LogPluginErrorCallback = (e: any, pluginName: string, note: types.Note | undefined, done: (message: types.Message) => void) => void; 618 619 let handlePlugins = async ( 620 initialOptions: types.BuildOptions, 621 plugins: types.Plugin[], 622 buildKey: number, 623 stash: ObjectStash, 624 ): Promise< 625 | { ok: true, requestPlugins: protocol.BuildPlugin[], runOnEndCallbacks: RunOnEndCallbacks, pluginRefs: Refs } 626 | { ok: false, error: any, pluginName: string } 627 > => { 628 let onStartCallbacks: { 629 name: string, 630 note: () => types.Note | undefined, 631 callback: () => (types.OnStartResult | null | void | Promise<types.OnStartResult | null | void>), 632 }[] = []; 633 634 let onEndCallbacks: { 635 name: string, 636 note: () => types.Note | undefined, 637 callback: (result: types.BuildResult) => (void | Promise<void>), 638 }[] = []; 639 640 let onResolveCallbacks: { 641 [id: number]: { 642 name: string, 643 note: () => types.Note | undefined, 644 callback: (args: types.OnResolveArgs) => 645 (types.OnResolveResult | null | undefined | Promise<types.OnResolveResult | null | undefined>), 646 }, 647 } = {}; 648 649 let onLoadCallbacks: { 650 [id: number]: { 651 name: string, 652 note: () => types.Note | undefined, 653 callback: (args: types.OnLoadArgs) => 654 (types.OnLoadResult | null | undefined | Promise<types.OnLoadResult | null | undefined>), 655 }, 656 } = {}; 657 658 let nextCallbackID = 0; 659 let i = 0; 660 let requestPlugins: protocol.BuildPlugin[] = []; 661 662 // Clone the plugin array to guard against mutation during iteration 663 plugins = [...plugins]; 664 665 for (let item of plugins) { 666 let keys: OptionKeys = {}; 667 if (typeof item !== 'object') throw new Error(`Plugin at index ${i} must be an object`); 668 let name = getFlag(item, keys, 'name', mustBeString); 669 if (typeof name !== 'string' || name === '') throw new Error(`Plugin at index ${i} is missing a name`); 670 try { 671 let setup = getFlag(item, keys, 'setup', mustBeFunction); 672 if (typeof setup !== 'function') throw new Error(`Plugin is missing a setup function`); 673 checkForInvalidFlags(item, keys, `on plugin ${JSON.stringify(name)}`); 674 675 let plugin: protocol.BuildPlugin = { 676 name, 677 onResolve: [], 678 onLoad: [], 679 }; 680 i++; 681 682 let promise = setup({ 683 initialOptions, 684 685 onStart(callback) { 686 let registeredText = `This error came from the "onStart" callback registered here` 687 let registeredNote = extractCallerV8(new Error(registeredText), streamIn, 'onStart'); 688 onStartCallbacks.push({ name: name!, callback, note: registeredNote }); 689 }, 690 691 onEnd(callback) { 692 let registeredText = `This error came from the "onEnd" callback registered here` 693 let registeredNote = extractCallerV8(new Error(registeredText), streamIn, 'onEnd'); 694 onEndCallbacks.push({ name: name!, callback, note: registeredNote }); 695 }, 696 697 onResolve(options, callback) { 698 let registeredText = `This error came from the "onResolve" callback registered here` 699 let registeredNote = extractCallerV8(new Error(registeredText), streamIn, 'onResolve'); 700 let keys: OptionKeys = {}; 701 let filter = getFlag(options, keys, 'filter', mustBeRegExp); 702 let namespace = getFlag(options, keys, 'namespace', mustBeString); 703 checkForInvalidFlags(options, keys, `in onResolve() call for plugin ${JSON.stringify(name)}`); 704 if (filter == null) throw new Error(`onResolve() call is missing a filter`); 705 let id = nextCallbackID++; 706 onResolveCallbacks[id] = { name: name!, callback, note: registeredNote }; 707 plugin.onResolve.push({ id, filter: filter.source, namespace: namespace || '' }); 708 }, 709 710 onLoad(options, callback) { 711 let registeredText = `This error came from the "onLoad" callback registered here` 712 let registeredNote = extractCallerV8(new Error(registeredText), streamIn, 'onLoad'); 713 let keys: OptionKeys = {}; 714 let filter = getFlag(options, keys, 'filter', mustBeRegExp); 715 let namespace = getFlag(options, keys, 'namespace', mustBeString); 716 checkForInvalidFlags(options, keys, `in onLoad() call for plugin ${JSON.stringify(name)}`); 717 if (filter == null) throw new Error(`onLoad() call is missing a filter`); 718 let id = nextCallbackID++; 719 onLoadCallbacks[id] = { name: name!, callback, note: registeredNote }; 720 plugin.onLoad.push({ id, filter: filter.source, namespace: namespace || '' }); 721 }, 722 }); 723 724 // Await a returned promise if there was one. This allows plugins to do 725 // some asynchronous setup while still retaining the ability to modify 726 // the build options. This deliberately serializes asynchronous plugin 727 // setup instead of running them concurrently so that build option 728 // modifications are easier to reason about. 729 if (promise) await promise; 730 731 requestPlugins.push(plugin); 732 } catch (e) { 733 return { ok: false, error: e, pluginName: name } 734 } 735 } 736 737 const callback: PluginCallback = async (request) => { 738 switch (request.command) { 739 case 'start': { 740 let response: protocol.OnStartResponse = { errors: [], warnings: [] }; 741 await Promise.all(onStartCallbacks.map(async ({ name, callback, note }) => { 742 try { 743 let result = await callback(); 744 745 if (result != null) { 746 if (typeof result !== 'object') throw new Error(`Expected onStart() callback in plugin ${JSON.stringify(name)} to return an object`); 747 let keys: OptionKeys = {}; 748 let errors = getFlag(result, keys, 'errors', mustBeArray); 749 let warnings = getFlag(result, keys, 'warnings', mustBeArray); 750 checkForInvalidFlags(result, keys, `from onStart() callback in plugin ${JSON.stringify(name)}`); 751 752 if (errors != null) response.errors!.push(...sanitizeMessages(errors, 'errors', stash, name)); 753 if (warnings != null) response.warnings!.push(...sanitizeMessages(warnings, 'warnings', stash, name)); 754 } 755 } catch (e) { 756 response.errors!.push(extractErrorMessageV8(e, streamIn, stash, note && note(), name)); 757 } 758 })) 759 return response; 760 } 761 762 case 'resolve': { 763 let response: protocol.OnResolveResponse = {}, name = '', callback, note; 764 for (let id of request.ids) { 765 try { 766 ({ name, callback, note } = onResolveCallbacks[id]); 767 let result = await callback({ 768 path: request.path, 769 importer: request.importer, 770 namespace: request.namespace, 771 resolveDir: request.resolveDir, 772 kind: request.kind, 773 pluginData: stash.load(request.pluginData), 774 }); 775 776 if (result != null) { 777 if (typeof result !== 'object') throw new Error(`Expected onResolve() callback in plugin ${JSON.stringify(name)} to return an object`); 778 let keys: OptionKeys = {}; 779 let pluginName = getFlag(result, keys, 'pluginName', mustBeString); 780 let path = getFlag(result, keys, 'path', mustBeString); 781 let namespace = getFlag(result, keys, 'namespace', mustBeString); 782 let external = getFlag(result, keys, 'external', mustBeBoolean); 783 let sideEffects = getFlag(result, keys, 'sideEffects', mustBeBoolean); 784 let pluginData = getFlag(result, keys, 'pluginData', canBeAnything); 785 let errors = getFlag(result, keys, 'errors', mustBeArray); 786 let warnings = getFlag(result, keys, 'warnings', mustBeArray); 787 let watchFiles = getFlag(result, keys, 'watchFiles', mustBeArray); 788 let watchDirs = getFlag(result, keys, 'watchDirs', mustBeArray); 789 checkForInvalidFlags(result, keys, `from onResolve() callback in plugin ${JSON.stringify(name)}`); 790 791 response.id = id; 792 if (pluginName != null) response.pluginName = pluginName; 793 if (path != null) response.path = path; 794 if (namespace != null) response.namespace = namespace; 795 if (external != null) response.external = external; 796 if (sideEffects != null) response.sideEffects = sideEffects; 797 if (pluginData != null) response.pluginData = stash.store(pluginData); 798 if (errors != null) response.errors = sanitizeMessages(errors, 'errors', stash, name); 799 if (warnings != null) response.warnings = sanitizeMessages(warnings, 'warnings', stash, name); 800 if (watchFiles != null) response.watchFiles = sanitizeStringArray(watchFiles, 'watchFiles'); 801 if (watchDirs != null) response.watchDirs = sanitizeStringArray(watchDirs, 'watchDirs'); 802 break; 803 } 804 } catch (e) { 805 return { id, errors: [extractErrorMessageV8(e, streamIn, stash, note && note(), name)] }; 806 } 807 } 808 return response; 809 } 810 811 case 'load': { 812 let response: protocol.OnLoadResponse = {}, name = '', callback, note; 813 for (let id of request.ids) { 814 try { 815 ({ name, callback, note } = onLoadCallbacks[id]); 816 let result = await callback({ 817 path: request.path, 818 namespace: request.namespace, 819 pluginData: stash.load(request.pluginData), 820 }); 821 822 if (result != null) { 823 if (typeof result !== 'object') throw new Error(`Expected onLoad() callback in plugin ${JSON.stringify(name)} to return an object`); 824 let keys: OptionKeys = {}; 825 let pluginName = getFlag(result, keys, 'pluginName', mustBeString); 826 let contents = getFlag(result, keys, 'contents', mustBeStringOrUint8Array); 827 let resolveDir = getFlag(result, keys, 'resolveDir', mustBeString); 828 let pluginData = getFlag(result, keys, 'pluginData', canBeAnything); 829 let loader = getFlag(result, keys, 'loader', mustBeString); 830 let errors = getFlag(result, keys, 'errors', mustBeArray); 831 let warnings = getFlag(result, keys, 'warnings', mustBeArray); 832 let watchFiles = getFlag(result, keys, 'watchFiles', mustBeArray); 833 let watchDirs = getFlag(result, keys, 'watchDirs', mustBeArray); 834 checkForInvalidFlags(result, keys, `from onLoad() callback in plugin ${JSON.stringify(name)}`); 835 836 response.id = id; 837 if (pluginName != null) response.pluginName = pluginName; 838 if (contents instanceof Uint8Array) response.contents = contents; 839 else if (contents != null) response.contents = protocol.encodeUTF8(contents); 840 if (resolveDir != null) response.resolveDir = resolveDir; 841 if (pluginData != null) response.pluginData = stash.store(pluginData); 842 if (loader != null) response.loader = loader; 843 if (errors != null) response.errors = sanitizeMessages(errors, 'errors', stash, name); 844 if (warnings != null) response.warnings = sanitizeMessages(warnings, 'warnings', stash, name); 845 if (watchFiles != null) response.watchFiles = sanitizeStringArray(watchFiles, 'watchFiles'); 846 if (watchDirs != null) response.watchDirs = sanitizeStringArray(watchDirs, 'watchDirs'); 847 break; 848 } 849 } catch (e) { 850 return { id, errors: [extractErrorMessageV8(e, streamIn, stash, note && note(), name)] }; 851 } 852 } 853 return response; 854 } 855 856 default: 857 throw new Error(`Invalid command: ` + (request as any).command); 858 } 859 } 860 861 let runOnEndCallbacks: RunOnEndCallbacks = (result, logPluginError, done) => done(); 862 863 if (onEndCallbacks.length > 0) { 864 runOnEndCallbacks = (result, logPluginError, done) => { 865 (async () => { 866 for (const { name, callback, note } of onEndCallbacks) { 867 try { 868 await callback(result) 869 } catch (e) { 870 result.errors.push(await new Promise<types.Message>(resolve => logPluginError(e, name, note && note(), resolve))) 871 } 872 } 873 })().then(done) 874 } 875 } 876 877 let refCount = 0; 878 return { 879 ok: true, 880 requestPlugins, 881 runOnEndCallbacks, 882 pluginRefs: { 883 ref() { if (++refCount === 1) pluginCallbacks.set(buildKey, callback); }, 884 unref() { if (--refCount === 0) pluginCallbacks.delete(buildKey) }, 885 }, 886 } 887 }; 888 889 interface ServeData { 890 wait: Promise<void> 891 stop: () => void 892 } 893 894 let buildServeData = (refs: Refs | null, options: types.ServeOptions, request: protocol.BuildRequest): ServeData => { 895 let keys: OptionKeys = {}; 896 let port = getFlag(options, keys, 'port', mustBeInteger); 897 let host = getFlag(options, keys, 'host', mustBeString); 898 let servedir = getFlag(options, keys, 'servedir', mustBeString); 899 let onRequest = getFlag(options, keys, 'onRequest', mustBeFunction); 900 let serveID = nextServeID++; 901 let onWait: ServeCallbacks['onWait']; 902 let wait = new Promise<void>((resolve, reject) => { 903 onWait = error => { 904 serveCallbacks.delete(serveID); 905 if (error !== null) reject(new Error(error)); 906 else resolve(); 907 }; 908 }); 909 request.serve = { serveID }; 910 checkForInvalidFlags(options, keys, `in serve() call`); 911 if (port !== void 0) request.serve.port = port; 912 if (host !== void 0) request.serve.host = host; 913 if (servedir !== void 0) request.serve.servedir = servedir; 914 serveCallbacks.set(serveID, { 915 onRequest, 916 onWait: onWait!, 917 }); 918 return { 919 wait, 920 stop() { 921 sendRequest<protocol.ServeStopRequest, null>(refs, { command: 'serve-stop', serveID }, () => { 922 // We don't care about the result 923 }); 924 }, 925 }; 926 }; 927 928 const buildLogLevelDefault = 'warning'; 929 const transformLogLevelDefault = 'silent'; 930 931 let buildOrServe: StreamService['buildOrServe'] = args => { 932 let key = nextBuildKey++; 933 const details = createObjectStash(); 934 let plugins: types.Plugin[] | undefined; 935 let { refs, options, isTTY, callback } = args; 936 if (typeof options === 'object') { 937 let value = options.plugins; 938 if (value !== void 0) { 939 if (!Array.isArray(value)) throw new Error(`"plugins" must be an array`); 940 plugins = value; 941 } 942 } 943 let logPluginError: LogPluginErrorCallback = (e, pluginName, note, done) => { 944 let flags: string[] = []; 945 try { pushLogFlags(flags, options, {}, isTTY, buildLogLevelDefault) } catch { } 946 const message = extractErrorMessageV8(e, streamIn, details, note, pluginName) 947 sendRequest(refs, { command: 'error', flags, error: message }, () => { 948 message.detail = details.load(message.detail); 949 done(message) 950 }); 951 }; 952 let handleError = (e: any, pluginName: string) => { 953 logPluginError(e, pluginName, void 0, error => { 954 callback(failureErrorWithLog('Build failed', [error], []), null); 955 }) 956 }; 957 if (plugins && plugins.length > 0) { 958 if (streamIn.isSync) return handleError(new Error('Cannot use plugins in synchronous API calls'), ''); 959 960 // Plugins can use async/await because they can't be run with "buildSync" 961 handlePlugins(options, plugins, key, details).then( 962 result => { 963 if (!result.ok) { 964 handleError(result.error, result.pluginName); 965 } else { 966 try { 967 buildOrServeContinue({ 968 ...args, 969 key, 970 details, 971 logPluginError, 972 requestPlugins: result.requestPlugins, 973 runOnEndCallbacks: result.runOnEndCallbacks, 974 pluginRefs: result.pluginRefs, 975 }) 976 } catch (e) { 977 handleError(e, ''); 978 } 979 } 980 }, 981 e => handleError(e, ''), 982 ) 983 } else { 984 try { 985 buildOrServeContinue({ 986 ...args, 987 key, 988 details, 989 logPluginError, 990 requestPlugins: null, 991 runOnEndCallbacks: (result, logPluginError, done) => done(), 992 pluginRefs: null, 993 }); 994 } catch (e) { 995 handleError(e, ''); 996 } 997 } 998 } 999 1000 // "buildOrServe" cannot be written using async/await due to "buildSync" and 1001 // must be written in continuation-passing style instead. Sorry about all of 1002 // the arguments, but these are passed explicitly instead of using another 1003 // nested closure because this function is already huge and I didn't want to 1004 // make it any bigger. 1005 let buildOrServeContinue = ({ 1006 callName, 1007 refs: callerRefs, 1008 serveOptions, 1009 options, 1010 isTTY, 1011 defaultWD, 1012 callback, 1013 key, 1014 details, 1015 logPluginError, 1016 requestPlugins, 1017 runOnEndCallbacks, 1018 pluginRefs, 1019 }: { 1020 callName: string, 1021 refs: Refs | null, 1022 serveOptions: types.ServeOptions | null, 1023 options: types.BuildOptions, 1024 isTTY: boolean, 1025 defaultWD: string, 1026 callback: (err: Error | null, res: types.BuildResult | types.ServeResult | null) => void, 1027 key: number, 1028 details: ObjectStash, 1029 logPluginError: LogPluginErrorCallback, 1030 requestPlugins: protocol.BuildPlugin[] | null, 1031 runOnEndCallbacks: RunOnEndCallbacks, 1032 pluginRefs: Refs | null, 1033 }) => { 1034 const refs = { 1035 ref() { 1036 if (pluginRefs) pluginRefs.ref() 1037 if (callerRefs) callerRefs.ref() 1038 }, 1039 unref() { 1040 if (pluginRefs) pluginRefs.unref() 1041 if (callerRefs) callerRefs.unref() 1042 }, 1043 } 1044 let writeDefault = !streamIn.isBrowser; 1045 let { 1046 entries, 1047 flags, 1048 write, 1049 stdinContents, 1050 stdinResolveDir, 1051 absWorkingDir, 1052 incremental, 1053 nodePaths, 1054 watch, 1055 } = flagsForBuildOptions(callName, options, isTTY, buildLogLevelDefault, writeDefault); 1056 let request: protocol.BuildRequest = { 1057 command: 'build', 1058 key, 1059 entries, 1060 flags, 1061 write, 1062 stdinContents, 1063 stdinResolveDir, 1064 absWorkingDir: absWorkingDir || defaultWD, 1065 incremental, 1066 nodePaths, 1067 }; 1068 if (requestPlugins) request.plugins = requestPlugins; 1069 let serve = serveOptions && buildServeData(refs, serveOptions, request); 1070 1071 // Factor out response handling so it can be reused for rebuilds 1072 let rebuild: types.BuildResult['rebuild'] | undefined; 1073 let stop: types.BuildResult['stop'] | undefined; 1074 let copyResponseToResult = (response: protocol.BuildResponse, result: types.BuildResult) => { 1075 if (response.outputFiles) result.outputFiles = response!.outputFiles.map(convertOutputFiles); 1076 if (response.metafile) result.metafile = JSON.parse(response!.metafile); 1077 if (response.writeToStdout !== void 0) console.log(protocol.decodeUTF8(response!.writeToStdout).replace(/\n$/, '')); 1078 }; 1079 let buildResponseToResult = ( 1080 response: protocol.BuildResponse | null, 1081 callback: (error: types.BuildFailure | null, result: types.BuildResult | null) => void, 1082 ): void => { 1083 let result: types.BuildResult = { 1084 errors: replaceDetailsInMessages(response!.errors, details), 1085 warnings: replaceDetailsInMessages(response!.warnings, details), 1086 }; 1087 copyResponseToResult(response!, result); 1088 runOnEndCallbacks(result, logPluginError, () => { 1089 if (result.errors.length > 0) { 1090 return callback(failureErrorWithLog('Build failed', result.errors, result.warnings), null); 1091 } 1092 1093 // Handle incremental rebuilds 1094 if (response!.rebuildID !== void 0) { 1095 if (!rebuild) { 1096 let isDisposed = false; 1097 (rebuild as any) = () => new Promise<types.BuildResult>((resolve, reject) => { 1098 if (isDisposed || isClosed) throw new Error('Cannot rebuild'); 1099 sendRequest<protocol.RebuildRequest, protocol.BuildResponse>(refs, { command: 'rebuild', rebuildID: response!.rebuildID! }, 1100 (error2, response2) => { 1101 if (error2) { 1102 const message: types.Message = { pluginName: '', text: error2, location: null, notes: [], detail: void 0 }; 1103 return callback(failureErrorWithLog('Build failed', [message], []), null); 1104 } 1105 buildResponseToResult(response2, (error3, result3) => { 1106 if (error3) reject(error3); 1107 else resolve(result3!); 1108 }); 1109 }); 1110 }); 1111 refs.ref() 1112 rebuild!.dispose = () => { 1113 if (isDisposed) return; 1114 isDisposed = true; 1115 sendRequest<protocol.RebuildDisposeRequest, null>(refs, { command: 'rebuild-dispose', rebuildID: response!.rebuildID! }, () => { 1116 // We don't care about the result 1117 }); 1118 refs.unref() // Do this after the callback so "sendRequest" can extend the lifetime 1119 }; 1120 } 1121 result.rebuild = rebuild; 1122 } 1123 1124 // Handle watch mode 1125 if (response!.watchID !== void 0) { 1126 if (!stop) { 1127 let isStopped = false; 1128 refs.ref() 1129 stop = () => { 1130 if (isStopped) return; 1131 isStopped = true; 1132 watchCallbacks.delete(response!.watchID!); 1133 sendRequest<protocol.WatchStopRequest, null>(refs, { command: 'watch-stop', watchID: response!.watchID! }, () => { 1134 // We don't care about the result 1135 }); 1136 refs.unref() // Do this after the callback so "sendRequest" can extend the lifetime 1137 } 1138 if (watch) { 1139 watchCallbacks.set(response!.watchID, (serviceStopError, watchResponse) => { 1140 if (serviceStopError) { 1141 if (watch!.onRebuild) watch!.onRebuild(serviceStopError as any, null); 1142 return; 1143 } 1144 let result2: types.BuildResult = { 1145 errors: replaceDetailsInMessages(watchResponse.errors, details), 1146 warnings: replaceDetailsInMessages(watchResponse.warnings, details), 1147 }; 1148 1149 // Note: "onEnd" callbacks should run even when there is no "onRebuild" callback 1150 copyResponseToResult(watchResponse, result2); 1151 runOnEndCallbacks(result2, logPluginError, () => { 1152 if (result2.errors.length > 0) { 1153 if (watch!.onRebuild) watch!.onRebuild(failureErrorWithLog('Build failed', result2.errors, result2.warnings), null); 1154 return; 1155 } 1156 if (watchResponse.rebuildID !== void 0) result2.rebuild = rebuild; 1157 result2.stop = stop; 1158 if (watch!.onRebuild) watch!.onRebuild(null, result2); 1159 }); 1160 }); 1161 } 1162 } 1163 result.stop = stop; 1164 } 1165 1166 callback(null, result); 1167 }); 1168 }; 1169 1170 if (write && streamIn.isBrowser) throw new Error(`Cannot enable "write" in the browser`); 1171 if (incremental && streamIn.isSync) throw new Error(`Cannot use "incremental" with a synchronous build`); 1172 if (watch && streamIn.isSync) throw new Error(`Cannot use "watch" with a synchronous build`); 1173 sendRequest<protocol.BuildRequest, protocol.BuildResponse>(refs, request, (error, response) => { 1174 if (error) return callback(new Error(error), null); 1175 if (serve) { 1176 let serveResponse = response as any as protocol.ServeResponse; 1177 let isStopped = false 1178 1179 // Add a ref/unref for "stop()" 1180 refs.ref() 1181 let result: types.ServeResult = { 1182 port: serveResponse.port, 1183 host: serveResponse.host, 1184 wait: serve.wait, 1185 stop() { 1186 if (isStopped) return 1187 isStopped = true 1188 serve!.stop(); 1189 refs.unref() // Do this after the callback so "stop" can extend the lifetime 1190 }, 1191 }; 1192 1193 // Add a ref/unref for "wait". This must be done independently of 1194 // "stop()" in case the response to "stop()" comes in first before 1195 // the request for "wait". Without this ref/unref, node may close 1196 // the child's stdin pipe after the "stop()" but before the "wait" 1197 // which will cause things to break. This caused a test failure. 1198 refs.ref() 1199 serve.wait.then(refs.unref, refs.unref) 1200 1201 return callback(null, result); 1202 } 1203 return buildResponseToResult(response!, callback); 1204 }); 1205 }; 1206 1207 let transform: StreamService['transform'] = ({ callName, refs, input, options, isTTY, fs, callback }) => { 1208 const details = createObjectStash(); 1209 1210 // Ideally the "transform()" API would be faster than calling "build()" 1211 // since it doesn't need to touch the file system. However, performance 1212 // measurements with large files on macOS indicate that sending the data 1213 // over the stdio pipe can be 2x slower than just using a temporary file. 1214 // 1215 // This appears to be an OS limitation. Both the JavaScript and Go code 1216 // are using large buffers but the pipe only writes data in 8kb chunks. 1217 // An investigation seems to indicate that this number is hard-coded into 1218 // the OS source code. Presumably files are faster because the OS uses 1219 // a larger chunk size, or maybe even reads everything in one syscall. 1220 // 1221 // The cross-over size where this starts to be faster is around 1mb on 1222 // my machine. In that case, this code tries to use a temporary file if 1223 // possible but falls back to sending the data over the stdio pipe if 1224 // that doesn't work. 1225 let start = (inputPath: string | null) => { 1226 try { 1227 if (typeof input !== 'string') throw new Error('The input to "transform" must be a string'); 1228 let flags = flagsForTransformOptions(callName, options, isTTY, transformLogLevelDefault); 1229 let request: protocol.TransformRequest = { 1230 command: 'transform', 1231 flags, 1232 inputFS: inputPath !== null, 1233 input: inputPath !== null ? inputPath : input, 1234 }; 1235 sendRequest<protocol.TransformRequest, protocol.TransformResponse>(refs, request, (error, response) => { 1236 if (error) return callback(new Error(error), null); 1237 let errors = replaceDetailsInMessages(response!.errors, details); 1238 let warnings = replaceDetailsInMessages(response!.warnings, details); 1239 let outstanding = 1; 1240 let next = () => --outstanding === 0 && callback(null, { warnings, code: response!.code, map: response!.map }); 1241 if (errors.length > 0) return callback(failureErrorWithLog('Transform failed', errors, warnings), null); 1242 1243 // Read the JavaScript file from the file system 1244 if (response!.codeFS) { 1245 outstanding++; 1246 fs.readFile(response!.code, (err, contents) => { 1247 if (err !== null) { 1248 callback(err, null); 1249 } else { 1250 response!.code = contents!; 1251 next(); 1252 } 1253 }); 1254 } 1255 1256 // Read the source map file from the file system 1257 if (response!.mapFS) { 1258 outstanding++; 1259 fs.readFile(response!.map, (err, contents) => { 1260 if (err !== null) { 1261 callback(err, null); 1262 } else { 1263 response!.map = contents!; 1264 next(); 1265 } 1266 }); 1267 } 1268 1269 next(); 1270 }); 1271 } catch (e) { 1272 let flags: string[] = []; 1273 try { pushLogFlags(flags, options, {}, isTTY, transformLogLevelDefault) } catch { } 1274 const error = extractErrorMessageV8(e, streamIn, details, void 0, ''); 1275 sendRequest(refs, { command: 'error', flags, error }, () => { 1276 error.detail = details.load(error.detail); 1277 callback(failureErrorWithLog('Transform failed', [error], []), null); 1278 }); 1279 } 1280 }; 1281 if (typeof input === 'string' && input.length > 1024 * 1024) { 1282 let next = start; 1283 start = () => fs.writeFile(input, next); 1284 } 1285 start(null); 1286 }; 1287 1288 let formatMessages: StreamService['formatMessages'] = ({ callName, refs, messages, options, callback }) => { 1289 let result = sanitizeMessages(messages, 'messages', null, ''); 1290 if (!options) throw new Error(`Missing second argument in ${callName}() call`); 1291 let keys: OptionKeys = {}; 1292 let kind = getFlag(options, keys, 'kind', mustBeString); 1293 let color = getFlag(options, keys, 'color', mustBeBoolean); 1294 let terminalWidth = getFlag(options, keys, 'terminalWidth', mustBeInteger); 1295 checkForInvalidFlags(options, keys, `in ${callName}() call`); 1296 if (kind === void 0) throw new Error(`Missing "kind" in ${callName}() call`); 1297 if (kind !== 'error' && kind !== 'warning') throw new Error(`Expected "kind" to be "error" or "warning" in ${callName}() call`); 1298 let request: protocol.FormatMsgsRequest = { 1299 command: 'format-msgs', 1300 messages: result, 1301 isWarning: kind === 'warning', 1302 } 1303 if (color !== void 0) request.color = color; 1304 if (terminalWidth !== void 0) request.terminalWidth = terminalWidth; 1305 sendRequest<protocol.FormatMsgsRequest, protocol.FormatMsgsResponse>(refs, request, (error, response) => { 1306 if (error) return callback(new Error(error), null); 1307 callback(null, response!.messages); 1308 }); 1309 }; 1310 1311 return { 1312 readFromStdout, 1313 afterClose, 1314 service: { 1315 buildOrServe, 1316 transform, 1317 formatMessages, 1318 }, 1319 }; 1320} 1321 1322// This stores JavaScript objects on the JavaScript side and temporarily 1323// substitutes them with an integer that can be passed through the Go side 1324// and back. That way we can associate JavaScript objects with Go objects 1325// even if the JavaScript objects aren't serializable. And we also avoid 1326// the overhead of serializing large JavaScript objects. 1327interface ObjectStash { 1328 load(id: number): any; 1329 store(value: any): number; 1330} 1331 1332function createObjectStash(): ObjectStash { 1333 const map = new Map<number, any>(); 1334 let nextID = 0; 1335 return { 1336 load(id) { 1337 return map.get(id); 1338 }, 1339 store(value) { 1340 if (value === void 0) return -1; 1341 const id = nextID++; 1342 map.set(id, value); 1343 return id; 1344 }, 1345 }; 1346} 1347 1348function extractCallerV8(e: Error, streamIn: StreamIn, ident: string): () => types.Note | undefined { 1349 let note: types.Note | undefined 1350 let tried = false 1351 return () => { 1352 if (tried) return note 1353 tried = true 1354 try { 1355 let lines = (e.stack + '').split('\n') 1356 lines.splice(1, 1) 1357 let location = parseStackLinesV8(streamIn, lines, ident) 1358 if (location) { 1359 note = { text: e.message, location } 1360 return note 1361 } 1362 } catch { 1363 } 1364 } 1365} 1366 1367function extractErrorMessageV8(e: any, streamIn: StreamIn, stash: ObjectStash | null, note: types.Note | undefined, pluginName: string): types.Message { 1368 let text = 'Internal error' 1369 let location: types.Location | null = null 1370 1371 try { 1372 text = ((e && e.message) || e) + ''; 1373 } catch { 1374 } 1375 1376 // Optionally attempt to extract the file from the stack trace, works in V8/node 1377 try { 1378 location = parseStackLinesV8(streamIn, (e.stack + '').split('\n'), '') 1379 } catch { 1380 } 1381 1382 return { pluginName, text, location, notes: note ? [note] : [], detail: stash ? stash.store(e) : -1 } 1383} 1384 1385function parseStackLinesV8(streamIn: StreamIn, lines: string[], ident: string): types.Location | null { 1386 let at = ' at ' 1387 1388 // Check to see if this looks like a V8 stack trace 1389 if (streamIn.readFileSync && !lines[0].startsWith(at) && lines[1].startsWith(at)) { 1390 for (let i = 1; i < lines.length; i++) { 1391 let line = lines[i] 1392 if (!line.startsWith(at)) continue 1393 line = line.slice(at.length) 1394 while (true) { 1395 // Unwrap a function name 1396 let match = /^(?:new |async )?\S+ \((.*)\)$/.exec(line) 1397 if (match) { 1398 line = match[1] 1399 continue 1400 } 1401 1402 // Unwrap an eval wrapper 1403 match = /^eval at \S+ \((.*)\)(?:, \S+:\d+:\d+)?$/.exec(line) 1404 if (match) { 1405 line = match[1] 1406 continue 1407 } 1408 1409 // Match on the file location 1410 match = /^(\S+):(\d+):(\d+)$/.exec(line) 1411 if (match) { 1412 let contents 1413 try { 1414 contents = streamIn.readFileSync(match[1], 'utf8') 1415 } catch { 1416 break 1417 } 1418 let lineText = contents.split(/\r\n|\r|\n|\u2028|\u2029/)[+match[2] - 1] || '' 1419 let column = +match[3] - 1 1420 let length = lineText.slice(column, column + ident.length) === ident ? ident.length : 0 1421 return { 1422 file: match[1], 1423 namespace: 'file', 1424 line: +match[2], 1425 column: protocol.encodeUTF8(lineText.slice(0, column)).length, 1426 length: protocol.encodeUTF8(lineText.slice(column, column + length)).length, 1427 lineText: lineText + '\n' + lines.slice(1).join('\n'), 1428 suggestion: '', 1429 } 1430 } 1431 break 1432 } 1433 } 1434 } 1435 1436 return null; 1437} 1438 1439function failureErrorWithLog(text: string, errors: types.Message[], warnings: types.Message[]): types.BuildFailure { 1440 let limit = 5 1441 let summary = errors.length < 1 ? '' : ` with ${errors.length} error${errors.length < 2 ? '' : 's'}:` + 1442 errors.slice(0, limit + 1).map((e, i) => { 1443 if (i === limit) return '\n...'; 1444 if (!e.location) return `\nerror: ${e.text}`; 1445 let { file, line, column } = e.location; 1446 let pluginText = e.pluginName ? `[plugin: ${e.pluginName}] ` : ''; 1447 return `\n${file}:${line}:${column}: error: ${pluginText}${e.text}`; 1448 }).join(''); 1449 let error: any = new Error(`${text}${summary}`); 1450 error.errors = errors; 1451 error.warnings = warnings; 1452 return error; 1453} 1454 1455function replaceDetailsInMessages(messages: types.Message[], stash: ObjectStash): types.Message[] { 1456 for (const message of messages) { 1457 message.detail = stash.load(message.detail); 1458 } 1459 return messages; 1460} 1461 1462function sanitizeLocation(location: types.PartialMessage['location'], where: string): types.Message['location'] { 1463 if (location == null) return null; 1464 1465 let keys: OptionKeys = {}; 1466 let file = getFlag(location, keys, 'file', mustBeString); 1467 let namespace = getFlag(location, keys, 'namespace', mustBeString); 1468 let line = getFlag(location, keys, 'line', mustBeInteger); 1469 let column = getFlag(location, keys, 'column', mustBeInteger); 1470 let length = getFlag(location, keys, 'length', mustBeInteger); 1471 let lineText = getFlag(location, keys, 'lineText', mustBeString); 1472 let suggestion = getFlag(location, keys, 'suggestion', mustBeString); 1473 checkForInvalidFlags(location, keys, where); 1474 1475 return { 1476 file: file || '', 1477 namespace: namespace || '', 1478 line: line || 0, 1479 column: column || 0, 1480 length: length || 0, 1481 lineText: lineText || '', 1482 suggestion: suggestion || '', 1483 }; 1484} 1485 1486function sanitizeMessages(messages: types.PartialMessage[], property: string, stash: ObjectStash | null, fallbackPluginName: string): types.Message[] { 1487 let messagesClone: types.Message[] = []; 1488 let index = 0; 1489 1490 for (const message of messages) { 1491 let keys: OptionKeys = {}; 1492 let pluginName = getFlag(message, keys, 'pluginName', mustBeString); 1493 let text = getFlag(message, keys, 'text', mustBeString); 1494 let location = getFlag(message, keys, 'location', mustBeObjectOrNull); 1495 let notes = getFlag(message, keys, 'notes', mustBeArray); 1496 let detail = getFlag(message, keys, 'detail', canBeAnything); 1497 let where = `in element ${index} of "${property}"`; 1498 checkForInvalidFlags(message, keys, where); 1499 1500 let notesClone: types.Note[] = []; 1501 if (notes) { 1502 for (const note of notes) { 1503 let noteKeys: OptionKeys = {}; 1504 let noteText = getFlag(note, noteKeys, 'text', mustBeString); 1505 let noteLocation = getFlag(note, noteKeys, 'location', mustBeObjectOrNull); 1506 checkForInvalidFlags(note, noteKeys, where); 1507 notesClone.push({ 1508 text: noteText || '', 1509 location: sanitizeLocation(noteLocation, where), 1510 }); 1511 } 1512 } 1513 1514 messagesClone.push({ 1515 pluginName: pluginName || fallbackPluginName, 1516 text: text || '', 1517 location: sanitizeLocation(location, where), 1518 notes: notesClone, 1519 detail: stash ? stash.store(detail) : -1, 1520 }); 1521 index++; 1522 } 1523 1524 return messagesClone; 1525} 1526 1527function sanitizeStringArray(values: any[], property: string): string[] { 1528 const result: string[] = []; 1529 for (const value of values) { 1530 if (typeof value !== 'string') throw new Error(`${JSON.stringify(property)} must be an array of strings`); 1531 result.push(value); 1532 } 1533 return result; 1534} 1535 1536function convertOutputFiles({ path, contents }: protocol.BuildOutputFile): types.OutputFile { 1537 let text: string | null = null; 1538 return { 1539 path, 1540 contents, 1541 get text() { 1542 if (text === null) text = protocol.decodeUTF8(contents); 1543 return text; 1544 }, 1545 } 1546} 1547