diff --git a/CHANGELOG.md b/CHANGELOG.md index 66ba0436d47..eefce071c0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +* JavaScript build API can now avoid writing to the file system ([#139](https://github.com/evanw/esbuild/issues/139) and [#220](https://github.com/evanw/esbuild/issues/220)) + + You can now pass `write: false` to the JavaScript build API to avoid writing to the file system. Instead, the returned object will have the `outputFiles` property with an array of output files, each of which has a string `path` property and a Uint8Array `contents` property. This brings the JavaScript API to parity with the Go API, which already had this feature. + ## 0.5.21 * Binaries for FreeBSD ([#217](https://github.com/evanw/esbuild/pull/217)) diff --git a/cmd/esbuild/service.go b/cmd/esbuild/service.go index 62e6ea5eb3c..b5bc38b38b8 100644 --- a/cmd/esbuild/service.go +++ b/cmd/esbuild/service.go @@ -183,6 +183,17 @@ func handlePingRequest(responses chan responseType, id string, rawArgs []string) } func handleBuildRequest(responses chan responseType, id string, rawArgs []string) { + // Special-case the service-only write flag + write := true + for i, arg := range rawArgs { + if arg == "--write=false" { + write = false + copy(rawArgs[i:], rawArgs[i+1:]) + rawArgs = rawArgs[:len(rawArgs)-1] + break + } + } + options, err := cli.ParseBuildOptions(rawArgs) if err != nil { responses <- responseType{ @@ -193,21 +204,41 @@ func handleBuildRequest(responses chan responseType, id string, rawArgs []string } result := api.Build(options) - for _, outputFile := range result.OutputFiles { - if err := os.MkdirAll(filepath.Dir(outputFile.Path), 0755); err != nil { - result.Errors = append(result.Errors, api.Message{Text: fmt.Sprintf( - "Failed to create output directory: %s", err.Error())}) - } else if err := ioutil.WriteFile(outputFile.Path, outputFile.Contents, 0644); err != nil { - result.Errors = append(result.Errors, api.Message{Text: fmt.Sprintf( - "Failed to write to output file: %s", err.Error())}) - } - } - - responses <- responseType{ + response := responseType{ "id": []byte(id), "errors": messagesToJSON(result.Errors), "warnings": messagesToJSON(result.Warnings), } + + if write { + // Write the output files to disk + for _, outputFile := range result.OutputFiles { + if err := os.MkdirAll(filepath.Dir(outputFile.Path), 0755); err != nil { + result.Errors = append(result.Errors, api.Message{Text: fmt.Sprintf( + "Failed to create output directory: %s", err.Error())}) + } else if err := ioutil.WriteFile(outputFile.Path, outputFile.Contents, 0644); err != nil { + result.Errors = append(result.Errors, api.Message{Text: fmt.Sprintf( + "Failed to write to output file: %s", err.Error())}) + } + } + } else { + // Pass the output files back to the caller + length := 4 + for _, outputFile := range result.OutputFiles { + length += 4 + len(outputFile.Path) + 4 + len(outputFile.Contents) + } + bytes := make([]byte, 0, length) + bytes = writeUint32(bytes, uint32(len(result.OutputFiles))) + for _, outputFile := range result.OutputFiles { + bytes = writeUint32(bytes, uint32(len(outputFile.Path))) + bytes = append(bytes, outputFile.Path...) + bytes = writeUint32(bytes, uint32(len(outputFile.Contents))) + bytes = append(bytes, outputFile.Contents...) + } + response["outputFiles"] = bytes + } + + responses <- response } func handleTransformRequest(responses chan responseType, id string, rawArgs []string) { diff --git a/lib/api-common.ts b/lib/api-common.ts index 4689754d248..3fb9e95be6a 100644 --- a/lib/api-common.ts +++ b/lib/api-common.ts @@ -36,6 +36,7 @@ function flagsForBuildOptions(options: types.BuildOptions, isTTY: boolean): stri if (options.resolveExtensions) flags.push(`--resolve-extensions=${options.resolveExtensions.join(',')}`); if (options.external) for (let name of options.external) flags.push(`--external:${name}`); if (options.loader) for (let ext in options.loader) flags.push(`--loader:${ext}=${options.loader[ext]}`); + if (options.write === false) flags.push(`--write=false`); for (let entryPoint of options.entryPoints) { if (entryPoint.startsWith('-')) throw new Error(`Invalid entry point: ${entryPoint}`); @@ -57,7 +58,7 @@ function flagsForTransformOptions(options: types.TransformOptions, isTTY: boolea } type Request = string[]; -type Response = { [key: string]: string }; +type Response = { [key: string]: Uint8Array }; type ResponseCallback = (err: string | null, res: Response) => void; export interface StreamIn { @@ -198,8 +199,8 @@ export function createChannel(options: StreamIn): StreamOut { let keyLength = readUInt32LE(bytes, eat(4)); let key = codec.decode(bytes.slice(offset, eat(keyLength) + keyLength)); let valueLength = readUInt32LE(bytes, eat(4)); - let value = codec.decode(bytes.slice(offset, eat(valueLength) + valueLength)); - if (key === 'id') id = value; + let value = bytes.slice(offset, eat(valueLength) + valueLength); + if (key === 'id') id = codec.decode(value); else response[key] = value; } @@ -207,7 +208,7 @@ export function createChannel(options: StreamIn): StreamOut { if (!id) throw new Error('Invalid message'); let callback = requests.get(id)!; requests.delete(id); - if (response.error) callback(response.error, {}); + if (response.error) callback(codec.decode(response.error), {}); else callback(null, response); }; @@ -220,10 +221,12 @@ export function createChannel(options: StreamIn): StreamOut { let flags = flagsForBuildOptions(options, isTTY); sendRequest(['build'].concat(flags), (error, response) => { if (error) return callback(new Error(error), null); - let errors = jsonToMessages(response.errors); - let warnings = jsonToMessages(response.warnings); + let errors = jsonToMessages(codec.decode(response.errors)); + let warnings = jsonToMessages(codec.decode(response.warnings)); if (errors.length > 0) return callback(failureErrorWithLog('Build failed', errors, warnings), null); - callback(null, { warnings }); + let result: types.BuildResult = { warnings }; + if (options.write === false) result.outputFiles = decodeOutputFiles(codec, response.outputFiles); + callback(null, result); }); }, @@ -231,10 +234,10 @@ export function createChannel(options: StreamIn): StreamOut { let flags = flagsForTransformOptions(options, isTTY); sendRequest(['transform', input].concat(flags), (error, response) => { if (error) return callback(new Error(error), null); - let errors = jsonToMessages(response.errors); - let warnings = jsonToMessages(response.warnings); + let errors = jsonToMessages(codec.decode(response.errors)); + let warnings = jsonToMessages(codec.decode(response.warnings)); if (errors.length > 0) return callback(failureErrorWithLog('Transform failed', errors, warnings), null); - callback(null, { warnings, js: response.js, jsSourceMap: response.jsSourceMap }); + callback(null, { warnings, js: codec.decode(response.js), jsSourceMap: codec.decode(response.jsSourceMap) }); }); }, }, @@ -287,3 +290,20 @@ function failureErrorWithLog(text: string, errors: types.Message[], warnings: ty error.warnings = warnings; return error; } + +function decodeOutputFiles(codec: TextCodec, bytes: Uint8Array): types.OutputFile[] { + let outputFiles: types.OutputFile[] = []; + let offset = 0; + let count = readUInt32LE(bytes, offset); + offset += 4; + for (let i = 0; i < count; i++) { + let pathLength = readUInt32LE(bytes, offset); + let path = codec.decode(bytes.slice(offset + 4, offset + 4 + pathLength)); + offset += 4 + pathLength; + let contentsLength = readUInt32LE(bytes, offset); + let contents = new Uint8Array(bytes.slice(offset + 4, offset + 4 + contentsLength)); + offset += 4 + contentsLength; + outputFiles.push({ path, contents }); + } + return outputFiles; +} diff --git a/lib/api-types.ts b/lib/api-types.ts index d553a8eb6f3..aa4a1506995 100644 --- a/lib/api-types.ts +++ b/lib/api-types.ts @@ -37,6 +37,7 @@ export interface BuildOptions extends CommonOptions { external?: string[]; loader?: { [ext: string]: Loader }; resolveExtensions?: string[]; + write?: boolean; entryPoints: string[]; } @@ -52,8 +53,14 @@ export interface Message { }; } +export interface OutputFile { + path: string; + contents: Uint8Array; +} + export interface BuildResult { warnings: Message[]; + outputFiles?: OutputFile[]; // Only when "write: false" } export interface BuildFailure extends Error { diff --git a/scripts/js-api-tests.js b/scripts/js-api-tests.js index e619da858d6..09ef011c78e 100644 --- a/scripts/js-api-tests.js +++ b/scripts/js-api-tests.js @@ -14,7 +14,8 @@ let buildTests = { const input = path.join(testDir, 'es6_to_cjs-in.js') const output = path.join(testDir, 'es6_to_cjs-out.js') await util.promisify(fs.writeFile)(input, 'export default 123') - await esbuild.build({ entryPoints: [input], bundle: true, outfile: output, format: 'cjs' }) + const value = await esbuild.build({ entryPoints: [input], bundle: true, outfile: output, format: 'cjs' }) + assert.strictEqual(value.outputFiles, void 0) const result = require(output) assert.strictEqual(result.default, 123) assert.strictEqual(result.__esModule, true) @@ -143,6 +144,32 @@ let buildTests = { assert.strictEqual(typeof outputInputs[makePath(imported)].bytesInOutput, 'number') assert.strictEqual(typeof outputInputs[makePath(text)].bytesInOutput, 'number') }, + + // Test in-memory output files + async writeFalse({ esbuild }) { + const input = path.join(testDir, 'writeFalse.js') + const output = path.join(testDir, 'writeFalse-out.js') + await util.promisify(fs.writeFile)(input, 'console.log()') + const value = await esbuild.build({ + entryPoints: [input], + bundle: true, + outfile: output, + sourcemap: true, + format: 'esm', + write: false, + }) + assert.strictEqual(await fs.existsSync(output), false) + assert.notStrictEqual(value.outputFiles, void 0) + assert.strictEqual(value.outputFiles.length, 2) + assert.strictEqual(value.outputFiles[0].path, output + '.map') + assert.strictEqual(value.outputFiles[0].contents.constructor, Uint8Array) + assert.strictEqual(value.outputFiles[1].path, output) + assert.strictEqual(value.outputFiles[1].contents.constructor, Uint8Array) + const sourceMap = JSON.parse(Buffer.from(value.outputFiles[0].contents).toString()) + const js = Buffer.from(value.outputFiles[1].contents).toString() + assert.strictEqual(sourceMap.version, 3) + assert.strictEqual(js, `// scripts/.js-api-tests/writeFalse.js\nconsole.log();\n//# sourceMappingURL=writeFalse-out.js.map\n`) + }, } async function futureSyntax(service, js, targetBelow, targetAbove) {