Skip to content

Commit

Permalink
feat: show GraphQL compile errors in browser overlay (#6247)
Browse files Browse the repository at this point in the history
This approach to displaying GraphQL compilation errors in the browser console utilizes the WebSocket connecting node running gatsby package responsible for executing GraphQL queries and the cache loaded and used on the browser side (thanks @pieh for helping in this). #5234
  • Loading branch information
leimonio authored and pieh committed Nov 9, 2018
1 parent 7e3227a commit 2cd7bfa
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 68 deletions.
42 changes: 3 additions & 39 deletions packages/gatsby/cache-dir/__tests__/.babelrc
@@ -1,41 +1,5 @@
{
babelrc: false,
presets: [
[
"@babel/preset-env",
{
loose: true,
modules: false,
useBuiltIns: "usage",
shippedProposals: true,
targets: {
browsers: [">0.25%", "not dead"],
},
},
],
[
"@babel/preset-react",
{
useBuiltIns: true,
pragma: "React.createElement",
},
],
],
plugins: [
[
"@babel/plugin-proposal-class-properties",
{
loose: true,
},
],
"@babel/plugin-syntax-dynamic-import",
[
"@babel/plugin-transform-runtime",
{
helpers: true,
regenerator: true,
corejs: false,
},
],
],
"presets": [
["babel-preset-gatsby"]
]
}
59 changes: 59 additions & 0 deletions packages/gatsby/cache-dir/__tests__/error-overlay-handler.js
@@ -0,0 +1,59 @@
import "@babel/polyfill"
const {
reportError,
clearError,
errorMap,
} = require(`../error-overlay-handler`)

import * as ErrorOverlay from "react-error-overlay"

jest.mock(`react-error-overlay`, () => {
return {
reportBuildError: jest.fn(),
dismissBuildError: jest.fn(),
startReportingRuntimeErrors: jest.fn(),
setEditorHandler: jest.fn(),
}
})

beforeEach(() => {
ErrorOverlay.reportBuildError.mockClear()
ErrorOverlay.dismissBuildError.mockClear()
})

describe(`errorOverlayHandler`, () => {
describe(`clearError()`, () => {
beforeEach(() => {
reportError(`foo`, `error`)
reportError(`bar`, `error`)
})
afterAll(() => {
clearError(`foo`)
clearError(`bar`)
})
it(`should clear specific error type`, () => {
expect(Object.keys(errorMap)).toHaveLength(2)
clearError(`foo`)
expect(Object.keys(errorMap)).toHaveLength(1)
expect(ErrorOverlay.dismissBuildError).not.toHaveBeenCalled()
})

it(`should call ErrorOverlay to dismiss build errors`, () => {
clearError(`foo`)
clearError(`bar`)
expect(ErrorOverlay.dismissBuildError).toHaveBeenCalled()
})
})
describe(`reportErrorOverlay()`, () => {
it(`should not add error if it's empty and not call ErrorOverlay`, () => {
reportError(`foo`, null)
expect(Object.keys(errorMap)).toHaveLength(0)
expect(ErrorOverlay.reportBuildError).not.toHaveBeenCalled()
})
it(`should add error if it has a truthy value and call ErrorOverlay`, () => {
reportError(`foo`, `bar`)
expect(Object.keys(errorMap)).toHaveLength(1)
expect(ErrorOverlay.reportBuildError).toHaveBeenCalled()
})
})
})
2 changes: 2 additions & 0 deletions packages/gatsby/cache-dir/__tests__/minimal-config.js
Expand Up @@ -9,6 +9,8 @@ it(
path.join(__dirname, `..`),
`--config-file`,
path.join(__dirname, `.babelrc`),
`--ignore`,
`**/__tests__`,
]

const spawn = child.spawn(process.execPath, args)
Expand Down
41 changes: 41 additions & 0 deletions packages/gatsby/cache-dir/error-overlay-handler.js
@@ -0,0 +1,41 @@
import * as ErrorOverlay from "react-error-overlay"

// Report runtime errors
ErrorOverlay.startReportingRuntimeErrors({
onError: () => {},
filename: `/commons.js`,
})
ErrorOverlay.setEditorHandler(errorLocation =>
window.fetch(
`/__open-stack-frame-in-editor?fileName=` +
window.encodeURIComponent(errorLocation.fileName) +
`&lineNumber=` +
window.encodeURIComponent(errorLocation.lineNumber || 1)
)
)

const errorMap = {}

const handleErrorOverlay = () => {
const errors = Object.values(errorMap)
if (errors.length > 0) {
const errorMsg = errors.join(`\n\n`)
ErrorOverlay.reportBuildError(errorMsg)
} else {
ErrorOverlay.dismissBuildError()
}
}

export const clearError = errorID => {
delete errorMap[errorID]
handleErrorOverlay()
}

export const reportError = (errorID, error) => {
if (error) {
errorMap[errorID] = error
}
handleErrorOverlay()
}

export { errorMap }
23 changes: 5 additions & 18 deletions packages/gatsby/cache-dir/root.js
Expand Up @@ -13,34 +13,21 @@ import loader from "./loader"
import JSONStore from "./json-store"
import EnsureResources from "./ensure-resources"

import * as ErrorOverlay from "react-error-overlay"

// Report runtime errors
ErrorOverlay.startReportingRuntimeErrors({
onError: () => {},
filename: `/commons.js`,
})
ErrorOverlay.setEditorHandler(errorLocation =>
window.fetch(
`/__open-stack-frame-in-editor?fileName=` +
window.encodeURIComponent(errorLocation.fileName) +
`&lineNumber=` +
window.encodeURIComponent(errorLocation.lineNumber || 1)
)
)
import { reportError, clearError } from "./error-overlay-handler"

if (window.__webpack_hot_middleware_reporter__ !== undefined) {
const overlayErrorID = `webpack`
// Report build errors
window.__webpack_hot_middleware_reporter__.useCustomOverlay({
showProblems(type, obj) {
if (type !== `errors`) {
ErrorOverlay.dismissBuildError()
clearError(overlayErrorID)
return
}
ErrorOverlay.reportBuildError(obj[0])
reportError(overlayErrorID, obj[0])
},
clear() {
ErrorOverlay.dismissBuildError()
clearError(overlayErrorID)
},
})
}
Expand Down
11 changes: 9 additions & 2 deletions packages/gatsby/cache-dir/socketIo.js
@@ -1,3 +1,5 @@
import { reportError, clearError } from "./error-overlay-handler"

let socket = null

let staticQueryData = {}
Expand Down Expand Up @@ -29,14 +31,19 @@ export default function socketIo() {
[msg.payload.id]: msg.payload.result,
}
}
}
if (msg.type === `pageQueryResult`) {
} else if (msg.type === `pageQueryResult`) {
if (didDataChange(msg, pageQueryData)) {
pageQueryData = {
...pageQueryData,
[msg.payload.id]: msg.payload.result,
}
}
} else if (msg.type === `overlayError`) {
if (msg.payload.message) {
reportError(msg.payload.id, msg.payload.message)
} else {
clearError(msg.payload.id)
}
}
if (msg.type && msg.payload) {
___emitter.emit(msg.type, msg.payload)
Expand Down
Expand Up @@ -20,6 +20,7 @@ import {
multipleRootQueriesError,
} from "./graphql-errors"
import report from "gatsby-cli/lib/reporter"
const websocketManager = require(`../../utils/websocket-manager`)

import type { DocumentNode, GraphQLSchema } from "graphql"

Expand Down Expand Up @@ -60,9 +61,13 @@ const validationRules = [
VariablesInAllowedPositionRule,
]

let lastRunHadErrors = null
const overlayErrorID = `graphql-compiler`

class Runner {
baseDir: string
schema: GraphQLSchema
errors: string[]
fragmentsDir: string

constructor(baseDir: string, fragmentsDir: string, schema: GraphQLSchema) {
Expand All @@ -72,10 +77,11 @@ class Runner {
}

reportError(message) {
if (process.env.NODE_ENV === `production`) {
report.panic(`${report.format.red(`GraphQL Error`)} ${message}`)
} else {
report.log(`${report.format.red(`GraphQL Error`)} ${message}`)
const queryErrorMessage = `${report.format.red(`GraphQL Error`)} ${message}`
report.panicOnBuild(queryErrorMessage)
if (process.env.gatsby_executing_command === `develop`) {
websocketManager.emitError(overlayErrorID, queryErrorMessage)
lastRunHadErrors = true
}
}

Expand Down Expand Up @@ -200,6 +206,14 @@ class Runner {
compiledNodes.set(filePath, query)
})

if (
process.env.gatsby_executing_command === `develop` &&
lastRunHadErrors
) {
websocketManager.emitError(overlayErrorID, null)
lastRunHadErrors = false
}

return compiledNodes
}
}
Expand Down
Expand Up @@ -32,7 +32,6 @@ module.exports = async (queryJob: QueryJob, component: Any) => {

// Run query
let result

// Nothing to do if the query doesn't exist.
if (!queryJob.query || queryJob.query === ``) {
result = {}
Expand Down Expand Up @@ -97,9 +96,7 @@ ${formatErrorDetails(errorDetails)}`)
dataPath = queryJob.hash
}

const programType = program._[0]

if (programType === `develop`) {
if (process.env.gatsby_executing_command === `develop`) {
if (queryJob.isPage) {
websocketManager.emitPageData({
result,
Expand Down
25 changes: 24 additions & 1 deletion packages/gatsby/src/utils/websocket-manager.js
Expand Up @@ -92,6 +92,7 @@ const getRoomNameFromPath = (path: string): string => `path-${path}`
class WebsocketManager {
pageResults: QueryResultsMap
staticQueryResults: QueryResultsMap
errors: Map<string, QueryResult>
isInitialised: boolean
activePaths: Set<string>
programDir: string
Expand All @@ -101,13 +102,15 @@ class WebsocketManager {
this.activePaths = new Set()
this.pageResults = new Map()
this.staticQueryResults = new Map()
this.errors = new Map()
this.websocket
this.programDir

this.init = this.init.bind(this)
this.getSocket = this.getSocket.bind(this)
this.emitPageData = this.emitPageData.bind(this)
this.emitStaticQueryData = this.emitStaticQueryData.bind(this)
this.emitError = this.emitError.bind(this)
}

init({ server, directory }) {
Expand All @@ -133,6 +136,15 @@ class WebsocketManager {
payload: result,
})
})
this.errors.forEach((message, errorID) => {
this.websocket.send({
type: `overlayError`,
payload: {
id: errorID,
message,
},
})
})

const leaveRoom = path => {
s.leave(getRoomNameFromPath(path))
Expand Down Expand Up @@ -194,10 +206,21 @@ class WebsocketManager {
}

emitPageData(data: QueryResult) {
this.pageResults.set(data.id, data)
if (this.isInitialised) {
this.websocket.send({ type: `pageQueryResult`, payload: data })
}
this.pageResults.set(data.id, data)
}
emitError(id: string, message?: string) {
if (message) {
this.errors.set(id, message)
} else {
this.errors.delete(id)
}

if (this.isInitialised) {
this.websocket.send({ type: `overlayError`, payload: { id, message } })
}
}
}

Expand Down

0 comments on commit 2cd7bfa

Please sign in to comment.