Skip to content

Commit

Permalink
feat(links): open files in configured editor
Browse files Browse the repository at this point in the history
If filenames are absolute the generated links can be opened
in an editor if the `editorScheme` property is set.

If `react-transform-catch-errors` passes in a `filename` this
is used for the top function if the stack trace filename
isn't absolute

Closes: #10
  • Loading branch information
stevoland authored and davidnpma committed Nov 14, 2015
1 parent f0229c5 commit 6b44f19
Show file tree
Hide file tree
Showing 10 changed files with 494 additions and 13 deletions.
5 changes: 4 additions & 1 deletion .babelrc
@@ -1,4 +1,7 @@
{ {
"stage": 0, "stage": 0,
"loose": "all" "loose": "all",
"plugins": [
"rewire"
]
} }
20 changes: 20 additions & 0 deletions README.md
Expand Up @@ -46,3 +46,23 @@ An error that's only in the console is only half the fun. Now you can use all th


## Will this catch errors for me? ## Will this catch errors for me?
No. As you can see above, this is only a UI component for rendering errors and their stack traces. It's works great with other solutions, that automate the error catching for you, see the [examples](https://github.com/KeywordBrain/redbox-react/tree/master/examples). No. As you can see above, this is only a UI component for rendering errors and their stack traces. It's works great with other solutions, that automate the error catching for you, see the [examples](https://github.com/KeywordBrain/redbox-react/tree/master/examples).

## Optional props

`editorScheme` `[?string]` If a filename in the stack trace is local, the component can create the
link to open your editor using this scheme eg: `subl` to create `subl://open?url=file:///filename`.

`useLines` `[boolean=true]` Line numbers in the stack trace may be unreliable depending on the
type of sourcemaps. You can choose to not display them with this flag.

`useColumns` `[boolean=true]` Column numbers in the stack trace may be unreliable depending on the
type of sourcemaps. You can choose to not display them with this flag.

If using [react-transform-catch-errors](https://github.com/gaearon/react-transform-catch-errors#installation) you can add these options to your `.babelrc` through the [`imports` property](https://github.com/gaearon/react-transform-catch-errors#installation).

## Sourcemaps with Webpack

If using [Webpack](https://webpack.github.io) you can get accurate filenames in the stacktrace by
setting the `output.devtoolModuleFilenameTemplate` settings to `/[absolute-resource-path]`.

It's recommended to set `devtool` setting to `'eval'`.
5 changes: 3 additions & 2 deletions package.json
Expand Up @@ -34,13 +34,14 @@
"babel-core": "^5.6.18", "babel-core": "^5.6.18",
"babel-eslint": "^3.1.15", "babel-eslint": "^3.1.15",
"babel-loader": "^5.1.4", "babel-loader": "^5.1.4",
"babel-plugin-rewire": "^0.1.22",
"rimraf": "^2.3.4", "rimraf": "^2.3.4",
"semantic-release": "^4.0.0",
"standard": "^5.0.0-2", "standard": "^5.0.0-2",
"tap-spec": "^4.0.2", "tap-spec": "^4.0.2",
"tape": "^4.0.1", "tape": "^4.0.1",
"webpack": "^1.9.6", "webpack": "^1.9.6",
"webpack-dev-server": "^1.8.2", "webpack-dev-server": "^1.8.2"
"semantic-release": "^4.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"react": ">=0.13.2 || ^0.14.0-rc1" "react": ">=0.13.2 || ^0.14.0-rc1"
Expand Down
29 changes: 25 additions & 4 deletions src/index.js
Expand Up @@ -2,23 +2,44 @@ import React, {Component, PropTypes} from 'react'
import style from './style.js' import style from './style.js'
import ErrorStackParser from 'error-stack-parser' import ErrorStackParser from 'error-stack-parser'
import assign from 'object-assign' import assign from 'object-assign'
import {isFilenameAbsolute, makeUrl, makeLinkText} from './lib'


export default class RedBox extends Component { export default class RedBox extends Component {
static propTypes = { static propTypes = {
error: PropTypes.instanceOf(Error).isRequired error: PropTypes.instanceOf(Error).isRequired,
filename: PropTypes.string,
editorScheme: PropTypes.string,
useLines: PropTypes.bool,
useColumns: PropTypes.bool
} }
static displayName = 'RedBox' static displayName = 'RedBox'
static defaultProps = {
useLines: true,
useColumns: true
}
render () { render () {
const {error} = this.props const {error, filename, editorScheme, useLines, useColumns} = this.props
const {redbox, message, stack, frame, file, linkToFile} = assign({}, style, this.props.style) const {redbox, message, stack, frame, file, linkToFile} = assign({}, style, this.props.style)


const frames = ErrorStackParser.parse(error).map((f, index) => { const frames = ErrorStackParser.parse(error).map((f, index) => {
const link = `${f.fileName}:${f.lineNumber}:${f.columnNumber}` let text
let url

if (index === 0 && filename && !isFilenameAbsolute(f.fileName)) {
url = makeUrl(filename, editorScheme)
text = makeLinkText(filename)
} else {
let lines = useLines ? f.lineNumber : null
let columns = useColumns ? f.columnNumber : null
url = makeUrl(f.fileName, editorScheme, lines, columns)
text = makeLinkText(f.fileName, lines, columns)
}

return ( return (
<div style={frame} key={index}> <div style={frame} key={index}>
<div>{f.functionName}</div> <div>{f.functionName}</div>
<div style={file}> <div style={file}>
<a href={link} style={linkToFile}>{link}</a> <a href={url} style={linkToFile}>{text}</a>
</div> </div>
</div> </div>
) )
Expand Down
63 changes: 63 additions & 0 deletions src/lib.js
@@ -0,0 +1,63 @@
export const filenameWithoutLoaders = (filename) => {
var index = filename.lastIndexOf('!')

return index < 0 ? filename : filename.substr(index + 1)
}

export const filenameHasLoaders = (filename) => {
const actualFilename = filenameWithoutLoaders(filename)

return actualFilename !== filename
}

export const filenameHasSchema = (filename) => {
return /^[\w]+\:/.test(filename)
}

export const isFilenameAbsolute = (filename) => {
const actualFilename = filenameWithoutLoaders(filename)

if (actualFilename.indexOf('/') === 0) {
return true
}

return false
}

export const makeUrl = (filename, scheme, line, column) => {
let actualFilename = filenameWithoutLoaders(filename)

if (filenameHasSchema(filename)) {
return actualFilename
}

let url = `file://${actualFilename}`

if (scheme) {
url = `${scheme}://open?url=${url}`

if (line && actualFilename === filename) {
url = `${url}&line=${line}`

if (column) {
url = `${url}&column=${column}`
}
}
}

return url
}

export const makeLinkText = (filename, line, column) => {
let text = filenameWithoutLoaders(filename)

if (line && text === filename) {
text = `${text}:${line}`

if (column) {
text = `${text}:${column}`
}
}

return text
}
8 changes: 4 additions & 4 deletions tests/errorStackParserMock.js
@@ -1,5 +1,5 @@
import framesStub from './framesStub.json' export default (framesStub) => {

return {
export default { parse: () => framesStub
parse: () => framesStub }
} }
15 changes: 14 additions & 1 deletion tests/framesStub.json
@@ -1 +1,14 @@
[{"functionName":"App.render","fileName":"webpack:///./components/App.js?","lineNumber":45,"columnNumber":12},{"functionName":"App.render","fileName":"webpack:///./~/react-hot-loader/~/react-hot-api/modules/makeAssimilatePrototype.js?","lineNumber":17,"columnNumber":41},{"functionName":"ReactCompositeComponentMixin._renderValidatedComponentWithoutOwnerOrContext","fileName":"webpack:///./~/react/lib/ReactCompositeComponent.js?","lineNumber":789,"columnNumber":34},{"functionName":"ReactCompositeComponentMixin._renderValidatedComponent","fileName":"webpack:///./~/react/lib/ReactCompositeComponent.js?","lineNumber":816,"columnNumber":14},{"functionName":"ReactPerf.measure.wrapper","fileName":"webpack:///./~/react/lib/ReactPerf.js?","lineNumber":70,"columnNumber":21},{"functionName":"ReactCompositeComponentMixin.mountComponent","fileName":"webpack:///./~/react/lib/ReactCompositeComponent.js?","lineNumber":237,"columnNumber":30},{"functionName":"ReactPerf.measure.wrapper","fileName":"webpack:///./~/react/lib/ReactPerf.js?","lineNumber":70,"columnNumber":21},{"functionName":"Object.ReactReconciler.mountComponent","fileName":"webpack:///./~/react/lib/ReactReconciler.js?","lineNumber":38,"columnNumber":35},{"functionName":"mountComponentIntoNode","fileName":"webpack:///./~/react/lib/ReactMount.js?","lineNumber":248,"columnNumber":32},{"functionName":"ReactReconcileTransaction.Mixin.perform","fileName":"webpack:///./~/react/lib/Transaction.js?","lineNumber":134,"columnNumber":20}] [
{
"functionName": "App.render",
"fileName": "webpack:\/\/\/.\/components\/App.js?",
"lineNumber": 45,
"columnNumber": 12
},
{
"functionName": "App.render",
"fileName": "webpack:\/\/\/.\/~\/react-hot-loader\/~\/react-hot-api\/modules\/makeAssimilatePrototype.js?",
"lineNumber": 17,
"columnNumber": 41
}
]
14 changes: 14 additions & 0 deletions tests/framesStubAbsoluteFilenames.json
@@ -0,0 +1,14 @@
[
{
"functionName": "App.render",
"fileName": "\/components\/App.js",
"lineNumber": 45,
"columnNumber": 12
},
{
"functionName": "App.render",
"fileName": "\/some-path\/modules\/makeAssimilatePrototype.js",
"lineNumber": 17,
"columnNumber": 41
}
]

0 comments on commit 6b44f19

Please sign in to comment.