Skip to content

Commit

Permalink
fix #1058: add the "formatMessages" api
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Mar 25, 2021
1 parent fc325d0 commit 5cf85d5
Show file tree
Hide file tree
Showing 11 changed files with 252 additions and 15 deletions.
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,29 @@

Previously the build results returned to the watch mode `onRebuild` callback was missing the `metafile` property when the `metafile: true` option was present. This bug has been fixed.

* Add a `formatMessages` API ([#1058](https://github.com/evanw/esbuild/issues/1058))

This API lets you print log messages to the terminal using the same log format that esbuild itself uses. This can be used to filter esbuild's warnings while still making the output look the same. Here's an example of calling this API:

This comment has been minimized.

Copy link
@lukeed

lukeed Mar 25, 2021

Contributor

Might be worth saying "warnings and/or errors" to avoid thinking it's only for warnings.


```js
import esbuild from 'esbuild'

const formatted = await esbuild.formatMessages([{
text: '"test" has already been declared',
location: { file: 'file.js', line: 2, column: 4, length: 4, lineText: 'let test = "second"' },
notes: [{
text: '"test" was originally declared here',
location: { file: 'file.js', line: 1, column: 4, length: 4, lineText: 'let test = "first"' },
}],
}], {
kind: 'error',
color: true,
terminalWidth: 100,
})

process.stdout.write(formatted.join(''))
```

## 0.10.0

**This release contains backwards-incompatible changes.** Since esbuild is before version 1.0.0, these changes have been released as a new minor version to reflect this (as [recommended by npm](https://docs.npmjs.com/cli/v6/using-npm/semver/)). You should either be pinning the exact version of `esbuild` in your `package.json` file or be using a version range syntax that only accepts patch upgrades such as `~0.9.0`. See the documentation about [semver](https://docs.npmjs.com/cli/v6/using-npm/semver/) for more information.
Expand Down
39 changes: 39 additions & 0 deletions cmd/esbuild/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,11 @@ func (service *serviceType) handleIncomingPacket(bytes []byte) (result outgoingP
}),
}

case "format-msgs":
return outgoingPacket{
bytes: service.handleFormatMessagesRequest(p.id, request),
}

default:
return outgoingPacket{
bytes: encodePacket(packet{
Expand Down Expand Up @@ -838,6 +843,40 @@ func (service *serviceType) handleTransformRequest(id uint32, request map[string
})
}

func (service *serviceType) handleFormatMessagesRequest(id uint32, request map[string]interface{}) []byte {
msgs := decodeMessages(request["messages"].([]interface{}))

options := api.FormatMessagesOptions{
Kind: api.ErrorMessage,
}
if request["isWarning"].(bool) {
options.Kind = api.WarningMessage
}
if value, ok := request["color"].(bool); ok {
options.Color = value
}
if value, ok := request["terminalWidth"].(int); ok {
options.TerminalWidth = value
}

result := api.FormatMessages(msgs, options)

return encodePacket(packet{
id: id,
value: map[string]interface{}{
"messages": encodeStringArray(result),
},
})
}

func encodeStringArray(strings []string) []interface{} {
values := make([]interface{}, len(strings))
for i, value := range strings {
values[i] = value
}
return values
}

func decodeStringArray(values []interface{}) []string {
strings := make([]string, len(values))
for i, value := range values {
Expand Down
12 changes: 6 additions & 6 deletions internal/logger/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -831,7 +831,7 @@ func (msg Msg) String(options OutputOptions, terminalInfo TerminalInfo) string {
}

// Format the message
text := msgString(options, terminalInfo, msg.Kind, msg.Data, maxMargin)
text := msgString(options.IncludeSource, terminalInfo, msg.Kind, msg.Data, maxMargin)

// Put a blank line between the message and the notes if the message has a stack trace
gap := ""
Expand All @@ -842,7 +842,7 @@ func (msg Msg) String(options OutputOptions, terminalInfo TerminalInfo) string {
// Format the notes
for _, note := range msg.Notes {
text += gap
text += msgString(options, terminalInfo, Note, note, maxMargin)
text += msgString(options.IncludeSource, terminalInfo, Note, note, maxMargin)
}

// Add extra spacing between messages if source code is present
Expand All @@ -868,7 +868,7 @@ func emptyMarginText(maxMargin int, isLast bool) string {
return fmt.Sprintf(" %s │ ", space)
}

func msgString(options OutputOptions, terminalInfo TerminalInfo, kind MsgKind, data MsgData, maxMargin int) string {
func msgString(includeSource bool, terminalInfo TerminalInfo, kind MsgKind, data MsgData, maxMargin int) string {
var colors Colors
if terminalInfo.UseColorEscapes {
colors = terminalColors
Expand All @@ -879,7 +879,7 @@ func msgString(options OutputOptions, terminalInfo TerminalInfo, kind MsgKind, d
messageColor := colors.Bold
textIndent := ""

if options.IncludeSource {
if includeSource {
textIndent = " > "
}

Expand All @@ -894,7 +894,7 @@ func msgString(options OutputOptions, terminalInfo TerminalInfo, kind MsgKind, d
prefixColor = colors.Reset
kindColor = colors.Bold
messageColor = ""
if options.IncludeSource {
if includeSource {
textIndent = " "
}

Expand All @@ -909,7 +909,7 @@ func msgString(options OutputOptions, terminalInfo TerminalInfo, kind MsgKind, d
colors.Reset)
}

if !options.IncludeSource {
if !includeSource {
return fmt.Sprintf("%s%s%s: %s%s: %s%s%s\n%s",
prefixColor, textIndent, data.Location.File,
kindColor, kind.String(),
Expand Down
19 changes: 15 additions & 4 deletions lib/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export const serve: typeof types.serve = () => {
export const transform: typeof types.transform = (input, options) =>
ensureServiceIsRunning().transform(input, options);

export const formatMessages: typeof types.formatMessages = (messages, options) =>
ensureServiceIsRunning().formatMessages(messages, options);

export const buildSync: typeof types.buildSync = () => {
throw new Error(`The "buildSync" API only works in node`);
};
Expand All @@ -24,9 +27,14 @@ export const transformSync: typeof types.transformSync = () => {
throw new Error(`The "transformSync" API only works in node`);
};

export const formatMessagesSync: typeof types.formatMessagesSync = () => {
throw new Error(`The "formatMessagesSync" API only works in node`);
};

interface Service {
build: typeof types.build;
transform: typeof types.transform;
formatMessages: typeof types.formatMessages;
}

let initializePromise: Promise<void> | undefined;
Expand Down Expand Up @@ -103,12 +111,15 @@ const startRunningService = async (wasmURL: string, useWorker: boolean): Promise
new Promise<types.BuildResult>((resolve, reject) =>
service.buildOrServe('build', null, null, options, false, '/', (err, res) =>
err ? reject(err) : resolve(res as types.BuildResult))),
transform: (input, options) => {
return new Promise((resolve, reject) =>
transform: (input, options) =>
new Promise((resolve, reject) =>
service.transform('transform', null, input, options || {}, false, {
readFile(_, callback) { callback(new Error('Internal error'), null); },
writeFile(_, callback) { callback(null); },
}, (err, res) => err ? reject(err) : resolve(res!)))
},
}, (err, res) => err ? reject(err) : resolve(res!))),
formatMessages: (messages, options) =>
new Promise((resolve, reject) =>
service.formatMessages('formatMessages', null, messages, options, (err, res) =>
err ? reject(err) : resolve(res!))),
}
}
35 changes: 33 additions & 2 deletions lib/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,14 @@ export interface StreamService {
fs: StreamFS,
callback: (err: Error | null, res: types.TransformResult | null) => void,
): void;

formatMessages(
callName: string,
refs: Refs | null,
messages: types.PartialMessage[],
options: types.FormatMessagesOptions,
callback: (err: Error | null, res: string[] | null) => void,
): void;
}

// This can't use any promises in the main execution flow because it must work
Expand Down Expand Up @@ -1066,6 +1074,29 @@ export function createChannel(streamIn: StreamIn): StreamOut {
}
start(null);
},

formatMessages(callName, refs, messages, options, callback) {
let result = sanitizeMessages(messages, 'messages', null);
if (!options) throw new Error(`Missing second argument in ${callName}() call`);
let keys: OptionKeys = {};
let kind = getFlag(options, keys, 'kind', mustBeString);
let color = getFlag(options, keys, 'color', mustBeBoolean);
let terminalWidth = getFlag(options, keys, 'terminalWidth', mustBeInteger);
checkForInvalidFlags(options, keys, `in ${callName}() call`);
if (kind === void 0) throw new Error(`Missing "kind" in ${callName}() call`);
if (kind !== 'error' && kind !== 'warning') throw new Error(`Expected "kind" to be "error" or "warning" in ${callName}() call`);
let request: protocol.FormatMsgsRequest = {
command: 'format-msgs',
messages: result,
isWarning: kind === 'warning',
}
if (color !== void 0) request.color = color;
if (terminalWidth !== void 0) request.terminalWidth = terminalWidth;
sendRequest<protocol.FormatMsgsRequest, protocol.FormatMsgsResponse>(refs, request, (error, response) => {
if (error) return callback(new Error(error), null);
callback(null, response!.messages);
});
},
},
};
}
Expand Down Expand Up @@ -1233,7 +1264,7 @@ function sanitizeLocation(location: types.PartialMessage['location'], where: str
};
}

function sanitizeMessages(messages: types.PartialMessage[], property: string, stash: ObjectStash): types.Message[] {
function sanitizeMessages(messages: types.PartialMessage[], property: string, stash: ObjectStash | null): types.Message[] {
let messagesClone: types.Message[] = [];
let index = 0;

Expand Down Expand Up @@ -1264,7 +1295,7 @@ function sanitizeMessages(messages: types.PartialMessage[], property: string, st
text: text || '',
location: sanitizeLocation(location, where),
notes: notesClone,
detail: stash.store(detail),
detail: stash ? stash.store(detail) : -1,
});
index++;
}
Expand Down
32 changes: 31 additions & 1 deletion lib/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ export let serve: typeof types.serve = (serveOptions, buildOptions) =>
export let transform: typeof types.transform = (input, options) =>
ensureServiceIsRunning().transform(input, options);

export let formatMessages: typeof types.formatMessages = (messages, options) =>
ensureServiceIsRunning().formatMessages(messages, options);

export let buildSync: typeof types.buildSync = (options: types.BuildOptions): any => {
// Try using a long-lived worker thread to avoid repeated start-up overhead
if (worker_threads) {
Expand Down Expand Up @@ -136,6 +139,21 @@ export let transformSync: typeof types.transformSync = (input, options) => {
return result!;
};

export let formatMessagesSync: typeof types.formatMessagesSync = (messages, options) => {
// Try using a long-lived worker thread to avoid repeated start-up overhead
if (worker_threads) {
if (!workerThreadService) workerThreadService = startWorkerThreadService(worker_threads);
return workerThreadService.formatMessagesSync(messages, options);
}

let result: string[];
runServiceSync(service => service.formatMessages('formatMessagesSync', null, messages, options, (err, res) => {
if (err) throw err;
result = res!;
}));
return result!;
};

let initializeWasCalled = false;

export let initialize: typeof types.initialize = options => {
Expand All @@ -152,6 +170,7 @@ interface Service {
build: typeof types.build;
serve: typeof types.serve;
transform: typeof types.transform;
formatMessages: typeof types.formatMessages;
}

let defaultWD = process.cwd();
Expand Down Expand Up @@ -246,6 +265,11 @@ let ensureServiceIsRunning = (): Service => {
},
}, (err, res) => err ? reject(err) : resolve(res!)));
},
formatMessages: (messages, options) => {
return new Promise((resolve, reject) =>
service.formatMessages('formatMessages', refs, messages, options, (err, res) =>
err ? reject(err) : resolve(res!)));
},
};
return longLivedService;
}
Expand Down Expand Up @@ -290,7 +314,8 @@ interface MainToWorkerMessage {

interface WorkerThreadService {
buildSync(options: types.BuildOptions): types.BuildResult;
transformSync(input: string, options?: types.TransformOptions): types.TransformResult;
transformSync: typeof types.transformSync;
formatMessagesSync: typeof types.formatMessagesSync;
}

let workerThreadService: WorkerThreadService | null = null;
Expand Down Expand Up @@ -379,6 +404,9 @@ let startWorkerThreadService = (worker_threads: typeof import('worker_threads'))
transformSync(input, options) {
return runCallSync('transform', [input, options]);
},
formatMessagesSync(messages, options) {
return runCallSync('formatMessages', [messages, options]);
},
};
};

Expand Down Expand Up @@ -415,6 +443,8 @@ let startSyncServiceWorker = () => {
workerPort.postMessage({ id, resolve: await service.build(args[0]) });
} else if (command === 'transform') {
workerPort.postMessage({ id, resolve: await service.transform(args[0], args[1]) });
} else if (command === 'formatMessages') {
workerPort.postMessage({ id, resolve: await service.formatMessages(args[0], args[1]) });
} else {
throw new Error(`Invalid command: ${command}`);
}
Expand Down
12 changes: 12 additions & 0 deletions lib/stdio_protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,18 @@ export interface TransformResponse {
mapFS: boolean;
}

export interface FormatMsgsRequest {
command: 'format-msgs';
messages: types.Message[];
isWarning: boolean;
color?: boolean;
terminalWidth?: number;
}

export interface FormatMsgsResponse {
messages: string[];
}

export interface OnResolveRequest {
command: 'resolve';
key: number;
Expand Down
24 changes: 22 additions & 2 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,12 +299,18 @@ export interface Metafile {
}
}

export interface FormatMessagesOptions {
kind: 'error' | 'warning';
color?: boolean;
terminalWidth?: number;
}

// This function invokes the "esbuild" command-line tool for you. It returns a
// promise that either resolves with a "BuildResult" object or rejects with a
// "BuildFailure" object.
//
// Works in node: yes
// Works in browser: no
// Works in browser: yes
export declare function build(options: BuildOptions & { write: false }): Promise<BuildResult & { outputFiles: OutputFile[] }>;
export declare function build(options: BuildOptions & { incremental: true }): Promise<BuildIncremental>;
export declare function build(options: BuildOptions): Promise<BuildResult>;
Expand All @@ -322,9 +328,17 @@ export declare function serve(serveOptions: ServeOptions, buildOptions: BuildOpt
// "TransformResult" object or rejected with a "TransformFailure" object.
//
// Works in node: yes
// Works in browser: no
// Works in browser: yes
export declare function transform(input: string, options?: TransformOptions): Promise<TransformResult>;

// Converts log messages to formatted message strings suitable for printing in
// the terminal. This allows you to reuse the built-in behavior of esbuild's
// log message formatter. This is a batch-oriented API for efficiency.
//
// Works in node: yes
// Works in browser: yes
export declare function formatMessages(messages: PartialMessage[], options: FormatMessagesOptions): Promise<string[]>;

// A synchronous version of "build".
//
// Works in node: yes
Expand All @@ -338,6 +352,12 @@ export declare function buildSync(options: BuildOptions): BuildResult;
// Works in browser: no
export declare function transformSync(input: string, options?: TransformOptions): TransformResult;

// A synchronous version of "formatMessages".
//
// Works in node: yes
// Works in browser: no
export declare function formatMessagesSync(messages: PartialMessage[], options: FormatMessagesOptions): string[];

// This configures the browser-based version of esbuild. It is necessary to
// call this first and wait for the returned promise to be resolved before
// making other API calls when using esbuild in the browser.
Expand Down
Loading

0 comments on commit 5cf85d5

Please sign in to comment.