Skip to content

Commit

Permalink
fix #139: add "write: false" to javascript api
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Jul 5, 2020
1 parent 086e03b commit 268f833
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 22 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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))
Expand Down
53 changes: 42 additions & 11 deletions cmd/esbuild/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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) {
Expand Down
40 changes: 30 additions & 10 deletions lib/api-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand All @@ -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 {
Expand Down Expand Up @@ -198,16 +199,16 @@ 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;
}

// Dispatch the response
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);
};

Expand All @@ -220,21 +221,23 @@ 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);
});
},

transform(input, options, isTTY, callback) {
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) });
});
},
},
Expand Down Expand Up @@ -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;
}
7 changes: 7 additions & 0 deletions lib/api-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export interface BuildOptions extends CommonOptions {
external?: string[];
loader?: { [ext: string]: Loader };
resolveExtensions?: string[];
write?: boolean;

entryPoints: string[];
}
Expand All @@ -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 {
Expand Down
29 changes: 28 additions & 1 deletion scripts/js-api-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 268f833

Please sign in to comment.