diff --git a/.changeset/early-carrots-appear.md b/.changeset/early-carrots-appear.md new file mode 100644 index 00000000..6d15eed8 --- /dev/null +++ b/.changeset/early-carrots-appear.md @@ -0,0 +1,5 @@ +--- +'wmr': patch +--- + +Add code frame to uncaught exceptions if possible diff --git a/packages/wmr/package.json b/packages/wmr/package.json index 9d7f3959..9d3f7f21 100644 --- a/packages/wmr/package.json +++ b/packages/wmr/package.json @@ -83,7 +83,7 @@ "sade": "^1.7.3", "sass": "^1.34.1", "semver": "^7.3.2", - "simple-code-frame": "^1.1.1", + "simple-code-frame": "^1.2.0", "sirv": "^1.0.6", "sourcemap-codec": "^1.4.8", "stylis": "^4.0.10", diff --git a/packages/wmr/src/build.js b/packages/wmr/src/build.js index 01ebe6bd..e591a61c 100644 --- a/packages/wmr/src/build.js +++ b/packages/wmr/src/build.js @@ -30,11 +30,18 @@ export default async function build(options) { if (!options.prerender) return; - const { routes } = await prerender(options); - const routeMap = routes.reduce((s, r) => { - s += `\n ${r.url}`; - if (r._discoveredBy) s += kl.dim(` [from ${r._discoveredBy.url}]`); - return s; - }, ''); - process.stdout.write(kl.bold(`Prerendered ${routes.length} page${routes.length == 1 ? '' : 's'}:`) + routeMap + '\n'); + try { + const { routes } = await prerender(options); + const routeMap = routes.reduce((s, r) => { + s += `\n ${r.url}`; + if (r._discoveredBy) s += kl.dim(` [from ${r._discoveredBy.url}]`); + return s; + }, ''); + process.stdout.write( + kl.bold(`Prerendered ${routes.length} page${routes.length == 1 ? '' : 's'}:`) + routeMap + '\n' + ); + } catch (err) { + err.hint = 'The following error was thrown during prerendering:'; + throw err; + } } diff --git a/packages/wmr/src/cli.js b/packages/wmr/src/cli.js index 99f6504f..6a111042 100644 --- a/packages/wmr/src/cli.js +++ b/packages/wmr/src/cli.js @@ -6,6 +6,9 @@ import start from './start.js'; import serve from './serve.js'; import * as errorstacks from 'errorstacks'; import * as kl from 'kolorist'; +import { fileURLToPath } from 'url'; +import { promises as fs } from 'fs'; +import { wmrCodeFrame } from './lib/output-utils.js'; const prog = sade('wmr'); @@ -73,21 +76,37 @@ function run(p) { /** * @param {Error} err */ -function catchException(err) { +async function catchException(err) { let text = ''; let stack = ''; + let codeFrame = ''; if (err.stack) { const formattedStack = errorstacks.parseStackTrace(err.stack); if (formattedStack.length > 0) { const idx = err.stack.indexOf(formattedStack[0].raw); text = err.stack.slice(0, idx).trim() + '\n'; stack = formattedStack.map(frame => frame.raw).join('\n'); + + // Find first non-internal frame of the stack + const frame = formattedStack.find(frame => !frame.fileName.startsWith('node:') && frame.type !== 'native'); + if (frame) { + let file = frame.fileName; + file = file.startsWith('file://') ? fileURLToPath(file) : file; + try { + const code = await fs.readFile(file, 'utf-8'); + codeFrame = wmrCodeFrame(code, frame.line - 1, frame.column); + } catch (err) {} + } } } if (!text) text = err.message || err + ''; - process.stderr.write(`\n${kl.red(text)}\n${stack ? kl.dim(stack + '\n\n') : ''}`); + const printFrame = codeFrame ? codeFrame + '\n' : ''; + const printStack = stack ? kl.dim(stack + '\n\n') : ''; + + const hint = err.hint ? err.hint + '\n\n' : ''; + process.stderr.write(`\n${kl.cyan(hint)}${kl.red(text)}${printFrame || '\n'}${printStack}`); process.exit(1); } diff --git a/packages/wmr/src/lib/output-utils.js b/packages/wmr/src/lib/output-utils.js index 678fc385..423fe458 100644 --- a/packages/wmr/src/lib/output-utils.js +++ b/packages/wmr/src/lib/output-utils.js @@ -3,6 +3,10 @@ import { createCodeFrame } from 'simple-code-frame'; import * as util from 'util'; import path from 'path'; +export function wmrCodeFrame(code, line, column, options = {}) { + return createCodeFrame(code, line, column, { maxWidth: Math.min(80, process.stdout.columns - 4), ...options }); +} + /** * @param {import('rollup').RollupOutput} bundle * @param {string} outDir @@ -105,7 +109,7 @@ export function codeFrame(code, loc, { before = 2, after = 3 } = {}) { ({ line, column } = loc); } - return createCodeFrame(code, line - 1, column, { before, after, colors: true }); + return wmrCodeFrame(code, line - 1, column, { before, after, colors: true }); } // Taken from https://github.com/visionmedia/debug/blob/e47f96de3de5921584364b4ac91e2769d22a3b1f/src/node.js#L35 diff --git a/packages/wmr/src/plugins/less-plugin.js b/packages/wmr/src/plugins/less-plugin.js index 6afc975b..a48acce4 100644 --- a/packages/wmr/src/plugins/less-plugin.js +++ b/packages/wmr/src/plugins/less-plugin.js @@ -1,7 +1,7 @@ import path from 'path'; -import { createCodeFrame } from 'simple-code-frame'; import { resolveAlias } from '../lib/aliasing.js'; import { isFile } from '../lib/fs-utils.js'; +import { wmrCodeFrame } from '../lib/output-utils.js'; /** @type {import('less') | undefined} */ let less; @@ -71,7 +71,7 @@ export async function renderLess(code, { id, resolve, sourcemap }) { } catch (err) { if (err.extract && 'line' in err && 'column' in err) { const code = err.extract.filter(l => l !== undefined).join('\n'); - err.codeFrame = createCodeFrame(code, err.line - 1, err.column); + err.codeFrame = wmrCodeFrame(code, err.line - 1, err.column); } throw err; diff --git a/packages/wmr/src/plugins/sass-plugin.js b/packages/wmr/src/plugins/sass-plugin.js index 32775ae9..73963daf 100644 --- a/packages/wmr/src/plugins/sass-plugin.js +++ b/packages/wmr/src/plugins/sass-plugin.js @@ -1,9 +1,8 @@ import path from 'path'; import { promisify } from 'util'; -import { debug } from '../lib/output-utils.js'; +import { wmrCodeFrame, debug } from '../lib/output-utils.js'; import * as kl from 'kolorist'; import { promises as fs } from 'fs'; -import { createCodeFrame } from 'simple-code-frame'; const log = debug('sass'); @@ -120,7 +119,7 @@ export default function sassPlugin({ production, sourcemap, root, mergedAssets } async function handleError(err) { if (err.file) { const code = await fs.readFile(err.file, 'utf-8'); - err.codeFrame = createCodeFrame(code, err.line - 1, err.column); + err.codeFrame = wmrCodeFrame(code, err.line - 1, err.column); } // Sass mixes stack in message, therefore we need to extract // just the message diff --git a/packages/wmr/src/plugins/sucrase-plugin.js b/packages/wmr/src/plugins/sucrase-plugin.js index 5d282c25..b568d9da 100644 --- a/packages/wmr/src/plugins/sucrase-plugin.js +++ b/packages/wmr/src/plugins/sucrase-plugin.js @@ -1,5 +1,5 @@ import * as sucrase from 'sucrase'; -import { createCodeFrame } from 'simple-code-frame'; +import { wmrCodeFrame } from '../lib/output-utils.js'; const cjsDefault = m => ('default' in m ? m.default : m); @@ -68,7 +68,7 @@ export default function sucrasePlugin(opts = {}) { }; } catch (err) { // Enhance error with code frame - err.codeFrame = createCodeFrame(code, err.loc.line - 1, err.loc.column); + err.codeFrame = wmrCodeFrame(code, err.loc.line - 1, err.loc.column); throw err; } } diff --git a/packages/wmr/src/plugins/wmr/styles/styles-plugin.js b/packages/wmr/src/plugins/wmr/styles/styles-plugin.js index ba736584..6728ab81 100644 --- a/packages/wmr/src/plugins/wmr/styles/styles-plugin.js +++ b/packages/wmr/src/plugins/wmr/styles/styles-plugin.js @@ -5,7 +5,7 @@ import { transformCssImports } from '../../../lib/transform-css-imports.js'; import { transformCss } from '../../../lib/transform-css.js'; import { matchAlias } from '../../../lib/aliasing.js'; import { modularizeCss } from './css-modules.js'; -import { createCodeFrame } from 'simple-code-frame'; +import { wmrCodeFrame } from '../../../lib/output-utils.js'; export const STYLE_REG = /\.(?:css|s[ac]ss|less)$/; @@ -48,7 +48,7 @@ export default function wmrStylesPlugin({ root, hot, production, alias, sourcema const lines = source.slice(0, match.index).split('\n'); const line = lines.length - 1; const column = lines[lines.length - 1].length; - const codeFrame = createCodeFrame(source, line, column); + const codeFrame = wmrCodeFrame(source, line, column); const originalName = basename(idRelative); const nameHint = basename(idRelative, extname(idRelative)) + '.module' + extname(idRelative); diff --git a/packages/wmr/test/fixtures/prerender-error/index.html b/packages/wmr/test/fixtures/prerender-error/index.html new file mode 100644 index 00000000..d2c9fb7d --- /dev/null +++ b/packages/wmr/test/fixtures/prerender-error/index.html @@ -0,0 +1,10 @@ + + + + + default title + + + + + diff --git a/packages/wmr/test/fixtures/prerender-error/index.js b/packages/wmr/test/fixtures/prerender-error/index.js new file mode 100644 index 00000000..a50046b6 --- /dev/null +++ b/packages/wmr/test/fixtures/prerender-error/index.js @@ -0,0 +1,3 @@ +export function prerender() { + return window.foo.bar; +} diff --git a/packages/wmr/test/production.test.js b/packages/wmr/test/production.test.js index bca8493e..2948afec 100644 --- a/packages/wmr/test/production.test.js +++ b/packages/wmr/test/production.test.js @@ -867,6 +867,17 @@ describe('production', () => { expect(instance.output.join('\n')).toMatch(/No prerender\(\) function/i); }); }); + + it('should catch uncaught exceptions during prerendering', async () => { + await loadFixture('prerender-error', env); + instance = await runWmr(env.tmp.path, 'build', '--prerender'); + const code = await instance.done; + + await withLog(instance.output, async () => { + expect(code).toBe(1); + expect(instance.output.join('\n')).toMatch(/The following error was thrown during prerendering/i); + }); + }); }); describe('Code Splitting', () => { diff --git a/yarn.lock b/yarn.lock index 1876318e..287d9e52 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8229,10 +8229,10 @@ signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== -simple-code-frame@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/simple-code-frame/-/simple-code-frame-1.1.1.tgz#f5c87a8d68a5d9b0df95081589a19e5ac79f6c2b" - integrity sha512-Xi3wMZQScdiJbg9+nuSIp3aG5FIBd+GMvxPf9tOB1v102/yfngTgQU9aSnULgtYSGqrSqqdMOKunMy+Jhx871g== +simple-code-frame@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/simple-code-frame/-/simple-code-frame-1.2.0.tgz#91e077f306bb6b939c5b77351eb73aa647dd36fb" + integrity sha512-/c0dj+4mp7Og6SZV3vVjWAYVKW1bwq+3zsMOQqkRcIE9CTG6P5Wo/utexgT0jEMG+feTht4H9G3s6ybMlbeA8g== dependencies: kolorist "^1.4.0"