-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
Copy pathnode.ts
633 lines (551 loc) · 22.2 KB
/
node.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
import type * as types from "../shared/types"
import * as common from "../shared/common"
import * as ourselves from "./node"
import { ESBUILD_BINARY_PATH, generateBinPath } from "./node-platform"
import child_process = require('child_process')
import crypto = require('crypto')
import path = require('path')
import fs = require('fs')
import os = require('os')
import tty = require('tty')
declare const ESBUILD_VERSION: string
// This file is used for both the "esbuild" package and the "esbuild-wasm"
// package. "WASM" will be true for "esbuild-wasm" and false for "esbuild".
declare const WASM: boolean
let worker_threads: typeof import('worker_threads') | undefined
if (process.env.ESBUILD_WORKER_THREADS !== '0') {
// Don't crash if the "worker_threads" library isn't present
try {
worker_threads = require('worker_threads')
} catch {
}
// Creating a worker in certain node versions doesn't work. The specific
// error is "TypeError: MessagePort was found in message but not listed
// in transferList". See: https://github.com/nodejs/node/issues/32250.
// We just pretend worker threads are unavailable in these cases.
let [major, minor] = process.versions.node.split('.')
if (
// <v12.17.0 does not work
+major < 12 || (+major === 12 && +minor < 17)
// >=v13.0.0 && <v13.13.0 also does not work
|| (+major === 13 && +minor < 13)
) {
worker_threads = void 0
}
}
// This should only be true if this is our internal worker thread. We want this
// library to be usable from other people's worker threads, so we should not be
// checking for "isMainThread".
let isInternalWorkerThread = worker_threads?.workerData?.esbuildVersion === ESBUILD_VERSION
let esbuildCommandAndArgs = (): [string, string[]] => {
// Try to have a nice error message when people accidentally bundle esbuild
// without providing an explicit path to the binary, or when using WebAssembly.
if ((!ESBUILD_BINARY_PATH || WASM) && (path.basename(__filename) !== 'main.js' || path.basename(__dirname) !== 'lib')) {
throw new Error(
`The esbuild JavaScript API cannot be bundled. Please mark the "esbuild" ` +
`package as external so it's not included in the bundle.\n` +
`\n` +
`More information: The file containing the code for esbuild's JavaScript ` +
`API (${__filename}) does not appear to be inside the esbuild package on ` +
`the file system, which usually means that the esbuild package was bundled ` +
`into another file. This is problematic because the API needs to run a ` +
`binary executable inside the esbuild package which is located using a ` +
`relative path from the API code to the executable. If the esbuild package ` +
`is bundled, the relative path will be incorrect and the executable won't ` +
`be found.`)
}
if (WASM) {
return ['node', [path.join(__dirname, '..', 'bin', 'esbuild')]]
} else {
const { binPath, isWASM } = generateBinPath()
if (isWASM) {
return ['node', [binPath]]
} else {
return [binPath, []]
}
}
}
// Return true if stderr is a TTY
let isTTY = () => tty.isatty(2)
let fsSync: common.StreamFS = {
readFile(tempFile, callback) {
try {
let contents = fs.readFileSync(tempFile, 'utf8')
try {
fs.unlinkSync(tempFile)
} catch {
}
callback(null, contents)
} catch (err: any) {
callback(err, null)
}
},
writeFile(contents, callback) {
try {
let tempFile = randomFileName()
fs.writeFileSync(tempFile, contents)
callback(tempFile)
} catch {
callback(null)
}
},
}
let fsAsync: common.StreamFS = {
readFile(tempFile, callback) {
try {
fs.readFile(tempFile, 'utf8', (err, contents) => {
try {
fs.unlink(tempFile, () => callback(err, contents))
} catch {
callback(err, contents)
}
})
} catch (err: any) {
callback(err, null)
}
},
writeFile(contents, callback) {
try {
let tempFile = randomFileName()
fs.writeFile(tempFile, contents, err =>
err !== null ? callback(null) : callback(tempFile))
} catch {
callback(null)
}
},
}
export let version = ESBUILD_VERSION
export let build: typeof types.build = (options: types.BuildOptions) =>
ensureServiceIsRunning().build(options)
export let context: typeof types.context = (buildOptions: types.BuildOptions) =>
ensureServiceIsRunning().context(buildOptions)
export let transform: typeof types.transform = (input: string | Uint8Array, options?: types.TransformOptions) =>
ensureServiceIsRunning().transform(input, options)
export let formatMessages: typeof types.formatMessages = (messages, options) =>
ensureServiceIsRunning().formatMessages(messages, options)
export let analyzeMetafile: typeof types.analyzeMetafile = (messages, options) =>
ensureServiceIsRunning().analyzeMetafile(messages, options)
export let buildSync: typeof types.buildSync = (options: types.BuildOptions) => {
// Try using a long-lived worker thread to avoid repeated start-up overhead
if (worker_threads && !isInternalWorkerThread) {
if (!workerThreadService) workerThreadService = startWorkerThreadService(worker_threads)
return workerThreadService.buildSync(options)
}
let result: types.BuildResult
runServiceSync(service => service.buildOrContext({
callName: 'buildSync',
refs: null,
options,
isTTY: isTTY(),
defaultWD,
callback: (err, res) => { if (err) throw err; result = res as types.BuildResult },
}))
return result!
}
export let transformSync: typeof types.transformSync = (input: string | Uint8Array, options?: types.TransformOptions) => {
// Try using a long-lived worker thread to avoid repeated start-up overhead
if (worker_threads && !isInternalWorkerThread) {
if (!workerThreadService) workerThreadService = startWorkerThreadService(worker_threads)
return workerThreadService.transformSync(input, options)
}
let result: types.TransformResult
runServiceSync(service => service.transform({
callName: 'transformSync',
refs: null,
input,
options: options || {},
isTTY: isTTY(),
fs: fsSync,
callback: (err, res) => { if (err) throw err; result = res! },
}))
return result!
}
export let formatMessagesSync: typeof types.formatMessagesSync = (messages, options) => {
// Try using a long-lived worker thread to avoid repeated start-up overhead
if (worker_threads && !isInternalWorkerThread) {
if (!workerThreadService) workerThreadService = startWorkerThreadService(worker_threads)
return workerThreadService.formatMessagesSync(messages, options)
}
let result: string[]
runServiceSync(service => service.formatMessages({
callName: 'formatMessagesSync',
refs: null,
messages,
options,
callback: (err, res) => { if (err) throw err; result = res! },
}))
return result!
}
export let analyzeMetafileSync: typeof types.analyzeMetafileSync = (metafile, options) => {
// Try using a long-lived worker thread to avoid repeated start-up overhead
if (worker_threads && !isInternalWorkerThread) {
if (!workerThreadService) workerThreadService = startWorkerThreadService(worker_threads)
return workerThreadService.analyzeMetafileSync(metafile, options)
}
let result: string
runServiceSync(service => service.analyzeMetafile({
callName: 'analyzeMetafileSync',
refs: null,
metafile: typeof metafile === 'string' ? metafile : JSON.stringify(metafile),
options,
callback: (err, res) => { if (err) throw err; result = res! },
}))
return result!
}
export const stop = () => {
if (stopService) stopService()
if (workerThreadService) workerThreadService.stop()
return Promise.resolve()
}
let initializeWasCalled = false
export let initialize: typeof types.initialize = options => {
options = common.validateInitializeOptions(options || {})
if (options.wasmURL) throw new Error(`The "wasmURL" option only works in the browser`)
if (options.wasmModule) throw new Error(`The "wasmModule" option only works in the browser`)
if (options.worker) throw new Error(`The "worker" option only works in the browser`)
if (initializeWasCalled) throw new Error('Cannot call "initialize" more than once')
ensureServiceIsRunning()
initializeWasCalled = true
return Promise.resolve()
}
interface Service {
build: typeof types.build
context: typeof types.context
transform: typeof types.transform
formatMessages: typeof types.formatMessages
analyzeMetafile: typeof types.analyzeMetafile
}
let defaultWD = process.cwd()
let longLivedService: Service | undefined
let stopService: (() => void) | undefined
let ensureServiceIsRunning = (): Service => {
if (longLivedService) return longLivedService
let [command, args] = esbuildCommandAndArgs()
let child = child_process.spawn(command, args.concat(`--service=${ESBUILD_VERSION}`, '--ping'), {
windowsHide: true,
stdio: ['pipe', 'pipe', 'inherit'],
cwd: defaultWD,
})
let { readFromStdout, afterClose, service } = common.createChannel({
writeToStdin(bytes) {
child.stdin.write(bytes, err => {
// Assume the service was stopped if we get an error writing to stdin
if (err) afterClose(err)
})
},
readFileSync: fs.readFileSync,
isSync: false,
hasFS: true,
esbuild: ourselves,
})
// Assume the service was stopped if we get an error writing to stdin
child.stdin.on('error', afterClose)
// Propagate errors about failure to run the executable itself
child.on('error', afterClose)
const stdin: typeof child.stdin & { unref?(): void } = child.stdin
const stdout: typeof child.stdout & { unref?(): void } = child.stdout
stdout.on('data', readFromStdout)
stdout.on('end', afterClose)
stopService = () => {
// Close all resources related to the subprocess.
stdin.destroy()
stdout.destroy()
child.kill()
initializeWasCalled = false
longLivedService = undefined
stopService = undefined
}
let refCount = 0
child.unref()
if (stdin.unref) {
stdin.unref()
}
if (stdout.unref) {
stdout.unref()
}
const refs: common.Refs = {
ref() { if (++refCount === 1) child.ref(); },
unref() { if (--refCount === 0) child.unref(); },
}
longLivedService = {
build: (options: types.BuildOptions) =>
new Promise<types.BuildResult>((resolve, reject) => {
service.buildOrContext({
callName: 'build',
refs,
options,
isTTY: isTTY(),
defaultWD,
callback: (err, res) => err ? reject(err) : resolve(res as types.BuildResult),
})
}),
context: (options: types.BuildOptions) =>
new Promise<types.BuildContext>((resolve, reject) =>
service.buildOrContext({
callName: 'context',
refs,
options,
isTTY: isTTY(),
defaultWD,
callback: (err, res) => err ? reject(err) : resolve(res as types.BuildContext),
})),
transform: (input: string | Uint8Array, options?: types.TransformOptions) =>
new Promise<types.TransformResult>((resolve, reject) =>
service.transform({
callName: 'transform',
refs,
input,
options: options || {},
isTTY: isTTY(),
fs: fsAsync,
callback: (err, res) => err ? reject(err) : resolve(res!),
})),
formatMessages: (messages, options) =>
new Promise((resolve, reject) =>
service.formatMessages({
callName: 'formatMessages',
refs,
messages,
options,
callback: (err, res) => err ? reject(err) : resolve(res!),
})),
analyzeMetafile: (metafile, options) =>
new Promise((resolve, reject) =>
service.analyzeMetafile({
callName: 'analyzeMetafile',
refs,
metafile: typeof metafile === 'string' ? metafile : JSON.stringify(metafile),
options,
callback: (err, res) => err ? reject(err) : resolve(res!),
})),
}
return longLivedService
}
let runServiceSync = (callback: (service: common.StreamService) => void): void => {
let [command, args] = esbuildCommandAndArgs()
let stdin = new Uint8Array()
let { readFromStdout, afterClose, service } = common.createChannel({
writeToStdin(bytes) {
if (stdin.length !== 0) throw new Error('Must run at most one command')
stdin = bytes
},
isSync: true,
hasFS: true,
esbuild: ourselves,
})
callback(service)
let stdout = child_process.execFileSync(command, args.concat(`--service=${ESBUILD_VERSION}`), {
cwd: defaultWD,
windowsHide: true,
input: stdin,
// We don't know how large the output could be. If it's too large, the
// command will fail with ENOBUFS. Reserve 16mb for now since that feels
// like it should be enough. Also allow overriding this with an environment
// variable.
maxBuffer: +process.env.ESBUILD_MAX_BUFFER! || 16 * 1024 * 1024,
})
readFromStdout(stdout)
afterClose(null)
}
let randomFileName = () => {
return path.join(os.tmpdir(), `esbuild-${crypto.randomBytes(32).toString('hex')}`)
}
interface MainToWorkerMessage {
sharedBuffer: SharedArrayBuffer
id: number
command: string
args: any[]
}
interface WorkerThreadService {
buildSync(options: types.BuildOptions): types.BuildResult
transformSync(input: string | Uint8Array, options?: types.TransformOptions): types.TransformResult
formatMessagesSync: typeof types.formatMessagesSync
analyzeMetafileSync: typeof types.analyzeMetafileSync
stop(): void
}
let workerThreadService: WorkerThreadService | null = null
let startWorkerThreadService = (worker_threads: typeof import('worker_threads')): WorkerThreadService => {
let { port1: mainPort, port2: workerPort } = new worker_threads.MessageChannel()
let worker = new worker_threads.Worker(__filename, {
workerData: { workerPort, defaultWD, esbuildVersion: ESBUILD_VERSION },
transferList: [workerPort],
// From node's documentation: https://nodejs.org/api/worker_threads.html
//
// Take care when launching worker threads from preload scripts (scripts loaded
// and run using the `-r` command line flag). Unless the `execArgv` option is
// explicitly set, new Worker threads automatically inherit the command line flags
// from the running process and will preload the same preload scripts as the main
// thread. If the preload script unconditionally launches a worker thread, every
// thread spawned will spawn another until the application crashes.
//
execArgv: [],
})
let nextID = 0
// This forbids options which would cause structured clone errors
let fakeBuildError = (text: string) => {
let error: any = new Error(`Build failed with 1 error:\nerror: ${text}`)
let errors: types.Message[] = [{ id: '', pluginName: '', text, location: null, notes: [], detail: void 0 }]
error.errors = errors
error.warnings = []
return error
}
let validateBuildSyncOptions = (options: types.BuildOptions | undefined): void => {
if (!options) return
let plugins = options.plugins
if (plugins && plugins.length > 0) throw fakeBuildError(`Cannot use plugins in synchronous API calls`)
}
// MessagePort doesn't copy the properties of Error objects. We still want
// error objects to have extra properties such as "warnings" so implement the
// property copying manually.
let applyProperties = (object: any, properties: Record<string, any>): void => {
for (let key in properties) {
object[key] = properties[key]
}
}
let runCallSync = (command: string, args: any[]): any => {
let id = nextID++
// Make a fresh shared buffer for every request. That way we can't have a
// race where a notification from the previous call overlaps with this call.
let sharedBuffer = new SharedArrayBuffer(8)
let sharedBufferView = new Int32Array(sharedBuffer)
// Send the message to the worker. Note that the worker could potentially
// complete the request before this thread returns from this call.
let msg: MainToWorkerMessage = { sharedBuffer, id, command, args }
worker.postMessage(msg)
// If the value hasn't changed (i.e. the request hasn't been completed,
// wait until the worker thread notifies us that the request is complete).
//
// Otherwise, if the value has changed, the request has already been
// completed. Don't wait in that case because the notification may never
// arrive if it has already been sent.
let status = Atomics.wait(sharedBufferView, 0, 0)
if (status !== 'ok' && status !== 'not-equal') throw new Error('Internal error: Atomics.wait() failed: ' + status)
let { message: { id: id2, resolve, reject, properties } } = worker_threads!.receiveMessageOnPort(mainPort)!
if (id !== id2) throw new Error(`Internal error: Expected id ${id} but got id ${id2}`)
if (reject) {
applyProperties(reject, properties)
throw reject
}
return resolve
}
// Calling unref() on a worker will allow the thread to exit if it's the last
// only active handle in the event system. This means node will still exit
// when there are no more event handlers from the main thread. So there's no
// need to call the "stop()" function.
worker.unref()
return {
buildSync(options) {
validateBuildSyncOptions(options)
return runCallSync('build', [options])
},
transformSync(input, options) {
return runCallSync('transform', [input, options])
},
formatMessagesSync(messages, options) {
return runCallSync('formatMessages', [messages, options])
},
analyzeMetafileSync(metafile, options) {
return runCallSync('analyzeMetafile', [metafile, options])
},
stop() {
worker.terminate()
workerThreadService = null
},
}
}
let startSyncServiceWorker = () => {
let workerPort: import('worker_threads').MessagePort = worker_threads!.workerData.workerPort
let parentPort = worker_threads!.parentPort!
// MessagePort doesn't copy the properties of Error objects. We still want
// error objects to have extra properties such as "warnings" so implement the
// property copying manually.
let extractProperties = (object: any): Record<string, any> => {
let properties: Record<string, any> = {}
if (object && typeof object === 'object') {
for (let key in object) {
properties[key] = object[key]
}
}
return properties
}
try {
let service = ensureServiceIsRunning()
// Take the default working directory from the main thread because we want it
// to be consistent. This will be the working directory that was current at
// the time the "esbuild" package was first imported.
defaultWD = worker_threads!.workerData.defaultWD
parentPort.on('message', (msg: MainToWorkerMessage) => {
(async () => {
let { sharedBuffer, id, command, args } = msg
let sharedBufferView = new Int32Array(sharedBuffer)
try {
switch (command) {
case 'build':
workerPort.postMessage({ id, resolve: await service.build(args[0]) })
break
case 'transform':
workerPort.postMessage({ id, resolve: await service.transform(args[0], args[1]) })
break
case 'formatMessages':
workerPort.postMessage({ id, resolve: await service.formatMessages(args[0], args[1]) })
break
case 'analyzeMetafile':
workerPort.postMessage({ id, resolve: await service.analyzeMetafile(args[0], args[1]) })
break
default:
throw new Error(`Invalid command: ${command}`)
}
} catch (reject) {
workerPort.postMessage({ id, reject, properties: extractProperties(reject) })
}
// The message has already been posted by this point, so it should be
// safe to wake the main thread. The main thread should always get the
// message we sent above.
// First, change the shared value. That way if the main thread attempts
// to wait for us after this point, the wait will fail because the shared
// value has changed.
Atomics.add(sharedBufferView, 0, 1)
// Then, wake the main thread. This handles the case where the main
// thread was already waiting for us before the shared value was changed.
Atomics.notify(sharedBufferView, 0, Infinity)
})()
})
}
// Creating the service can fail if the on-disk state is corrupt. In that case
// we just fail all incoming messages with whatever error message we got.
// Otherwise incoming messages will hang forever waiting for a reply.
catch (reject) {
parentPort.on('message', (msg: MainToWorkerMessage) => {
let { sharedBuffer, id } = msg
let sharedBufferView = new Int32Array(sharedBuffer)
workerPort.postMessage({ id, reject, properties: extractProperties(reject) })
// The message has already been posted by this point, so it should be
// safe to wake the main thread. The main thread should always get the
// message we sent above.
// First, change the shared value. That way if the main thread attempts
// to wait for us after this point, the wait will fail because the shared
// value has changed.
Atomics.add(sharedBufferView, 0, 1)
// Then, wake the main thread. This handles the case where the main
// thread was already waiting for us before the shared value was changed.
Atomics.notify(sharedBufferView, 0, Infinity)
})
}
}
// If we're in the worker thread, start the worker code
if (isInternalWorkerThread) {
startSyncServiceWorker()
}
// Export this module's exports as an export named "default" to try to work
// around problems due to the "default" import mess.
//
// More detail: When this module is converted to CommonJS, we add Babel's
// "__esModule" marker since this module used to be ESM. However, without this
// default export below, tools that respect the "__esModule" marker will have
// a default export of undefined since there was no default export. This is
// problematic because node's implementation of importing CommonJS into ESM
// broke compatibility with the ecosystem and decided to set the "default"
// export to "module.exports" regardless of the "__esModule" marker. I'm hoping
// that by setting "module.exports.default = module.exports", we can hopefully
// make this work ok in both environments.
export default ourselves