Skip to content

Solution for #4 #5

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
105 changes: 105 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,108 @@ dist

# TernJS port file
.tern-port

### WebStorm ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839

# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf

# Generated files
.idea/**/contentModel.xml

# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml

# Gradle
.idea/**/gradle.xml
.idea/**/libraries

# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr

# CMake
cmake-build-*/

# Mongo Explorer plugin
.idea/**/mongoSettings.xml

# File-based project format
*.iws

# IntelliJ
out/

# mpeltonen/sbt-idea plugin
.idea_modules/

# JIRA plugin
atlassian-ide-plugin.xml

# Cursive Clojure plugin
.idea/replstate.xml

# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties

# Editor-based Rest Client
.idea/httpRequests

# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser

### WebStorm Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721

# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr

# Sonarlint plugin
# https://plugins.jetbrains.com/plugin/7973-sonarlint
.idea/**/sonarlint/

# SonarQube Plugin
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
.idea/**/sonarIssues.xml

# Markdown Navigator plugin
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
.idea/**/markdown-navigator.xml
.idea/**/markdown-navigator-enh.xml
.idea/**/markdown-navigator/

# Cache file creation bug
# See https://youtrack.jetbrains.com/issue/JBR-2257
.idea/$CACHE_FILE$

# CodeStream plugin
# https://plugins.jetbrains.com/plugin/12206-codestream
.idea/codestream.xml

# End of https://www.toptal.com/developers/gitignore/api/webstorm
8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions .idea/stacktracify.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# stacktracify

**NOTICE:** This is a modified version of the excellent `stacktracify` CLI-tool, created by: [mifi/stacktracify](https://github.com/mifi/stacktracify)

This modified version allows for passing in a path to a folder of source maps (which was previously limited to a single file).

In addition to this, support for the following parameters have also been added:

| Parameter name | Abbreviation | Description |
|----------------|--------------|-----------------------------------------------------------------------------------------------------------|
| `--legend` | `-l` | Prints a legend, indicating when unable to not find a source map, or resolve line from a found source map |
| `--debug` | `-d` | Prints debug information, useful for determining lookup-logic for relative paths etc. |

**WARNING:** This version has not been made available to be installed on [npm](https://www.npmjs.com/), and hence must be installed
by cloning this repository, running `yarn install` and linking the index.js file as an executable script (or invoke directly)!

## Original documentation

Have you ever been faced with a stacktrace that looks like this?

```
Expand Down
171 changes: 155 additions & 16 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,68 +3,207 @@
const meow = require('meow');
const stackTraceParser = require('stacktrace-parser');
const fs = require('fs-extra');
const {basename, join, resolve} = require('path');
const {lstat} = require('fs').promises;
const clipboardy = require('clipboardy');
const { SourceMapConsumer } = require('source-map');

const WARNINGS = {
noPosition: '❓',
noSmc: '🌍'
};

function formatStackFrame(frame, decoration = null) {
const { file, methodName, lineNumber, column } = frame;
const parts = [];
if (decoration) {
parts.push(`[${decoration}] `);
}
parts.push('at ');
if (methodName) {
parts.push(methodName);
}
if (file) {
parts.push(' (');
parts.push(file);
if (lineNumber && column) {
parts.push(':');
parts.push(column);
parts.push(':');
parts.push(lineNumber);
}
parts.push(')');
}

return parts.join('');
}

class SourceMapRegistry {
// Map of "basename" -> "fullpath"
sourceMapFiles = new Map();
// Map of "basename" -> "source map consumer for source map file"
sourceMaps = new Map();

async getSourceMapConsumer(path) {
const key = basename(path) + '.map';
const fullPath = this.sourceMapFiles.get(key);

let smc = this.sourceMaps.get(key);
if (!smc && fullPath) {
// Acquire smc
const mapContent = JSON.parse(await fs.readFile(fullPath, 'utf-8'));
smc = await new SourceMapConsumer(mapContent);
this.sourceMaps.set(key, smc);
}
return smc;
}

async initialize(path) {
this.sourceMapFiles = new Map();
this.sourceMaps = new Map();

const stat = await fs.lstat(path);
if (stat.isFile()) {
this.sourceMapFiles.set(basename(path), path);
} else {
const found = await this.findFiles(path);
found.forEach(each => this.sourceMapFiles.set(basename(each), each));
}

if (debug) {
console.log('[DEBUG] Found the following files:');
for (var [key, value] of this.sourceMapFiles.entries()) {
console.log(`- ${value}`);
}
console.log('');
}
}

async findFiles(folder) {
const results = []

// Get all files from the folder
let items = await fs.readdir(folder);

// Loop through the results, possibly recurse
for (const item of items) {
try {
const fullPath = join(folder, item)

if (
fs.statSync(fullPath).isDirectory()) {

// Its a folder, recursively get the child folders' files
results.push(
...(await this.findFiles(fullPath))
)
} else {
// Filter by the file name pattern, if there is one
if (item.search(new RegExp('.*\.js\.map', 'i')) > -1) {
results.push(resolve(fullPath))
}
}
} catch (error) {
// Ignore!
}
}

return results
}
}

const cli = meow(`
Usage
$ stacktracify <map-path>

Options
--file, -f (default is read from clipboard)
--debug, -d (defaults to false)
--legend, -l (displays legend for parsing hints, eg ${Object.keys(WARNINGS).join(', ')} - disabled as default)

Examples
$ stacktracify /path/to/js.map --file /path/to/my-stacktrace.txt
$ stacktracify /path/to/source-maps --file /path/to/my-stacktrace.txt --debug --legend
`, {
flags: {
file: {
type: 'string',
alias: 'f',
},
debug: {
type: 'boolean',
alias: 'd',
},
legend: {
type: 'boolean',
alias: 'l',
},
},
});


const { file } = cli.flags;
var { file, debug, legend } = cli.flags;

(async () => {
try {
// Determine path of source maps
const mapPath = cli.input[0];
if (!mapPath) cli.showHelp();
const mapContent = JSON.parse(await fs.readFile(mapPath, 'utf-8'));
// WTF? promise?
const smc = await new SourceMapConsumer(mapContent);

// Create registry
const registry = new SourceMapRegistry();
await registry.initialize(mapPath);

// Acquire stacktrace
let str;
if (file !== undefined) {
str = await fs.readFile(file, 'utf-8');
} else {
str = await clipboardy.read();
}

// Parse stacktrace
const stack = stackTraceParser.parse(str);
if (stack.length === 0) throw new Error('No stack found');

// Print "header" (usually message of what went wrong, eg. message of Error)
const header = str.split('\n').find(line => line.trim().length > 0);

if (header) console.log(header);

stack.forEach(({ methodName, lineNumber, column }) => {
if (header && !header.includes(stack[0].file)) {
console.log(header);
}

// Translate stacktrace
const warnings = [];
for (const each of stack) {
const { file, methodName, lineNumber, column } = each;
try {
if (lineNumber == null || lineNumber < 1) {
console.log(` at ${methodName || ''}`);
} else {
const pos = smc.originalPositionFor({ line: lineNumber, column });
if (pos && pos.line != null) {
console.log(` at ${pos.name || ''} (${pos.source}:${pos.line}:${pos.column})`);
const smc = await registry.getSourceMapConsumer(file);
if (smc && typeof smc.originalPositionFor === 'function') {
const pos = smc && smc.originalPositionFor({ line: lineNumber, column }) || undefined;
if (pos && pos.line != null) {
console.log(` at ${pos.name || ''} (${pos.source}:${pos.line}:${pos.column})`);
} else {
console.log(` ${formatStackFrame(each, legend && WARNINGS.noPosition)}`);
warnings.push(WARNINGS.noPosition);
}
} else {
console.log(` ${formatStackFrame(each, legend && WARNINGS.noSmc)}`);
warnings.push(WARNINGS.noSmc);
}

// console.log('src', smc.sourceContentFor(pos.source));

}
} catch (err) {
console.log(` at FAILED_TO_PARSE_LINE`);
console.log(` at FAILED_TO_PARSE_LINE`, err);
}
});
}

if (warnings.length > 0) {
console.log('\nLegend:\n-------');
console.log(`[${WARNINGS.noPosition}] -> Indicates that a the particular stack frame could not be located in the source map`);
console.log(`[${WARNINGS.noSmc}] -> Indicates that a source map could not be located for the stack frame`);
}
} catch (err) {
console.error(err);
}
Expand Down