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