123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339 |
- "use strict";
- /* ------------------------------------------------------------------------ */
- const O = Object,
- isBrowser = (typeof window !== 'undefined') && (window.window === window) && window.navigator,
- nodeRequire = isBrowser ? null : module.require, // to prevent bundlers from expanding the require call
- lastOf = x => x[x.length - 1],
- getSource = require ('get-source'),
- partition = require ('./impl/partition'),
- asTable = require ('as-table'),
- nixSlashes = x => x.replace (/\\/g, '/'),
- pathRoot = isBrowser ? window.location.href : (nixSlashes (process.cwd ()) + '/')
- /* ------------------------------------------------------------------------ */
- class StackTracey {
- constructor (input, offset) {
-
- const originalInput = input
- , isParseableSyntaxError = input && (input instanceof SyntaxError && !isBrowser)
-
- /* new StackTracey () */
- if (!input) {
- input = new Error ()
- offset = (offset === undefined) ? 1 : offset
- }
- /* new StackTracey (Error) */
- if (input instanceof Error) {
- input = input.stack || ''
- }
- /* new StackTracey (string) */
- if (typeof input === 'string') {
- input = this.rawParse (input).slice (offset).map (x => this.extractEntryMetadata (x))
- }
- /* new StackTracey (array) */
- if (Array.isArray (input)) {
- if (isParseableSyntaxError) {
-
- const rawLines = nodeRequire ('util').inspect (originalInput).split ('\n')
- , fileLine = rawLines[0].split (':')
- , line = fileLine.pop ()
- , file = fileLine.join (':')
- if (file) {
- input.unshift ({
- file: nixSlashes (file),
- line: line,
- column: (rawLines[2] || '').indexOf ('^') + 1,
- sourceLine: rawLines[1],
- callee: '(syntax error)',
- syntaxError: true
- })
- }
- }
- this.items = input
- } else {
- this.items = []
- }
- }
- extractEntryMetadata (e) {
- const decomposedPath = this.decomposePath (e.file || '')
- const fileRelative = decomposedPath[0]
- const externalDomain = decomposedPath[1]
- return O.assign (e, {
- calleeShort: e.calleeShort || lastOf ((e.callee || '').split ('.')),
- fileRelative: fileRelative,
- fileShort: this.shortenPath (fileRelative),
- fileName: lastOf ((e.file || '').split ('/')),
- thirdParty: this.isThirdParty (fileRelative, externalDomain) && !e.index,
- externalDomain: externalDomain
- })
- }
- shortenPath (relativePath) {
- return relativePath.replace (/^node_modules\//, '')
- .replace (/^webpack\/bootstrap\//, '')
- .replace (/^__parcel_source_root\//, '')
- }
- decomposePath (fullPath) {
- let result = fullPath
-
- if (isBrowser) result = result.replace (pathRoot, '')
- const externalDomainMatch = result.match (/^(http|https)\:\/\/?([^\/]+)\/(.*)/)
- const externalDomain = externalDomainMatch ? externalDomainMatch[2] : undefined
- result = externalDomainMatch ? externalDomainMatch[3] : result
- if (!isBrowser) result = nodeRequire ('path').relative (pathRoot, result)
- return [
- nixSlashes(result).replace (/^.*\:\/\/?\/?/, ''), // cut webpack:/// and webpack:/ things
- externalDomain
- ]
- }
- isThirdParty (relativePath, externalDomain) {
- return externalDomain ||
- (relativePath[0] === '~') || // webpack-specific heuristic
- (relativePath[0] === '/') || // external source
- (relativePath.indexOf ('node_modules') === 0) ||
- (relativePath.indexOf ('webpack/bootstrap') === 0)
- }
- rawParse (str) {
- const lines = (str || '').split ('\n')
- const entries = lines.map (line => {
- line = line.trim ()
- let callee, fileLineColumn = [], native, planA, planB
- if ((planA = line.match (/at (.+) \(eval at .+ \((.+)\), .+\)/)) || // eval calls
- (planA = line.match (/at (.+) \((.+)\)/)) ||
- ((line.slice (0, 3) !== 'at ') && (planA = line.match (/(.*)@(.*)/)))) {
- callee = planA[1]
- native = (planA[2] === 'native')
- fileLineColumn = (planA[2].match (/(.*):(\d+):(\d+)/) ||
- planA[2].match (/(.*):(\d+)/) || []).slice (1)
- } else if ((planB = line.match (/^(at\s+)*(.+):(\d+):(\d+)/) )) {
- fileLineColumn = (planB).slice (2)
- } else {
- return undefined
- }
- /* Detect things like Array.reduce
- TODO: detect more built-in types */
-
- if (callee && !fileLineColumn[0]) {
- const type = callee.split ('.')[0]
- if (type === 'Array') {
- native = true
- }
- }
- return {
- beforeParse: line,
- callee: callee || '',
- index: isBrowser && (fileLineColumn[0] === window.location.href),
- native: native || false,
- file: nixSlashes (fileLineColumn[0] || ''),
- line: parseInt (fileLineColumn[1] || '', 10) || undefined,
- column: parseInt (fileLineColumn[2] || '', 10) || undefined
- }
- })
- return entries.filter (x => (x !== undefined))
- }
- withSourceAt (i) {
- return this.items[i] && this.withSource (this.items[i])
- }
- withSourceAsyncAt (i) {
- return this.items[i] && this.withSourceAsync (this.items[i])
- }
- withSource (loc) {
-
- if (this.shouldSkipResolving (loc)) {
- return loc
-
- } else {
- let resolved = getSource (loc.file || '').resolve (loc)
- if (!resolved.sourceFile) {
- return loc
- }
- return this.withSourceResolved (loc, resolved)
- }
- }
- withSourceAsync (loc) {
- if (this.shouldSkipResolving (loc)) {
- return Promise.resolve (loc)
-
- } else {
- return getSource.async (loc.file || '')
- .then (x => x.resolve (loc))
- .then (resolved => this.withSourceResolved (loc, resolved))
- .catch (e => this.withSourceResolved (loc, { error: e, sourceLine: '' }))
- }
- }
- shouldSkipResolving (loc) {
- return loc.sourceFile || loc.error || (loc.file && loc.file.indexOf ('<') >= 0) // skip things like <anonymous> and stuff that was already fetched
- }
- withSourceResolved (loc, resolved) {
- if (resolved.sourceFile && !resolved.sourceFile.error) {
- resolved.file = nixSlashes (resolved.sourceFile.path)
- resolved = this.extractEntryMetadata (resolved)
- }
- if (resolved.sourceLine.includes ('// @hide')) {
- resolved.sourceLine = resolved.sourceLine.replace ('// @hide', '')
- resolved.hide = true
- }
- if (resolved.sourceLine.includes ('__webpack_require__') || // webpack-specific heuristics
- resolved.sourceLine.includes ('/******/ ({')) {
- resolved.thirdParty = true
- }
- return O.assign ({ sourceLine: '' }, loc, resolved)
- }
- withSources () {
- return this.map (x => this.withSource (x))
- }
- withSourcesAsync () {
- return Promise.all (this.items.map (x => this.withSourceAsync (x)))
- .then (items => new StackTracey (items))
- }
- mergeRepeatedLines () {
- return new StackTracey (
- partition (this.items, e => e.file + e.line).map (
- group => {
- return group.items.slice (1).reduce ((memo, entry) => {
- memo.callee = (memo.callee || '<anonymous>') + ' → ' + (entry.callee || '<anonymous>')
- memo.calleeShort = (memo.calleeShort || '<anonymous>') + ' → ' + (entry.calleeShort || '<anonymous>')
- return memo
- }, O.assign ({}, group.items[0]))
- }
- )
- )
- }
- clean () {
- const s = this.withSources ().mergeRepeatedLines ()
- return s.filter (s.isClean.bind (s))
- }
- cleanAsync () {
- return this.withSourcesAsync ().then (s => {
- s = s.mergeRepeatedLines ()
- return s.filter (s.isClean.bind (s))
- })
- }
- isClean (entry, index) {
- return (index === 0) || !(entry.thirdParty || entry.hide || entry.native)
- }
- at (i) {
- return O.assign ({
- beforeParse: '',
- callee: '<???>',
- index: false,
- native: false,
- file: '<???>',
- line: 0,
- column: 0
- }, this.items[i])
- }
- asTable (opts) {
- const maxColumnWidths = (opts && opts.maxColumnWidths) || this.maxColumnWidths ()
- const trimEnd = (s, n) => s && ((s.length > n) ? (s.slice (0, n-1) + '…') : s)
- const trimStart = (s, n) => s && ((s.length > n) ? ('…' + s.slice (-(n-1))) : s)
- const trimmed = this.map (
- e => [
- ('at ' + trimEnd (e.calleeShort, maxColumnWidths.callee)),
- trimStart ((e.fileShort && (e.fileShort + ':' + e.line)) || '', maxColumnWidths.file),
- trimEnd (((e.sourceLine || '').trim () || ''), maxColumnWidths.sourceLine)
- ]
- )
- return asTable (trimmed.items)
- }
- maxColumnWidths () {
- return {
- callee: 30,
- file: 60,
- sourceLine: 80
- }
- }
- static resetCache () {
- getSource.resetCache ()
- getSource.async.resetCache ()
- }
- static locationsEqual (a, b) {
- return (a.file === b.file) &&
- (a.line === b.line) &&
- (a.column === b.column)
- }
- }
- /* Array methods
- ------------------------------------------------------------------------ */
- ;['map', 'filter', 'slice', 'concat'].forEach (method => {
- StackTracey.prototype[method] = function (/*...args */) { // no support for ...args in Node v4 :(
- return new StackTracey (this.items[method].apply (this.items, arguments))
- }
- })
- /* ------------------------------------------------------------------------ */
- module.exports = StackTracey
|