From f7f1a3f5dcc086f1364610bb2db319da7dbb2cbc Mon Sep 17 00:00:00 2001 From: askarby Date: Fri, 21 Aug 2020 08:00:05 +0200 Subject: [PATCH] Modified to support path of map files, as well as additional parameters (mostly useful for debugging --- .gitignore | 105 +++++++++++++++++++++++++ .idea/modules.xml | 8 ++ .idea/stacktracify.iml | 12 +++ .idea/vcs.xml | 6 ++ README.md | 16 ++++ index.js | 171 +++++++++++++++++++++++++++++++++++++---- 6 files changed, 302 insertions(+), 16 deletions(-) create mode 100644 .idea/modules.xml create mode 100644 .idea/stacktracify.iml create mode 100644 .idea/vcs.xml diff --git a/.gitignore b/.gitignore index 6704566..9e6c6a6 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..5bcbb08 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/stacktracify.iml b/.idea/stacktracify.iml new file mode 100644 index 0000000..24643cc --- /dev/null +++ b/.idea/stacktracify.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 84e00d7..dcfffef 100644 --- a/README.md +++ b/README.md @@ -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? ``` diff --git a/index.js b/index.js index 1bf6720..1f31f0e 100644 --- a/index.js +++ b/index.js @@ -3,9 +3,114 @@ 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 @@ -13,58 +118,92 @@ const cli = meow(` 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); }