diff --git a/packages/cli/lib/lib/babel-config.js b/packages/cli/lib/lib/babel-config.js index b9a60a9ee..f49c98eaf 100644 --- a/packages/cli/lib/lib/babel-config.js +++ b/packages/cli/lib/lib/babel-config.js @@ -25,7 +25,10 @@ module.exports = function(env, options = {}) { require.resolve('@babel/plugin-syntax-dynamic-import'), require.resolve('@babel/plugin-transform-object-assign'), [require.resolve('@babel/plugin-proposal-decorators'), { legacy: true }], - [require.resolve('@babel/plugin-proposal-class-properties'), { loose: true }], + [ + require.resolve('@babel/plugin-proposal-class-properties'), + { loose: true }, + ], require.resolve('@babel/plugin-proposal-object-rest-spread'), isProd && require.resolve('babel-plugin-transform-react-remove-prop-types'), diff --git a/packages/cli/lib/lib/webpack/prerender.js b/packages/cli/lib/lib/webpack/prerender.js index a3e93a2b1..63974fb85 100644 --- a/packages/cli/lib/lib/webpack/prerender.js +++ b/packages/cli/lib/lib/webpack/prerender.js @@ -1,10 +1,13 @@ -const { red, yellow } = require('kleur'); +const { red } = require('kleur'); const { resolve } = require('path'); const { readFileSync } = require('fs'); const stackTrace = require('stack-trace'); const { SourceMapConsumer } = require('source-map'); +const { error, info } = require('../../util'); +const outdent = require('outdent'); +const { codeFrameColumns } = require('@babel/code-frame'); -module.exports = function(env, params) { +module.exports = function prerender(env, params) { params = params || {}; let entry = resolve(env.dest, './ssr-build/ssr-bundle.js'); @@ -14,8 +17,8 @@ module.exports = function(env, params) { global.location = { href: url, pathname: url }; try { - let m = require(entry), - app = (m && m.default) || m; + const m = require(entry); + const app = (m && m.default) || m; if (typeof app !== 'function') { // eslint-disable-next-line no-console @@ -32,35 +35,68 @@ module.exports = function(env, params) { )); return renderToString(preact.h(app, { ...params, url })); } catch (err) { - let stack = stackTrace.parse(err).filter(s => s.getFileName() === entry)[0]; + const stack = stackTrace + .parse(err) + .filter(s => s.getFileName().includes('ssr-build'))[0]; if (!stack) { - throw err; + error(err); + return ''; } handlePrerenderError(err, env, stack, entry); + return ''; } }; -async function handlePrerenderError(err, env, stack, entry) { - let errorMessage = err.toString(); - let isReferenceError = errorMessage.startsWith('ReferenceError'); - let methodName = stack.getMethodName(); - let sourceMapContent, position, sourcePath, sourceLines, sourceCodeHighlight; - +function getLines(env, position) { + let sourcePath; try { - sourceMapContent = JSON.parse(readFileSync(`${entry}.map`)); + sourcePath = resolve(env.src, position.source); + return readFileSync(sourcePath, 'utf-8').split('\n'); } catch (err) { - process.stderr.write(red(`Unable to read sourcemap: ${entry}.map\n`)); + try { + sourcePath = resolve(env.cwd, position.source); + return readFileSync(sourcePath, 'utf-8').split('\n'); + } catch (err) { + error(`Unable to read file: ${sourcePath} (${position.source})\n`); + } } +} - if (sourceMapContent) { - await SourceMapConsumer.with(sourceMapContent, null, consumer => { - position = consumer.originalPositionFor({ - line: stack.getLineNumber(), - column: stack.getColumnNumber(), +async function handlePrerenderError(err, env, stack, entry) { + const errorMessage = err.toString(); + const isReferenceError = errorMessage.startsWith('ReferenceError'); + const methodName = stack.getMethodName(); + const fileName = stack.getFileName().replace(/\\/g, '/'); + + let position; + + info(fileName); + if (/webpack:/.test(fileName)) { + position = { + source: fileName.replace(/.+webpack:/, 'webpack://'), + line: stack.getLineNumber(), + column: stack.getColumnNumber(), + }; + } else { + try { + const sourceMapContent = JSON.parse( + readFileSync(`${entry}.map`, 'utf-8') + ); + + await SourceMapConsumer.with(sourceMapContent, null, consumer => { + position = consumer.originalPositionFor({ + line: stack.getLineNumber(), + column: stack.getColumnNumber(), + }); }); - }); + } catch (err) { + error(`Unable to read sourcemap: ${entry}.map`); + } + } + if (position) { + info(JSON.stringify(position)); position.source = position.source .replace('webpack://', '.') .replace(/^.*~\/((?:@[^/]+\/)?[^/]+)/, (s, name) => @@ -68,69 +104,48 @@ async function handlePrerenderError(err, env, stack, entry) { .resolve(name) .replace(/^(.*?\/node_modules\/(@[^/]+\/)?[^/]+)(\/.*)$/, '$1') ); - - sourcePath = resolve(env.src, position.source); - sourceLines; - try { - sourceLines = readFileSync(sourcePath, 'utf-8').split('\n'); - } catch (err) { - try { - sourceLines = readFileSync( - require.resolve(position.source), - 'utf-8' - ).split('\n'); - } catch (err) { - process.stderr.write(red(`Unable to read file: ${sourcePath}\n`)); - } - // process.stderr.write(red(`Unable to read file: ${sourcePath}\n`)); - } - sourceCodeHighlight = ''; - - if (sourceLines) { - for (var i = -4; i <= 4; i++) { - let color = i === 0 ? red : yellow; - let line = position.line + i; - let sourceLine = sourceLines[line - 1]; - sourceCodeHighlight += sourceLine ? `${color(sourceLine)}\n` : ''; - } - } - } - - process.stderr.write('\n'); - process.stderr.write(red(`${errorMessage}\n`)); - process.stderr.write(`method: ${methodName}\n`); - if (sourceMapContent) { - process.stderr.write( - `at: ${sourcePath}:${position.line}:${position.column}\n` - ); - process.stderr.write('\n'); - process.stderr.write('Source code:\n\n'); - process.stderr.write(sourceCodeHighlight); - process.stderr.write('\n'); } else { - process.stderr.write(stack.toString() + '\n'); + position = { + source: stack.getFileName(), + line: stack.getLineNumber(), + column: stack.getColumnNumber(), + }; } - process.stderr.write( - `This ${ + + const sourceLines = getLines(env, position); + + let sourceCodeHighlight = sourceLines + ? codeFrameColumns( + sourceLines.join('\n'), + { start: { line: position.line, column: position.column } }, + { highlightCode: true } + ) + : ''; + + const stderr = process.stderr.write.bind(process.stderr); + + stderr('\n'); + stderr(outdent` + [PrerenderError]: ${red(`${errorMessage}`)} + --> ${position.source}:${position.line}:${position.column} (${methodName || + ''}) + ${sourceCodeHighlight} + + ${red(`${err.stack}`)} + + This ${ isReferenceError ? 'is most likely' : 'could be' - } caused by using DOM or Web APIs.\n` - ); - process.stderr.write( - `Pre-render runs in node and has no access to globals available in browsers.\n\n` - ); - process.stderr.write( - `Consider wrapping code producing error in: 'if (typeof window !== "undefined") { ... }'\n` - ); - - if (methodName === 'componentWillMount') { - process.stderr.write(`or place logic in 'componentDidMount' method.\n`); - } - process.stderr.write('\n'); - process.stderr.write( - `Alternatively use 'preact build --no-prerender' to disable prerendering.\n\n` - ); - process.stderr.write( - 'See https://github.com/developit/preact-cli#pre-rendering for further information.' - ); + } caused by using DOM or Web APIs. + Pre-render runs in node and has no access to globals available in browsers. + Consider wrapping code producing error in: 'if (typeof window !== "undefined") { ... }\ + ${ + methodName === 'componentWillMount' + ? `\nor place logic in 'componentDidMount' method.` + : '' + } + + Alternatively use \`preact build --no-prerender\` to disable prerendering. + See https://github.com/developit/preact-cli#pre-rendering for further information. + `); process.exit(1); } diff --git a/packages/cli/package.json b/packages/cli/package.json index 0ac5a5fec..a2aed0f3e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -64,6 +64,7 @@ "preact-render-to-string": "*" }, "dependencies": { + "@babel/code-frame": "^7.5.5", "@babel/core": "^7.4.5", "@babel/plugin-proposal-class-properties": "^7.4.4", "@babel/plugin-proposal-decorators": "^7.4.4", @@ -105,6 +106,7 @@ "minimatch": "^3.0.3", "optimize-css-assets-webpack-plugin": "^5.0.1", "ora": "^3.4.0", + "outdent": "^0.7.0", "postcss-load-config": "^2.1.0", "postcss-loader": "^3.0.0", "progress-bar-webpack-plugin": "^1.12.1", diff --git a/yarn.lock b/yarn.lock index 3469e729c..fec1342bd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -44,6 +44,13 @@ dependencies: "@babel/highlight" "^7.0.0" +"@babel/code-frame@^7.5.5": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d" + integrity sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw== + dependencies: + "@babel/highlight" "^7.0.0" + "@babel/core@^7.1.0", "@babel/core@^7.4.0", "@babel/core@^7.4.5": version "7.5.4" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.5.4.tgz#4c32df7ad5a58e9ea27ad025c11276324e0b4ddd" @@ -9653,6 +9660,11 @@ osenv@0, osenv@^0.1.4, osenv@^0.1.5: os-homedir "^1.0.0" os-tmpdir "^1.0.0" +outdent@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/outdent/-/outdent-0.7.0.tgz#cfd1f1956305141e0cf3e898ada6547373c1997a" + integrity sha512-Ue462G+UIFoyQmOzapGIKWS3d/9NHeD/018WGEDZIhN2/VaQpVXbofMcZX0socv1fw4/tmEn7Vd3McOdPZfKzQ== + p-cancelable@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" @@ -10211,7 +10223,7 @@ postcss-discard-overridden@^4.0.1: dependencies: postcss "^7.0.0" -postcss-load-config@^2.0.0: +postcss-load-config@^2.0.0, postcss-load-config@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-2.1.0.tgz#c84d692b7bb7b41ddced94ee62e8ab31b417b003" integrity sha512-4pV3JJVPLd5+RueiVVB+gFOAa7GWc25XQcMp86Zexzke69mKf6Nx9LRcQywdz7yZI9n1udOxmLuAwTBypypF8Q==