github.com/evanw/esbuild@v0.21.4/scripts/esbuild.js (about) 1 const childProcess = require('child_process') 2 const path = require('path') 3 const fs = require('fs') 4 const os = require('os') 5 6 const repoDir = path.dirname(__dirname) 7 const denoDir = path.join(repoDir, 'deno') 8 const npmDir = path.join(repoDir, 'npm', 'esbuild') 9 const version = fs.readFileSync(path.join(repoDir, 'version.txt'), 'utf8').trim() 10 const nodeTarget = 'node10'; // See: https://nodejs.org/en/about/releases/ 11 const denoTarget = 'deno1'; // See: https://nodejs.org/en/about/releases/ 12 const umdBrowserTarget = 'es2015'; // Transpiles "async" 13 const esmBrowserTarget = 'es2017'; // Preserves "async" 14 15 const buildNeutralLib = (esbuildPath) => { 16 const libDir = path.join(npmDir, 'lib') 17 const binDir = path.join(npmDir, 'bin') 18 fs.mkdirSync(libDir, { recursive: true }) 19 fs.mkdirSync(binDir, { recursive: true }) 20 21 // Generate "npm/esbuild/install.js" 22 childProcess.execFileSync(esbuildPath, [ 23 path.join(repoDir, 'lib', 'npm', 'node-install.ts'), 24 '--outfile=' + path.join(npmDir, 'install.js'), 25 '--bundle', 26 '--target=' + nodeTarget, 27 // Note: https://socket.dev have complained that inlining the version into 28 // the install script messes up some internal scanning that they do by 29 // making it seem like esbuild's install script code changes with every 30 // esbuild release. So now we read it from "package.json" instead. 31 // '--define:ESBUILD_VERSION=' + JSON.stringify(version), 32 '--external:esbuild', 33 '--platform=node', 34 '--log-level=warning', 35 ], { cwd: repoDir }) 36 37 // Generate "npm/esbuild/lib/main.js" 38 childProcess.execFileSync(esbuildPath, [ 39 path.join(repoDir, 'lib', 'npm', 'node.ts'), 40 '--outfile=' + path.join(libDir, 'main.js'), 41 '--bundle', 42 '--target=' + nodeTarget, 43 '--define:WASM=false', 44 '--define:ESBUILD_VERSION=' + JSON.stringify(version), 45 '--external:esbuild', 46 '--platform=node', 47 '--log-level=warning', 48 ], { cwd: repoDir }) 49 50 // Generate "npm/esbuild/bin/esbuild" 51 childProcess.execFileSync(esbuildPath, [ 52 path.join(repoDir, 'lib', 'npm', 'node-shim.ts'), 53 '--outfile=' + path.join(binDir, 'esbuild'), 54 '--bundle', 55 '--target=' + nodeTarget, 56 '--define:ESBUILD_VERSION=' + JSON.stringify(version), 57 '--external:esbuild', 58 '--platform=node', 59 '--log-level=warning', 60 ], { cwd: repoDir }) 61 62 // Generate "npm/esbuild/lib/main.d.ts" 63 const types_ts = fs.readFileSync(path.join(repoDir, 'lib', 'shared', 'types.ts'), 'utf8') 64 fs.writeFileSync(path.join(libDir, 'main.d.ts'), types_ts) 65 66 // Get supported platforms 67 const platforms = { exports: {} } 68 new Function('module', 'exports', 'require', childProcess.execFileSync(esbuildPath, [ 69 path.join(repoDir, 'lib', 'npm', 'node-platform.ts'), 70 '--bundle', 71 '--target=' + nodeTarget, 72 '--external:esbuild', 73 '--platform=node', 74 '--log-level=warning', 75 ], { cwd: repoDir }))(platforms, platforms.exports, require) 76 const optionalDependencies = Object.fromEntries(Object.values({ 77 ...platforms.exports.knownWindowsPackages, 78 ...platforms.exports.knownUnixlikePackages, 79 ...platforms.exports.knownWebAssemblyFallbackPackages, 80 }).sort().map(x => [x, version])) 81 82 // Update "npm/esbuild/package.json" 83 const pjPath = path.join(npmDir, 'package.json') 84 const package_json = JSON.parse(fs.readFileSync(pjPath, 'utf8')) 85 package_json.optionalDependencies = optionalDependencies 86 fs.writeFileSync(pjPath, JSON.stringify(package_json, null, 2) + '\n') 87 } 88 89 async function generateWorkerCode({ esbuildPath, wasm_exec_js, minify, target }) { 90 const input = ` 91 let onmessage 92 let globalThis = {} 93 for (let o = self; o; o = Object.getPrototypeOf(o)) 94 for (let k of Object.getOwnPropertyNames(o)) 95 if (!(k in globalThis)) 96 Object.defineProperty(globalThis, k, { get: () => self[k] }) 97 ${wasm_exec_js.replace(/\bfs\./g, 'globalThis.fs.')} 98 ${fs.readFileSync(path.join(repoDir, 'lib', 'shared', 'worker.ts'), 'utf8')} 99 return m => onmessage(m) 100 ` 101 const args = [ 102 '--loader=ts', 103 '--target=' + target, 104 '--define:ESBUILD_VERSION=' + JSON.stringify(version), 105 ].concat(minify ? ['--minify'] : []) 106 107 // Note: This uses "execFile" because "execFileSync" in node appears to have 108 // a bug. Specifically when using the "input" option of "execFileSync" to 109 // provide stdin, sometimes (~2% of the time?) node writes all of the input 110 // but then doesn't close the stream. The Go side is stuck reading from stdin 111 // within "ioutil.ReadAll(os.Stdin)" so I suspect it's a bug in node, not in 112 // Go. Explicitly calling "stdin.end()" on the node side appears to fix it. 113 const wasmExecAndWorker = (await new Promise((resolve, reject) => { 114 const proc = childProcess.execFile(esbuildPath, args, { cwd: repoDir }, (err, stdout) => { 115 if (err) reject(err) 116 else resolve(stdout) 117 }) 118 proc.stdin.write(input) 119 proc.stdin.end() 120 })).toString().trim() 121 122 const commentLines = wasm_exec_js.split('\n') 123 const firstNonComment = commentLines.findIndex(line => !line.startsWith('//')) 124 const commentPrefix = '\n' + commentLines.slice(0, firstNonComment).join('\n') + '\n' 125 if (minify) return `(postMessage=>{${commentPrefix}${wasmExecAndWorker}})` 126 return `((postMessage) => {${(commentPrefix + wasmExecAndWorker).replace(/\n/g, '\n ')}\n })` 127 } 128 129 exports.buildWasmLib = async (esbuildPath) => { 130 // Asynchronously start building the WebAssembly module 131 const npmWasmDir = path.join(repoDir, 'npm', 'esbuild-wasm') 132 const goBuildPromise = new Promise((resolve, reject) => childProcess.execFile('go', 133 [ 134 'build', 135 '-o', path.join(npmWasmDir, 'esbuild.wasm'), 136 '-ldflags=-s -w', // This removes ~0.14mb of unnecessary WebAssembly code 137 '-trimpath', 138 path.join(repoDir, 'cmd', 'esbuild'), 139 ], 140 { cwd: repoDir, stdio: 'inherit', env: { ...process.env, GOOS: 'js', GOARCH: 'wasm' } }, 141 err => err ? reject(err) : resolve())) 142 143 const libDir = path.join(npmWasmDir, 'lib') 144 const esmDir = path.join(npmWasmDir, 'esm') 145 fs.mkdirSync(libDir, { recursive: true }) 146 fs.mkdirSync(esmDir, { recursive: true }) 147 148 // Generate "npm/esbuild-wasm/wasm_exec.js" 149 const GOROOT = childProcess.execFileSync('go', ['env', 'GOROOT']).toString().trim() 150 let wasm_exec_js = fs.readFileSync(path.join(GOROOT, 'misc', 'wasm', 'wasm_exec.js'), 'utf8') 151 let wasm_exec_node_js = fs.readFileSync(path.join(GOROOT, 'misc', 'wasm', 'wasm_exec_node.js'), 'utf8') 152 fs.writeFileSync(path.join(npmWasmDir, 'wasm_exec.js'), wasm_exec_js) 153 fs.writeFileSync(path.join(npmWasmDir, 'wasm_exec_node.js'), wasm_exec_node_js) 154 155 // Generate "npm/esbuild-wasm/lib/main.js" 156 childProcess.execFileSync(esbuildPath, [ 157 path.join(repoDir, 'lib', 'npm', 'node.ts'), 158 '--outfile=' + path.join(libDir, 'main.js'), 159 '--bundle', 160 '--target=' + nodeTarget, 161 '--format=cjs', 162 '--define:WASM=true', 163 '--define:ESBUILD_VERSION=' + JSON.stringify(version), 164 '--external:esbuild', 165 '--platform=node', 166 '--log-level=warning', 167 ], { cwd: repoDir }) 168 169 // Generate "npm/esbuild-wasm/lib/main.d.ts" and "npm/esbuild-wasm/lib/browser.d.ts" 170 const types_ts = fs.readFileSync(path.join(repoDir, 'lib', 'shared', 'types.ts'), 'utf8') 171 fs.writeFileSync(path.join(libDir, 'main.d.ts'), types_ts) 172 fs.writeFileSync(path.join(libDir, 'browser.d.ts'), types_ts) 173 fs.writeFileSync(path.join(esmDir, 'browser.d.ts'), types_ts) 174 175 for (const minify of [false, true]) { 176 const minifyFlags = minify ? ['--minify'] : [] 177 const wasmWorkerCodeUMD = await generateWorkerCode({ esbuildPath, wasm_exec_js, minify, target: umdBrowserTarget }) 178 const wasmWorkerCodeESM = await generateWorkerCode({ esbuildPath, wasm_exec_js, minify, target: esmBrowserTarget }) 179 180 // Generate "npm/esbuild-wasm/lib/browser.*" 181 const umdPrefix = `(module=>{` 182 const umdSuffix = `})(typeof module==="object"?module:{set exports(x){(typeof self!=="undefined"?self:this).esbuild=x}});` 183 const browserCJS = childProcess.execFileSync(esbuildPath, [ 184 path.join(repoDir, 'lib', 'npm', 'browser.ts'), 185 '--bundle', 186 '--target=' + umdBrowserTarget, 187 '--format=cjs', 188 '--define:ESBUILD_VERSION=' + JSON.stringify(version), 189 '--define:WEB_WORKER_SOURCE_CODE=' + JSON.stringify(wasmWorkerCodeUMD), 190 '--banner:js=' + umdPrefix, 191 '--footer:js=' + umdSuffix, 192 '--log-level=warning', 193 ].concat(minifyFlags), { cwd: repoDir }).toString().replace('WEB_WORKER_FUNCTION', wasmWorkerCodeUMD) 194 fs.writeFileSync(path.join(libDir, minify ? 'browser.min.js' : 'browser.js'), browserCJS) 195 196 // Generate "npm/esbuild-wasm/esm/browser.*" 197 const browserESM = childProcess.execFileSync(esbuildPath, [ 198 path.join(repoDir, 'lib', 'npm', 'browser.ts'), 199 '--bundle', 200 '--target=' + esmBrowserTarget, 201 '--format=esm', 202 '--define:ESBUILD_VERSION=' + JSON.stringify(version), 203 '--define:WEB_WORKER_SOURCE_CODE=' + JSON.stringify(wasmWorkerCodeESM), 204 '--log-level=warning', 205 ].concat(minifyFlags), { cwd: repoDir }).toString().replace('WEB_WORKER_FUNCTION', wasmWorkerCodeESM) 206 fs.writeFileSync(path.join(esmDir, minify ? 'browser.min.js' : 'browser.js'), browserESM) 207 } 208 209 // Join with the asynchronous WebAssembly build 210 await goBuildPromise 211 212 // Also copy this into the WebAssembly shim directories 213 for (const dir of [ 214 path.join(repoDir, 'npm', '@esbuild', 'android-arm'), 215 path.join(repoDir, 'npm', '@esbuild', 'android-x64'), 216 ]) { 217 fs.mkdirSync(path.join(dir, 'bin'), { recursive: true }) 218 fs.writeFileSync(path.join(dir, 'wasm_exec.js'), wasm_exec_js) 219 fs.writeFileSync(path.join(dir, 'wasm_exec_node.js'), wasm_exec_node_js) 220 fs.copyFileSync(path.join(npmWasmDir, 'bin', 'esbuild'), path.join(dir, 'bin', 'esbuild')) 221 fs.copyFileSync(path.join(npmWasmDir, 'esbuild.wasm'), path.join(dir, 'esbuild.wasm')) 222 } 223 } 224 225 const buildDenoLib = async (esbuildPath) => { 226 // Generate "deno/mod.js" 227 childProcess.execFileSync(esbuildPath, [ 228 path.join(repoDir, 'lib', 'deno', 'mod.ts'), 229 '--bundle', 230 '--outfile=' + path.join(denoDir, 'mod.js'), 231 '--target=' + denoTarget, 232 '--define:ESBUILD_VERSION=' + JSON.stringify(version), 233 '--platform=neutral', 234 '--log-level=warning', 235 '--banner:js=/// <reference types="./mod.d.ts" />', 236 ], { cwd: repoDir }) 237 238 // Generate "deno/wasm.js" 239 const GOROOT = childProcess.execFileSync('go', ['env', 'GOROOT']).toString().trim() 240 let wasm_exec_js = fs.readFileSync(path.join(GOROOT, 'misc', 'wasm', 'wasm_exec.js'), 'utf8') 241 const wasmWorkerCode = await generateWorkerCode({ esbuildPath, wasm_exec_js, minify: true, target: denoTarget }) 242 const modWASM = childProcess.execFileSync(esbuildPath, [ 243 path.join(repoDir, 'lib', 'deno', 'wasm.ts'), 244 '--bundle', 245 '--target=' + denoTarget, 246 '--define:ESBUILD_VERSION=' + JSON.stringify(version), 247 '--define:WEB_WORKER_SOURCE_CODE=' + JSON.stringify(wasmWorkerCode), 248 '--platform=neutral', 249 '--log-level=warning', 250 '--banner:js=/// <reference types="./wasm.d.ts" />', 251 ], { cwd: repoDir }).toString().replace('WEB_WORKER_FUNCTION', wasmWorkerCode) 252 fs.writeFileSync(path.join(denoDir, 'wasm.js'), modWASM) 253 254 // Generate "deno/mod.d.ts" 255 const types_ts = fs.readFileSync(path.join(repoDir, 'lib', 'shared', 'types.ts'), 'utf8') 256 fs.writeFileSync(path.join(denoDir, 'mod.d.ts'), types_ts) 257 fs.writeFileSync(path.join(denoDir, 'wasm.d.ts'), types_ts) 258 259 // And copy the WebAssembly file over to the Deno library as well 260 fs.copyFileSync(path.join(repoDir, 'npm', 'esbuild-wasm', 'esbuild.wasm'), path.join(repoDir, 'deno', 'esbuild.wasm')) 261 } 262 263 // Writing a file atomically is important for watch mode tests since we don't 264 // want to read the file after it has been truncated but before the new contents 265 // have been written. 266 exports.writeFileAtomic = (where, contents) => { 267 // Note: Can't use "os.tmpdir()" because that doesn't work on Windows. CI runs 268 // tests on D:\ and the temporary directory is on C:\ or the other way around. 269 // And apparently it's impossible to move files between C:\ and D:\ or something. 270 // So we have to write the file in the same directory as the destination. This is 271 // unfortunate because it will unnecessarily trigger extra watch mode rebuilds. 272 // So we have to make our tests extra robust so they can still work with random 273 // extra rebuilds thrown in. 274 const file = path.join(path.dirname(where), '.esbuild-atomic-file-' + Math.random().toString(36).slice(2)) 275 fs.writeFileSync(file, contents) 276 fs.renameSync(file, where) 277 } 278 279 exports.buildBinary = () => { 280 childProcess.execFileSync('go', ['build', '-ldflags=-s -w', '-trimpath', './cmd/esbuild'], { cwd: repoDir, stdio: 'ignore' }) 281 return path.join(repoDir, process.platform === 'win32' ? 'esbuild.exe' : 'esbuild') 282 } 283 284 exports.removeRecursiveSync = path => { 285 try { 286 fs.rmSync(path, { recursive: true }) 287 } catch (e) { 288 // Removing stuff on Windows is flaky and unreliable. Don't fail tests 289 // on CI if Windows is just being a pain. Common causes of flakes include 290 // random EPERM and ENOTEMPTY errors. 291 // 292 // The general "solution" to this is to try asking Windows to redo the 293 // failing operation repeatedly until eventually giving up after a 294 // timeout. But that doesn't guarantee that flakes will be fixed so we 295 // just give up instead. People that want reasonable file system 296 // behavior on Windows should use WSL instead. 297 } 298 } 299 300 const updateVersionPackageJSON = pathToPackageJSON => { 301 const version = fs.readFileSync(path.join(path.dirname(__dirname), 'version.txt'), 'utf8').trim() 302 const json = JSON.parse(fs.readFileSync(pathToPackageJSON, 'utf8')) 303 304 if (json.version !== version) { 305 json.version = version 306 fs.writeFileSync(pathToPackageJSON, JSON.stringify(json, null, 2) + '\n') 307 } 308 } 309 310 exports.installForTests = () => { 311 // Build the "esbuild" binary and library 312 const esbuildPath = exports.buildBinary() 313 buildNeutralLib(esbuildPath) 314 315 // Install the "esbuild" package to a temporary directory. On Windows, it's 316 // sometimes randomly impossible to delete this installation directory. My 317 // best guess is that this is because the esbuild process is kept alive until 318 // the process exits for "buildSync" and "transformSync", and that sometimes 319 // prevents Windows from deleting the directory it's in. The call in tests to 320 // "rimraf.sync()" appears to hang when this happens. Other operating systems 321 // don't have a problem with this. This has only been a problem on the Windows 322 // VM in GitHub CI. I cannot reproduce this issue myself. 323 const installDir = path.join(os.tmpdir(), 'esbuild-' + Math.random().toString(36).slice(2)) 324 const env = { ...process.env, ESBUILD_BINARY_PATH: esbuildPath } 325 fs.mkdirSync(installDir) 326 fs.writeFileSync(path.join(installDir, 'package.json'), '{}') 327 childProcess.execSync(`npm pack --silent "${npmDir}"`, { cwd: installDir, stdio: 'inherit' }) 328 childProcess.execSync(`npm install --silent --no-audit --no-optional --ignore-scripts=false --progress=false esbuild-${version}.tgz`, { cwd: installDir, env, stdio: 'inherit' }) 329 330 // Evaluate the code 331 const ESBUILD_PACKAGE_PATH = path.join(installDir, 'node_modules', 'esbuild') 332 const mod = require(ESBUILD_PACKAGE_PATH) 333 Object.defineProperty(mod, 'ESBUILD_PACKAGE_PATH', { value: ESBUILD_PACKAGE_PATH }) 334 return mod 335 } 336 337 const updateVersionGo = () => { 338 const version_txt = fs.readFileSync(path.join(repoDir, 'version.txt'), 'utf8').trim() 339 const version_go = `package main\n\nconst esbuildVersion = "${version_txt}"\n` 340 const version_go_path = path.join(repoDir, 'cmd', 'esbuild', 'version.go') 341 342 // Update this atomically to avoid issues with this being overwritten during use 343 const temp_path = version_go_path + Math.random().toString(36).slice(1) 344 fs.writeFileSync(temp_path, version_go) 345 fs.renameSync(temp_path, version_go_path) 346 } 347 348 // This is helpful for ES6 modules which don't have access to __dirname 349 exports.dirname = __dirname 350 351 // The main Makefile invokes this script before publishing 352 if (require.main === module) { 353 if (process.argv.indexOf('--wasm') >= 0) exports.buildWasmLib(process.argv[2]) 354 else if (process.argv.indexOf('--deno') >= 0) buildDenoLib(process.argv[2]) 355 else if (process.argv.indexOf('--version') >= 0) updateVersionPackageJSON(process.argv[2]) 356 else if (process.argv.indexOf('--neutral') >= 0) buildNeutralLib(process.argv[2]) 357 else if (process.argv.indexOf('--update-version-go') >= 0) updateVersionGo() 358 else throw new Error('Expected a flag') 359 }