Skip to content

Instantly share code, notes, and snippets.

@kiliman
Last active July 29, 2021 13:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kiliman/f2b467d79cbe4b05767d6fe690cd62be to your computer and use it in GitHub Desktop.
Save kiliman/f2b467d79cbe4b05767d6fe690cd62be to your computer and use it in GitHub Desktop.
Patch to Remix for source-maps
// Copyright © 2021 React Training LLC. All rights reserved.
'use strict'
const sourceMap = require('source-map')
const fs = require('fs')
const path = require('path')
Object.defineProperty(exports, '__esModule', { value: true })
/**
* This thing probably warrants some explanation.
*
* The whole point here is to emulate componentDidCatch for server rendering and
* data loading. It can get tricky. React can do this on component boundaries
* but doesn't support it for server rendering or data loading. We know enough
* with nested routes to be able to emulate the behavior (because we know them
* statically before rendering.)
*
* Each route can export an `ErrorBoundary`.
*
* - When rendering throws an error, the nearest error boundary will render
* (normal react componentDidCatch). This will be the route's own boundary, but
* if none is provided, it will bubble up to the parents.
* - When data loading throws an error, the nearest error boundary will render
* - When performing an action, the nearest error boundary for the action's
* route tree will render (no redirect happens)
*
* During normal react rendering, we do nothing special, just normal
* componentDidCatch.
*
* For server rendering, we mutate `renderBoundaryRouteId` to know the last
* layout that has an error boundary that tried to render. This emulates which
* layout would catch a thrown error. If the rendering fails, we catch the error
* on the server, and go again a second time with the emulator holding on to the
* information it needs to render the same error boundary as a dynamically
* thrown render error.
*
* When data loading, server or client side, we use the emulator to likewise
* hang on to the error and re-render at the appropriate layout (where a thrown
* error would have been caught by cDC).
*
* When actions throw, it all works the same. There's an edge case to be aware
* of though. Actions normally are required to redirect, but in the case of
* errors, we render the action's route with the emulator holding on to the
* error. If during this render a parent route/loader throws we ignore that new
* error and render the action's original error as deeply as possible. In other
* words, we simply ignore the new error and use the action's error in place
* because it came first, and that just wouldn't be fair to let errors cut in
* line.
*/
const ROOT = process.env.INIT_CWD + '/'
function serializeError(error) {
const lines = error.stack.split('\n')
const stack = lines.map(mapSource).join('\n')
return {
message: error.message,
stack,
}
}
// pattern for source mapping
const re = /at.+\((?<filename>.+):(?<line>\d+):(?<column>\d+)\)/
function mapSource(s) {
const match = re.exec(s)
if (!match) {
// make filename relative to project root
return cleanFilename(s)
}
const { filename, line, column } = match.groups
const mapFile = `${filename}.map`
if (!fs.existsSync(mapFile)) {
return cleanFilename(s)
}
// read source map and setup consumer
const map = JSON.parse(fs.readFileSync(mapFile))
map.sourceRoot = path.dirname(mapFile)
const smc = new sourceMap.SourceMapConsumer(map)
// get position
const pos = getOriginalPositionFor(
smc,
parseInt(line, 10),
parseInt(column, 10),
)
if (!pos.source) {
// no sourcemap so return original line
return cleanFilename(s)
}
const content = getSourceContentFor(smc, pos)
return ` at \`${content.trim()}\` (${cleanFilename(pos.source)}:${
pos.line
}:${pos.column})`
}
function cleanFilename(filename) {
if (filename.includes('route-module:')) {
let start = filename.indexOf('route-module:')
filename = filename.substring(start)
}
let newFilename = filename.replace('route-module:', '').replace(ROOT, './')
return newFilename
}
function getOriginalPositionFor(smc, line, column) {
const mapPos = { line, column }
const pos = smc.originalPositionFor(mapPos)
return pos
}
function getSourceContentFor(smc, pos) {
const src = smc.sourceContentFor(pos.source)
return src.split('\n')[pos.line - 1]
}
exports.serializeError = serializeError
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment