mirror of
https://github.com/zhigang1992/esbuild.git
synced 2026-01-12 22:46:54 +08:00
293 lines
12 KiB
JavaScript
293 lines
12 KiB
JavaScript
const childProcess = require('child_process')
|
|
const path = require('path')
|
|
const zlib = require('zlib')
|
|
const fs = require('fs')
|
|
const os = require('os')
|
|
|
|
const repoDir = path.dirname(__dirname)
|
|
const denoDir = path.join(repoDir, 'deno')
|
|
const npmDir = path.join(repoDir, 'npm', 'esbuild')
|
|
const version = fs.readFileSync(path.join(repoDir, 'version.txt'), 'utf8').trim()
|
|
const nodeTarget = 'node10'; // See: https://nodejs.org/en/about/releases/
|
|
const umdBrowserTarget = 'es2015'; // Transpiles "async"
|
|
const esmBrowserTarget = 'es2017'; // Preserves "async"
|
|
|
|
exports.buildNativeLib = (esbuildPath) => {
|
|
const libDir = path.join(npmDir, 'lib')
|
|
fs.mkdirSync(libDir, { recursive: true })
|
|
|
|
// Generate "npm/esbuild/install.js"
|
|
childProcess.execFileSync(esbuildPath, [
|
|
path.join(repoDir, 'lib', 'npm', 'install.ts'),
|
|
'--outfile=' + path.join(npmDir, 'install.js'),
|
|
'--target=' + nodeTarget,
|
|
'--define:ESBUILD_VERSION=' + JSON.stringify(version),
|
|
'--platform=node',
|
|
'--log-level=warning',
|
|
], { cwd: repoDir })
|
|
|
|
// Generate "npm/esbuild/lib/main.js"
|
|
childProcess.execFileSync(esbuildPath, [
|
|
path.join(repoDir, 'lib', 'npm', 'node.ts'),
|
|
'--outfile=' + path.join(libDir, 'main.js'),
|
|
'--bundle',
|
|
'--target=' + nodeTarget,
|
|
'--format=cjs',
|
|
'--define:WASM=false',
|
|
'--define:ESBUILD_VERSION=' + JSON.stringify(version),
|
|
'--platform=node',
|
|
'--log-level=warning',
|
|
], { cwd: repoDir })
|
|
|
|
// Generate "npm/esbuild/lib/main.d.ts"
|
|
const types_ts = fs.readFileSync(path.join(repoDir, 'lib', 'shared', 'types.ts'), 'utf8')
|
|
fs.writeFileSync(path.join(libDir, 'main.d.ts'), types_ts)
|
|
}
|
|
|
|
exports.buildWasmLib = async (esbuildPath) => {
|
|
// Asynchronously start building the WebAssembly module
|
|
const npmWasmDir = path.join(repoDir, 'npm', 'esbuild-wasm')
|
|
const goBuildPromise = new Promise((resolve, reject) => childProcess.execFile('go',
|
|
[
|
|
'build',
|
|
'-o', path.join(npmWasmDir, 'esbuild.wasm'),
|
|
'-ldflags=-s -w', // This removes ~0.14mb of unnecessary WebAssembly code
|
|
path.join(repoDir, 'cmd', 'esbuild'),
|
|
],
|
|
{ cwd: repoDir, stdio: 'inherit', env: { ...process.env, GOOS: 'js', GOARCH: 'wasm' } },
|
|
err => err ? reject(err) : resolve()))
|
|
|
|
const libDir = path.join(npmWasmDir, 'lib')
|
|
const esmDir = path.join(npmWasmDir, 'esm')
|
|
fs.mkdirSync(libDir, { recursive: true })
|
|
fs.mkdirSync(esmDir, { recursive: true })
|
|
|
|
// Generate "npm/esbuild-wasm/wasm_exec.js"
|
|
const toReplace = 'global.fs = fs;';
|
|
const GOROOT = childProcess.execFileSync('go', ['env', 'GOROOT']).toString().trim();
|
|
let wasm_exec_js = fs.readFileSync(path.join(GOROOT, 'misc', 'wasm', 'wasm_exec.js'), 'utf8');
|
|
let index = wasm_exec_js.indexOf(toReplace);
|
|
if (index === -1) throw new Error(`Failed to find ${JSON.stringify(toReplace)} in Go JS shim code`);
|
|
wasm_exec_js = wasm_exec_js.replace(toReplace, `
|
|
global.fs = Object.assign({}, fs, {
|
|
// Hack around a Unicode bug in node: https://github.com/nodejs/node/issues/24550
|
|
write(fd, buf, offset, length, position, callback) {
|
|
if (offset === 0 && length === buf.length && position === null) {
|
|
if (fd === process.stdout.fd) {
|
|
try {
|
|
process.stdout.write(buf, err => err ? callback(err, 0, null) : callback(null, length, buf));
|
|
} catch (err) {
|
|
callback(err, 0, null);
|
|
}
|
|
return;
|
|
}
|
|
if (fd === process.stderr.fd) {
|
|
try {
|
|
process.stderr.write(buf, err => err ? callback(err, 0, null) : callback(null, length, buf));
|
|
} catch (err) {
|
|
callback(err, 0, null);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
fs.write(fd, buf, offset, length, position, callback);
|
|
},
|
|
});
|
|
`);
|
|
fs.writeFileSync(path.join(npmWasmDir, 'wasm_exec.js'), wasm_exec_js);
|
|
|
|
// Generate "npm/esbuild-wasm/lib/main.js"
|
|
childProcess.execFileSync(esbuildPath, [
|
|
path.join(repoDir, 'lib', 'npm', 'node.ts'),
|
|
'--outfile=' + path.join(libDir, 'main.js'),
|
|
'--bundle',
|
|
'--target=' + nodeTarget,
|
|
'--format=cjs',
|
|
'--define:WASM=true',
|
|
'--define:ESBUILD_VERSION=' + JSON.stringify(version),
|
|
'--platform=node',
|
|
'--log-level=warning',
|
|
], { cwd: repoDir })
|
|
|
|
// Generate "npm/esbuild-wasm/lib/main.d.ts" and "npm/esbuild-wasm/lib/browser.d.ts"
|
|
const types_ts = fs.readFileSync(path.join(repoDir, 'lib', 'shared', 'types.ts'), 'utf8')
|
|
fs.writeFileSync(path.join(libDir, 'main.d.ts'), types_ts)
|
|
fs.writeFileSync(path.join(libDir, 'browser.d.ts'), types_ts)
|
|
fs.writeFileSync(path.join(esmDir, 'browser.d.ts'), types_ts)
|
|
|
|
for (const minify of [false, true]) {
|
|
const minifyFlags = minify ? ['--minify'] : []
|
|
let wasmWorkerCode = {}
|
|
|
|
for (const [format, target] of Object.entries({ umd: umdBrowserTarget, esm: esmBrowserTarget })) {
|
|
// Process "npm/esbuild-wasm/wasm_exec.js"
|
|
let wasmExecCode = wasm_exec_js;
|
|
if (minify) {
|
|
const wasmExecMin = childProcess.execFileSync(esbuildPath, [
|
|
'--target=' + target,
|
|
].concat(minifyFlags), { cwd: repoDir, input: wasmExecCode }).toString()
|
|
const commentLines = wasmExecCode.split('\n')
|
|
const firstNonComment = commentLines.findIndex(line => !line.startsWith('//'))
|
|
wasmExecCode = '\n' + commentLines.slice(0, firstNonComment).concat(wasmExecMin).join('\n')
|
|
}
|
|
|
|
// Process "lib/worker.ts"
|
|
const workerCode = childProcess.execFileSync(esbuildPath, [
|
|
path.join(repoDir, 'lib', 'npm', 'worker.ts'),
|
|
'--target=' + target,
|
|
'--define:ESBUILD_VERSION=' + JSON.stringify(version),
|
|
'--log-level=warning',
|
|
].concat(minifyFlags), { cwd: repoDir }).toString().trim()
|
|
|
|
wasmWorkerCode[format] = wasmExecCode + workerCode
|
|
}
|
|
|
|
// Generate "npm/esbuild-wasm/lib/browser.*"
|
|
const umdPrefix = `(exports=>{`
|
|
const umdSuffix = `})(typeof exports==="object"?exports:(typeof self!=="undefined"?self:this).esbuild={});`
|
|
const browserCJS = childProcess.execFileSync(esbuildPath, [
|
|
path.join(repoDir, 'lib', 'npm', 'browser.ts'),
|
|
'--bundle',
|
|
'--target=' + umdBrowserTarget,
|
|
'--format=cjs',
|
|
'--define:ESBUILD_VERSION=' + JSON.stringify(version),
|
|
'--define:WEB_WORKER_SOURCE_CODE=' + JSON.stringify(wasmWorkerCode.umd),
|
|
'--banner:js=' + umdPrefix,
|
|
'--footer:js=' + umdSuffix,
|
|
'--log-level=warning',
|
|
].concat(minifyFlags), { cwd: repoDir }).toString()
|
|
fs.writeFileSync(path.join(libDir, minify ? 'browser.min.js' : 'browser.js'), browserCJS)
|
|
|
|
// Generate "npm/esbuild-wasm/esm/browser.min.js"
|
|
const browserESM = childProcess.execFileSync(esbuildPath, [
|
|
path.join(repoDir, 'lib', 'npm', 'browser.ts'),
|
|
'--bundle',
|
|
'--target=' + esmBrowserTarget,
|
|
'--format=esm',
|
|
'--define:ESBUILD_VERSION=' + JSON.stringify(version),
|
|
'--define:WEB_WORKER_SOURCE_CODE=' + JSON.stringify(wasmWorkerCode.esm),
|
|
'--log-level=warning',
|
|
].concat(minifyFlags), { cwd: repoDir }).toString()
|
|
fs.writeFileSync(path.join(esmDir, minify ? 'browser.min.js' : 'browser.js'), browserESM)
|
|
}
|
|
|
|
// Generate the "exit0" stubs
|
|
const exit0Map = {};
|
|
const exit0Dir = path.join(repoDir, 'lib', 'npm', 'exit0');
|
|
for (const entry of fs.readdirSync(exit0Dir)) {
|
|
if (entry.endsWith('.node')) {
|
|
const absPath = path.join(exit0Dir, entry);
|
|
const compressed = zlib.deflateRawSync(fs.readFileSync(absPath), { level: 9 });
|
|
exit0Map[entry] = compressed.toString('base64');
|
|
}
|
|
}
|
|
fs.writeFileSync(path.join(npmWasmDir, 'exit0.js'), `
|
|
// Each of these is a native module that calls "exit(0)". This is a workaround
|
|
// for https://github.com/nodejs/node/issues/36616. These native modules are
|
|
// stored in a string both to make them smaller and to hide them from Yarn 2,
|
|
// since they make Yarn 2 unzip this package.
|
|
|
|
module.exports = ${JSON.stringify(exit0Map, null, 2)};
|
|
`);
|
|
|
|
// Join with the asynchronous WebAssembly build
|
|
await goBuildPromise;
|
|
}
|
|
|
|
exports.buildDenoLib = (esbuildPath) => {
|
|
// Generate "deno/esbuild/mod.js"
|
|
childProcess.execFileSync(esbuildPath, [
|
|
path.join(repoDir, 'lib', 'deno', 'mod.ts'),
|
|
'--bundle',
|
|
'--outfile=' + path.join(denoDir, 'mod.js'),
|
|
'--target=es2020',
|
|
'--define:ESBUILD_VERSION=' + JSON.stringify(version),
|
|
'--platform=neutral',
|
|
'--log-level=warning',
|
|
'--banner:js=/// <reference path="./mod.d.ts" />',
|
|
], { cwd: repoDir })
|
|
|
|
// Generate "deno/esbuild/mod.d.ts"
|
|
const types_ts = fs.readFileSync(path.join(repoDir, 'lib', 'shared', 'types.ts'), 'utf8')
|
|
fs.writeFileSync(path.join(denoDir, 'mod.d.ts'), types_ts +
|
|
`\n// Unlike node, Deno lacks the necessary APIs to clean up child processes` +
|
|
`\n// automatically. You must manually call stop() in Deno when you're done` +
|
|
`\n// using esbuild or Deno will continue running forever.` +
|
|
`\nexport function stop(): void;` +
|
|
`\n`)
|
|
}
|
|
|
|
// Writing a file atomically is important for watch mode tests since we don't
|
|
// want to read the file after it has been truncated but before the new contents
|
|
// have been written.
|
|
exports.writeFileAtomic = (where, contents) => {
|
|
// Note: Can't use "os.tmpdir()" because that doesn't work on Windows. CI runs
|
|
// tests on D:\ and the temporary directory is on C:\ or the other way around.
|
|
// And apparently it's impossible to move files between C:\ and D:\ or something.
|
|
// So we have to write the file in the same directory as the destination. This is
|
|
// unfortunate because it will unnecessarily trigger extra watch mode rebuilds.
|
|
// So we have to make our tests extra robust so they can still work with random
|
|
// extra rebuilds thrown in.
|
|
const file = path.join(path.dirname(where), '.esbuild-atomic-file-' + Math.random().toString(36).slice(2))
|
|
fs.writeFileSync(file, contents)
|
|
fs.renameSync(file, where)
|
|
}
|
|
|
|
exports.buildBinary = () => {
|
|
childProcess.execFileSync('go', ['build', '-ldflags=-s -w', './cmd/esbuild'], { cwd: repoDir, stdio: 'ignore' })
|
|
return path.join(repoDir, process.platform === 'win32' ? 'esbuild.exe' : 'esbuild')
|
|
}
|
|
|
|
exports.removeRecursiveSync = path => {
|
|
try {
|
|
fs.rmSync(path, { recursive: true })
|
|
} catch (e) {
|
|
// Removing stuff on Windows is flaky and unreliable. Don't fail tests
|
|
// on CI if Windows is just being a pain. Common causes of flakes include
|
|
// random EPERM and ENOTEMPTY errors.
|
|
//
|
|
// The general "solution" to this is to try asking Windows to redo the
|
|
// failing operation repeatedly until eventually giving up after a
|
|
// timeout. But that doesn't guarantee that flakes will be fixed so we
|
|
// just give up instead. People that want reasonable file system
|
|
// behavior on Windows should use WSL instead.
|
|
}
|
|
}
|
|
|
|
exports.installForTests = () => {
|
|
// Build the "esbuild" binary and library
|
|
const esbuildPath = exports.buildBinary()
|
|
exports.buildNativeLib(esbuildPath)
|
|
|
|
// Install the "esbuild" package to a temporary directory. On Windows, it's
|
|
// sometimes randomly impossible to delete this installation directory. My
|
|
// best guess is that this is because the esbuild process is kept alive until
|
|
// the process exits for "buildSync" and "transformSync", and that sometimes
|
|
// prevents Windows from deleting the directory it's in. The call in tests to
|
|
// "rimraf.sync()" appears to hang when this happens. Other operating systems
|
|
// don't have a problem with this. This has only been a problem on the Windows
|
|
// VM in GitHub CI. I cannot reproduce this issue myself.
|
|
const installDir = path.join(os.tmpdir(), 'esbuild-' + Math.random().toString(36).slice(2))
|
|
const env = { ...process.env, ESBUILD_BINARY_PATH: esbuildPath }
|
|
fs.mkdirSync(installDir)
|
|
fs.writeFileSync(path.join(installDir, 'package.json'), '{}')
|
|
childProcess.execSync(`npm pack --silent "${npmDir}"`, { cwd: installDir, stdio: 'inherit' })
|
|
childProcess.execSync(`npm install --silent --no-audit --progress=false esbuild-${version}.tgz`, { cwd: installDir, env, stdio: 'inherit' })
|
|
|
|
// Evaluate the code
|
|
const ESBUILD_PACKAGE_PATH = path.join(installDir, 'node_modules', 'esbuild')
|
|
const mod = require(ESBUILD_PACKAGE_PATH)
|
|
mod.ESBUILD_PACKAGE_PATH = ESBUILD_PACKAGE_PATH
|
|
return mod
|
|
}
|
|
|
|
// This is helpful for ES6 modules which don't have access to __dirname
|
|
exports.dirname = __dirname
|
|
|
|
// The main Makefile invokes this script before publishing
|
|
if (require.main === module) {
|
|
if (process.argv.indexOf('--wasm') >= 0) exports.buildWasmLib(process.argv[2])
|
|
else if (process.argv.indexOf('--deno') >= 0) exports.buildDenoLib(process.argv[2])
|
|
else exports.buildNativeLib(process.argv[2])
|
|
}
|