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  }