Skip to content

Commit

Permalink
chore!: rename to source-mapper (#35)
Browse files Browse the repository at this point in the history
* chore: rename to source-mapper

* chore: fix linter warnings

* chore: fix pkg

BREAKING CHANGE: import StackConverter is now import SourceMapper
  • Loading branch information
bobbyg603 authored Nov 13, 2023
1 parent d4b2c5d commit ce47f34
Show file tree
Hide file tree
Showing 10 changed files with 317 additions and 259 deletions.
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

❤️ [email protected]
```

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

0 comments on commit ce47f34

Please sign in to comment.