Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 23 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@
</a>
</div>

# 🥞 stack-converter
`stack-converter` is a utility for translating function names, file names and line numbers in uglified JavaScript Error stack frames to the corresponding values in the original source. `stack-converter` is distributed as both a package and a library and is used by the [BugSplat](https://www.bugsplat.com) backend to deliver crash reporting as a service for JavaScript and TypeScript applications.
# 🗺️ source-mapper
`source-mapper` is a utility for translating function names, file names and line numbers in uglified JavaScript Error stack frames to the corresponding values in the original source. `source-mapper` is distributed as both a package and a library and is used by the [BugSplat](https://www.bugsplat.com) backend to deliver crash reporting as a service for JavaScript and TypeScript applications.

The following is an example JavaScript Error stack converted to its TypeScript equivalent using `stack-converter`:
The following is an example JavaScript Error stack converted to its TypeScript equivalent using `source-mapper`:

```
Error: BugSplat rocks!
at crash (/Users/bobby/Desktop/bugsplat/stack-converter/dist/bin/cmd.js:16:11)
at /Users/bobby/Desktop/bugsplat/stack-converter/dist/bin/cmd.js:6:9
at Object.<anonymous> (/Users/bobby/Desktop/bugsplat/stack-converter/dist/bin/cmd.js:14:3)
at crash (/Users/bobby/Desktop/bugsplat/source-mapper/dist/bin/cmd.js:16:11)
at /Users/bobby/Desktop/bugsplat/source-mapper/dist/bin/cmd.js:6:9
at Object.<anonymous> (/Users/bobby/Desktop/bugsplat/source-mapper/dist/bin/cmd.js:14:3)
```

```
Expand All @@ -31,49 +31,54 @@ Error: BugSplat rocks!
```

## 🖥 Command Line
<<<<<<< HEAD

1. Install this package globally `npm i -g @bugsplat/stack-converter`
2. Run `stack-converter -h` to see the latest usage information:

=======
1. Install this package globally `npm i -g @bugsplat/source-mapper`
2. Run `source-mapper -h` to see the latest usage information:
>>>>>>> 0431171 (chore: rename to source-mapper)
```bash
bobby@BugSplat % ~ % stack-converter -h
bobby@BugSplat % ~ % source-mapper -h

@bugsplat/stack-converter contains a command line utility and set of libraries to help you demangle JavaScript stack frames.
@bugsplat/source-mapper contains a command line utility and set of libraries to help you demangle JavaScript stack frames.

stack-converter command line usage:
source-mapper command line usage:

stack-converter [ [ "/source-map-directory" OR "/source.js.map" ] [ "/stack-trace.txt" ] ]
source-mapper [ [ "/source-map-directory" OR "/source.js.map" ] [ "/stack-trace.txt" ] ]

* Optionally provide either a path to a directory containing source maps or a .map.js file - Defaults to the current directory
* Optionally provide a path to a .txt file containing a JavaScript Error stack trace - Defaults to the value in the clipboard

❤️ support@bugsplat.com
```

3. Run `stack-converter` and optionally specify a path to a directory containing .js.map files, path to a single .js.map file, and a path to a .txt file containing a stringified JavaScript Error. If no options are provided `stack-converter` will default to looking in the current directory for source maps and attempt to read the stringified JavaScript error stack from the system clipboard.
3. Run `source-mapper` and optionally specify a path to a directory containing .js.map files, path to a single .js.map file, and a path to a .txt file containing a stringified JavaScript Error. If no options are provided `source-mapper` will default to looking in the current directory for source maps and attempt to read the stringified JavaScript error stack from the system clipboard.

## 🧩 API

1. Install this package locally `npm i @bugsplat/stack-converter`
2. Import `StackConverter` from `@bugsplat/stack-converter`
1. Install this package locally `npm i @bugsplat/source-mapper`
2. Import `SourceMapper` from `@bugsplat/source-mapper`

```ts
import { StackConverter } from '@bugsplat/stack-converter';
import { SourceMapper } from '@bugsplat/source-mapper';
```

3. Create a new instance of `StackConverter` passing it an array of paths to source map files. You can also await the static factory function `createFromDirectory(directory: string): Promise<StackConverter>` which takes a path to a directory and creates a new StackConverter with an array of source map file paths it finds in the specified directory
3. Create a new instance of `SourceMapper` passing it an array of paths to source map files. You can also await the static factory function `createFromDirectory(directory: string): Promise<SourceMapper>` which takes a path to a directory and creates a new SourceMapper with an array of source map file paths it finds in the specified directory

```ts
const converter = new StackConverter(sourceMapFilePaths);
const mapper = new SourceMapper(sourceMapFilePaths);
```

```ts
const converter = await StackConverter.createFromDirectory(directory);
const mapper = await SourceMapper.createFromDirectory(directory);
```

4. Await the call to convert passing it the stack property from a JavaScript Error object
```ts
const result = await converter.convert(error.stack);
const result = await mapper.convert(error.stack);
```

Thanks for using BugSplat!
10 changes: 5 additions & 5 deletions bin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
import clipboard from 'clipboardy';
import { Stats } from 'fs';
import * as fs from 'fs/promises';
import { StackConverter } from '../lib/stack-converter';
import { SourceMapper } from '../lib/source-mapper';

const helpAndExit = () => {
const help = `
@bugsplat/stack-converter contains a command line utility and set of libraries to help you demangle JavaScript stack frames.
@bugsplat/source-mapper contains a command line utility and set of libraries to help you demangle JavaScript stack frames.

stack-converter command line usage:
source-mapper command line usage:

stack-converter [ [ "/source-map-directory" OR "/source.js.map" ] [ "/stack-trace.txt" ] ]
source-mapper [ [ "/source-map-directory" OR "/source.js.map" ] [ "/stack-trace.txt" ] ]

* Optionally provide either a path to a directory containing source maps or a .map.js file - Defaults to the current directory
* Optionally provide a path to a .txt file containing a JavaScript Error stack trace - Defaults to the value in the clipboard
Expand Down Expand Up @@ -67,7 +67,7 @@ const helpAndExit = () => {
throw new Error('Stack contents are empty');
}

const converter = sourceMapStat.isDirectory() ? await StackConverter.createFromDirectory(sourceMapPath) : new StackConverter([sourceMapPath]);
const converter = sourceMapStat.isDirectory() ? await SourceMapper.createFromDirectory(sourceMapPath) : new SourceMapper([sourceMapPath]);
const { error, stack } = await converter.convert(stackFileContents);
if (error) {
throw new Error(error);
Expand Down
2 changes: 1 addition & 1 deletion lib/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { StackConverter } from './stack-converter';
export { SourceMapper as SourceMapper } from './source-mapper';
52 changes: 26 additions & 26 deletions lib/stack-converter.ts → lib/source-mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,44 +7,44 @@ import * as stackTraceParser from 'stacktrace-parser';
/**
* A class for converting stacktraces, mangled by transpiling, back to match the originating files.
* Usage:
* const converter = new StackConverter(['a-sourceMap-file.js.sourceMap']);
* const converter = new SourceMapper(['a-sourceMap-file.js.sourceMap']);
* const mangledStack = '....';
* const newStack = await converter.convert(managedStack);
*/
export class StackConverter {
export class SourceMapper {
private static readonly INDENT: string = ' ';

/**
* Create a StackConverter by passing an array of paths to source map files
* Create a SourceMapper by passing an array of paths to source map files
*
* @param sourceMapFilePaths - an array of paths to source map files for converting stacks
* @throws - throws if no sourceMapFilePaths are provided
*/
constructor(private sourceMapFilePaths: Array<string>) {
if (!sourceMapFilePaths?.length) {
throw new Error('Could not create StackConverter: no source map file paths were provided!');
throw new Error('Could not create SourceMapper: no source map file paths were provided!');
}
}

/**
* Convenience method for creating a StackConverter from a directory containing source map files
* Convenience method for creating a SourceMapper from a directory containing source map files
*
* @param directory - path to directory containing source map files
* @throws - throws if no source map files exist in dir
* @returns - promise that resolves to a new StackConverter
* @returns - promise that resolves to a new SourceMapper
*/
static async createFromDirectory(directory: string): Promise<StackConverter> {
static async createFromDirectory(directory: string): Promise<SourceMapper> {
try {
await fs.lstat(directory);
} catch(error) {
throw new Error(`Could not create StackConverter: ${directory} does not exist or is inaccessible!`);
throw new Error(`Could not create SourceMapper: ${directory} does not exist or is inaccessible!`);
}

const files = await fs.readdir(directory);
const sourceMapFilePaths = files
.filter(file => file.endsWith('.map'))
.map(file => path.join(directory, file));
return new StackConverter(sourceMapFilePaths);
return new SourceMapper(sourceMapFilePaths);
}

/**
Expand All @@ -67,22 +67,22 @@ export class StackConverter {
const sourceMaps: { [filename: string]: SourceMapConsumer } = {};
const sourceMapErrors: { [filename: string]: boolean } = {};

const errorLine = StackConverter.getChromiumErrorLineOrEmpty(stack);
const errorLine = SourceMapper.getChromiumErrorLineOrEmpty(stack);
if (errorLine) {
buff.push(errorLine);
}

for (const frame of stackFrames) {
const { file, methodName, lineNumber, column } = frame;
if (file in sourceMapErrors) {
const comment = StackConverter.errorLoadingSourceMapComment('previous error');
buff.push(StackConverter.frameLine(methodName, file, lineNumber, column, comment));
const comment = SourceMapper.errorLoadingSourceMapComment('previous error');
buff.push(SourceMapper.frameLine(methodName, file, lineNumber, column, comment));
continue;
}

if (!lineNumber && !column) {
// handle case where <anonymous> is returned as file with no lineNumber or column
buff.push(StackConverter.frameLine(methodName, file, lineNumber, column));
buff.push(SourceMapper.frameLine(methodName, file, lineNumber, column));
continue;
}

Expand All @@ -94,25 +94,25 @@ export class StackConverter {

if (!mapFile) {
set(sourceMapErrors, file, true);
const comment = StackConverter.errorLoadingSourceMapComment('source map not found');
buff.push(StackConverter.frameLine(methodName, file, lineNumber, column, comment));
const comment = SourceMapper.errorLoadingSourceMapComment('source map not found');
buff.push(SourceMapper.frameLine(methodName, file, lineNumber, column, comment));
continue;
}

try {
if (!lineNumber || (lineNumber < 1)) {
const funcName = methodName || '';
buff.push(`${StackConverter.INDENT}at ${funcName}`);
buff.push(`${SourceMapper.INDENT}at ${funcName}`);
continue;
}

let sourceMapConsumer: SourceMapConsumer = get(sourceMaps, mapFile);
if (!sourceMapConsumer) {
const { sourceMap, error } = await StackConverter.sourceMapFromFile(mapFile);
const { sourceMap, error } = await SourceMapper.sourceMapFromFile(mapFile);
if (!sourceMap || error) {
set(sourceMapErrors, file, true);
const comment = StackConverter.errorLoadingSourceMapComment(error);
buff.push(StackConverter.frameLine(methodName, file, lineNumber, column, comment));
const comment = SourceMapper.errorLoadingSourceMapComment(error);
buff.push(SourceMapper.frameLine(methodName, file, lineNumber, column, comment));
continue;
}
sourceMaps[mapFile] = sourceMap;
Expand All @@ -121,15 +121,15 @@ export class StackConverter {

const originalPosition = sourceMapConsumer.originalPositionFor({ line: lineNumber, column });
if (!originalPosition || !originalPosition.line) {
const comment = StackConverter.couldNotConvertStackFrameComment('original position not found');
buff.push(StackConverter.frameLine(methodName, file, lineNumber, column, comment));
const comment = SourceMapper.couldNotConvertStackFrameComment('original position not found');
buff.push(SourceMapper.frameLine(methodName, file, lineNumber, column, comment));
continue;
}
const name = originalPosition.name || methodName;
buff.push(StackConverter.frameLine(name, originalPosition.source, originalPosition.line, originalPosition.column));
buff.push(SourceMapper.frameLine(name, originalPosition.source, originalPosition.line, originalPosition.column));
} catch (error) {
const comment = StackConverter.couldNotConvertStackFrameComment((error as Error).message);
buff.push(StackConverter.frameLine(methodName, file, lineNumber, column, comment));
const comment = SourceMapper.couldNotConvertStackFrameComment((error as Error).message);
buff.push(SourceMapper.frameLine(methodName, file, lineNumber, column, comment));
}
}
return { stack: buff.join('\n') };
Expand All @@ -146,10 +146,10 @@ export class StackConverter {
private static frameLine(methodName: string, file: string, line: number, column: number, comment?: string): string {
const method = methodName || '<unknown>';
if (!line && !column) {
return `${StackConverter.INDENT}at ${method} (${file})` + (comment ? ' ***' + comment : '');
return `${SourceMapper.INDENT}at ${method} (${file})` + (comment ? ' ***' + comment : '');
}

return `${StackConverter.INDENT}at ${method} (${file}:${line}:${column})` + (comment ? ' ***' + comment : '');
return `${SourceMapper.INDENT}at ${method} (${file}:${line}:${column})` + (comment ? ' ***' + comment : '');
}

private static getChromiumErrorLineOrEmpty(stack: string): string {
Expand Down
Loading