diff --git a/CHANGELOG.md b/CHANGELOG.md index 6017ad0e0d8..cee305cefb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,12 @@ The workaround was to manually check for this case and then ignore the error in this specific case. With this release, it should now be possible to pipe something to the `esbuild` command on Windows. +* Fix stdout and stderr not supporting Unicode in the `esbuild-wasm` package on Windows ([#687](https://github.com/evanw/esbuild/issues/687)) + + Node's `fs.write` API is broken when writing Unicode to stdout and stderr on Windows, and this will never be fixed: [nodejs/node#24550](https://github.com/nodejs/node/issues/24550). This is problematic for Go's WebAssembly implementation because it uses this API for writing to all file descriptors. + + The workaround is to manually intercept the file descriptors for stdout and stderr and redirect them to `process.stdout` and `process.stderr` respectively. Passing Unicode text to `write()` on these objects instead of on the `fs` API strangely works fine. So with this release, Unicode text should now display correctly when using esbuild's WebAssembly implementation on Windows (or at least, as correctly as the poor Unicode support in Windows Command Prompt allows). + * Add a hack for faster command-line execution for the WebAssembly module in certain cases Node has an unfortunate bug where the node process is unnecessarily kept open while a WebAssembly module is being optimized: https://github.com/nodejs/node/issues/36616. This means cases where running `esbuild` should take a few milliseconds can end up taking many seconds instead. diff --git a/scripts/esbuild.js b/scripts/esbuild.js index 40af767e616..860a39b5137 100644 --- a/scripts/esbuild.js +++ b/scripts/esbuild.js @@ -52,11 +52,37 @@ exports.buildWasmLib = async (esbuildPath) => { 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(); - fs.copyFileSync( - path.join(GOROOT, 'misc', 'wasm', 'wasm_exec.js'), - path.join(npmWasmDir, 'wasm_exec.js'), - ); + 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 bug in node: https://github.com/nodejs/node/issues/24550 + writeSync(fd, buf) { + if (fd === process.stdout.fd) return process.stdout.write(buf), buf.length; + if (fd === process.stderr.fd) return process.stderr.write(buf), buf.length; + return fs.writeSync(fd, buf); + }, + 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); } + catch (err) { return callback(err, 0, null); } + return callback(null, length, buf); + } + if (fd === process.stderr.fd) { + try { process.stderr.write(buf); } + catch (err) { return callback(err, 0, null); } + return callback(null, length, buf); + } + } + 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, [ @@ -80,13 +106,11 @@ exports.buildWasmLib = async (esbuildPath) => { const minifyFlags = minify ? ['--minify'] : [] // Process "npm/esbuild-wasm/wasm_exec.js" - const wasm_exec_js = path.join(npmWasmDir, 'wasm_exec.js') - let wasmExecCode = fs.readFileSync(wasm_exec_js, 'utf8'); + let wasmExecCode = wasm_exec_js; if (minify) { const wasmExecMin = childProcess.execFileSync(esbuildPath, [ - wasm_exec_js, '--target=es2015', - ].concat(minifyFlags), { cwd: repoDir }).toString() + ].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') diff --git a/scripts/wasm-tests.js b/scripts/wasm-tests.js index afc6680ea66..6d36ad28992 100644 --- a/scripts/wasm-tests.js +++ b/scripts/wasm-tests.js @@ -39,6 +39,40 @@ const tests = { assert.deepStrictEqual(exports.default, 3); }, + stdinStdoutUnicodeTest({ testDir, esbuildPath }) { + const stdout = child_process.execFileSync('node', [ + esbuildPath, + '--format=cjs', + ], { + stdio: ['pipe', 'pipe', 'inherit'], + cwd: testDir, + input: `export default ['π', '🍕']`, + }).toString(); + + // Check that the bundle is valid + const module = { exports: {} }; + new Function('module', 'exports', stdout)(module, module.exports); + assert.deepStrictEqual(module.exports.default, ['π', '🍕']); + }, + + stdinOutfileUnicodeTest({ testDir, esbuildPath }) { + const outfile = path.join(testDir, 'out.js') + child_process.execFileSync('node', [ + esbuildPath, + '--bundle', + '--format=cjs', + '--outfile=' + outfile, + ], { + stdio: ['pipe', 'pipe', 'inherit'], + cwd: testDir, + input: `export default ['π', '🍕']`, + }).toString(); + + // Check that the bundle is valid + const exports = require(outfile); + assert.deepStrictEqual(exports.default, ['π', '🍕']); + }, + importRelativeFileTest({ testDir, esbuildPath }) { const outfile = path.join(testDir, 'out.js') const packageJSON = path.join(__dirname, '..', 'npm', 'esbuild-wasm', 'package.json');