Last active
July 29, 2021 13:21
-
-
Save kiliman/f2b467d79cbe4b05767d6fe690cd62be to your computer and use it in GitHub Desktop.
Patch to Remix for source-maps
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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