From 58e3a0ae146d1095bc928bb0db7b289e2a9ab1e1 Mon Sep 17 00:00:00 2001 From: Guilherme Correia Date: Tue, 8 Aug 2023 14:02:16 -0300 Subject: [PATCH 1/5] =?UTF-8?q?=E2=9C=A8=20Add=20overlay=20to=20point=20un?= =?UTF-8?q?defined-nodes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/element.js | 4 + shared/generateTree.js | 15 +-- shared/runtimeError.js | 242 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 251 insertions(+), 10 deletions(-) create mode 100644 shared/runtimeError.js diff --git a/shared/element.js b/shared/element.js index 87db2113..a0ff7cc5 100644 --- a/shared/element.js +++ b/shared/element.js @@ -1,4 +1,5 @@ import fragment from './fragment' +import runtimeError from './runtimeError' const seed = Object.freeze([]) @@ -7,6 +8,9 @@ function normalize(child) { } export default function element(type, props, ...children) { + if (type === undefined) { + runtimeError.add(props?.__source, { disableProductionThrow: true }) + } children = seed.concat(...children).map(normalize) if (type === 'textarea') { children = [children.join('')] diff --git a/shared/generateTree.js b/shared/generateTree.js index fae3986d..99c34eba 100644 --- a/shared/generateTree.js +++ b/shared/generateTree.js @@ -2,19 +2,13 @@ import generateKey from '../shared/generateKey' import { isClass, isFalse, isFunction, isUndefined } from '../shared/nodes' import fragment from './fragment' import { transformNodes } from './plugins' +import runtimeErrors from '../shared/runtimeError' -async function generateBranch(siblings, node, depth, scope) { +async function generateBranch(siblings, node, depth, scope, parentAttributes) { transformNodes(scope, node, depth) if (isUndefined(node)) { - let message = 'Attempting to render an undefined node. \n' - if (node === undefined) { - message += - 'This error usually happens because of a missing return statement around JSX or returning undefined from a renderable function.' - } else { - message += 'This error usually happens because of a missing import statement or a typo on a component tag' - } - throw new Error(message) + return runtimeErrors.add(parentAttributes?.__source, { node }) } if (isFalse(node)) { @@ -144,7 +138,7 @@ async function generateBranch(siblings, node, depth, scope) { const children = node.type(context) node.children = [].concat(children) for (let i = 0; i < node.children.length; i++) { - await generateBranch(siblings, node.children[i], `${depth}-${i}`, scope) + await generateBranch(siblings, node.children[i], `${depth}-${i}`, scope, node.attributes) } return } @@ -181,6 +175,7 @@ async function generateBranch(siblings, node, depth, scope) { } export default async function generateTree(node, scope) { + runtimeErrors.clear() const tree = { type: 'div', attributes: { id: 'application' }, children: [] } await generateBranch(tree.children, node, '0', scope) return tree diff --git a/shared/runtimeError.js b/shared/runtimeError.js new file mode 100644 index 00000000..1c358516 --- /dev/null +++ b/shared/runtimeError.js @@ -0,0 +1,242 @@ +// Modified from: webpack-dev-server/client/overlay +// The error overlay is inspired (and mostly copied) by Create React App (https://github.com/facebookincubator/create-react-app) +// They, in turn, got inspired by webpack-hot-middleware (https://github.com/glenjamin/webpack-hot-middleware). +// styles are inspired by `react-error-overlay` +const msgStyles = { + backgroundColor: 'rgba(206, 17, 38, 0.05)', + color: '#fccfcf', + padding: '1rem 1rem 1.5rem 1rem', + marginBottom: '1rem' +} +const iframeStyle = { + 'z-index': 9999999999 +} +const containerStyle = { + position: 'fixed', + inset: 0, + fontSize: '1rem', + padding: '2rem 2rem 1rem', + whiteSpace: 'pre-wrap', + overflow: 'auto', + backgroundColor: '#111827', + fontFamily: 'Roboto, Consolas, monospace', + fontWeight: '600' +} +const headerStyle = { + color: '#EF5350', + fontSize: '2em', + margin: '0 2rem 2rem 0' +} +const dismissButtonStyle = { + color: '#ffffff', + width: '2.5rem', + fontSize: '1.7rem', + margin: '1rem', + cursor: 'pointer', + position: 'absolute', + right: 0, + top: 0, + backgroundColor: 'transparent', + border: 'none', + fontFamily: 'Menlo, Consolas, monospace' +} +const msgTypeStyle = { + color: '#EF5350', + fontSize: '1.1rem', + marginBottom: '1rem', + cursor: 'pointer' +} +const msgTextStyle = { + lineHeight: '1.5', + fontSize: '1rem', + fontFamily: 'Menlo, Consolas, monospace', + fontWeight: 'initial' +} + +/** + * @param {{ source: { fileName: string, lineNumber: string, columnNumber: string }, filenameWithLOC: string }} item + * @returns {Promise<{ header: string, body: string}>} + */ +async function formatProblem(item) { + const { file, relativePath } = await getFile(item.source) + const message = `Undefined node at:\n${file}` + const linkStyle = 'all:unset;margin-left:.3em;color:#F48FB1;font-size:14px;' + const openEditor = `Open in Editor >` + const { lineNumber, columnNumber } = item.source + return { + header: `${relativePath}:${lineNumber}:${columnNumber} ${openEditor}`, + body: message || '' + } +} + +function createOverlay() { + /** @type {HTMLIFrameElement | null | undefined} */ + let overlayElement + /** @type {HTMLDivElement | null | undefined} */ + let containerElement + + /** + * + * @param {HTMLElement} element + * @param {CSSStyleDeclaration} style + */ + function applyStyle(element, style) { + Object.keys(style).forEach(function (prop) { + element.style[prop] = style[prop] + }) + } + + function createEl(elName, styles = {}, attributes = {}) { + const el = document.createElement(elName) + applyStyle(el, styles) + Object.keys(attributes).forEach(key => { + el[key] = attributes[key] + }) + return el + } + + function createContainer(onLoad) { + overlayElement = createEl('div', iframeStyle, { + id: 'nullstack-dev-server-client-overlay' + }) + const contentElement = createEl('div', containerStyle, { + id: 'nullstack-dev-server-client-overlay-div' + }) + const headerElement = createEl('div', headerStyle, { + innerText: 'Runtime errors:' + }) + const closeButtonElement = createEl('button', dismissButtonStyle, { + innerText: 'x', + ariaLabel: 'Dismiss' + }) + closeButtonElement.addEventListener('click', () => clear(true)) + containerElement = createEl('div') + contentElement.append(headerElement, closeButtonElement, containerElement) + + overlayElement.appendChild(contentElement) + onLoad(containerElement) + document.body.appendChild(overlayElement) + } + + /** + * @param {(element: HTMLDivElement) => void} callback + */ + function ensureOverlayExists(callback) { + if (containerElement) { + // Everything is ready, call the callback right away. + callback(containerElement) + return + } + if (overlayElement) { + return + } + createContainer(callback) + } + + // Successful compilation. + /** + * @param {boolean} force + */ + function clear(force) { + const initializedClient = isClient() && initialized() + if ((!initializedClient || !overlayElement) && !force) { + return + } + + // Clean up and reset internal state. + document.body.removeChild(overlayElement) + overlayElement = null + containerElement = null + storedErrors = [] + } + + // Compilation with errors (e.g. syntax error or missing modules). + /** + * @param {{ source: object, filenameWithLOC: string }} messageData + */ + async function show(messageData) { + const { header, body } = await formatProblem(messageData) + ensureOverlayExists(function () { + const typeElement = createEl('div', msgTypeStyle, { + innerHTML: header + }) + typeElement.addEventListener('click', function () { + const query = new URLSearchParams({ + fileName: messageData.filenameWithLOC + }) + fetch(`/nullstack-dev-server/open-editor?${query}`) + }) + + const entryElement = createEl('div', msgStyles) + const messageTextNode = createEl('div', msgTextStyle, { + innerText: body + }) + entryElement.append(typeElement, messageTextNode) + + containerElement.appendChild(entryElement) + }) + } + return { show, clear } +} + +const overlay = createOverlay() +let storedErrors = [] +let initialRenders = 0 + +/** + * @param {{ fileName: string, lineNumber: string, columnNumber: string }} source + * @param {{ disableProductionThrow: boolean, node: object }} options + */ +async function add(source, options) { + ++initialRenders + if (!isClient()) return throwUndefinedProd(options) + if (!source) return throwUndefinedMain(options) + + const { fileName, lineNumber, columnNumber } = source + const filenameWithLOC = `${fileName}:${lineNumber}:${columnNumber}` + if (storedErrors.includes(filenameWithLOC)) return + storedErrors.push(filenameWithLOC) + await overlay.show({ + source, + filenameWithLOC + }) +} + +/** + * @param {{ fileName: string, lineNumber: string, columnNumber: string }} source + */ +async function getFile(source) { + const query = new URLSearchParams(source) + return (await fetch(`/nullstack-dev-server/get-file?${query}`)).json() +} + +function isClient() { + return typeof window !== 'undefined' && window?.document +} + +function initialized() { + return initialRenders > 2 +} + +/** + * @param {{ disableProductionThrow: boolean }} options + */ +function throwUndefinedProd(options) { + if (options.disableProductionThrow) return + throw new Error(` + 🚨 An undefined node exist on your application! + 🚨 Access this route on development mode to get the location!`) +} + +/** + * @param {{ node: object | undefined }} options + */ +function throwUndefinedMain(options) { + if (options.node !== undefined) return + throw new Error('Your main component is trying to render an undefined node!') +} + +module.exports = { + add, + clear: overlay.clear, +} \ No newline at end of file From 0a177ae7dbb61dbbd620202cffbcfaa2ccff2a63 Mon Sep 17 00:00:00 2001 From: Guilherme Correia Date: Tue, 8 Aug 2023 16:04:56 -0300 Subject: [PATCH 2/5] =?UTF-8?q?=E2=9C=A8=20Add=20get-file/open-editor=20ro?= =?UTF-8?q?utes=20to=20dev=20server?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + server/devRoutes.js | 49 +++++++++++++++++++++++++++++++++++++++++++++ server/server.js | 3 +++ 3 files changed, 53 insertions(+) create mode 100644 server/devRoutes.js diff --git a/package.json b/package.json index 3317b859..07eeb604 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "express": "4.18.2", "fs-extra": "11.1.0", "lightningcss": "^1.19.0", + "launch-editor": "2.6.0", "mini-css-extract-plugin": "2.7.2", "node-fetch": "2.6.7", "nodemon-webpack-plugin": "^4.8.1", diff --git a/server/devRoutes.js b/server/devRoutes.js new file mode 100644 index 00000000..43248fe0 --- /dev/null +++ b/server/devRoutes.js @@ -0,0 +1,49 @@ +import path from 'path' +import fs from 'fs' + +/** + * @param {import("express").Application} server + */ +export default function addDevRoutes(server) { + server.get("/nullstack-dev-server/open-editor", (req, res) => { + const { fileName } = req.query + + if (fileName) { + const launchEditor = require("launch-editor") + launchEditor(fileName, process.env.NULLSTACK_EDITOR || 'code') + } + + res.end() + }) + + server.get("/nullstack-dev-server/get-file", (req, res) => { + /** @type {{ fileName: string, lineNumber: string, columnNumber: string }} */ + const { fileName, lineNumber, columnNumber } = req.query + + const originalFile = fs.readFileSync(fileName, 'utf8') + const lineBelowError = ( + '|'.padStart(lineNumber.length + 4, ' ') + + '^'.padStart(parseInt(columnNumber) + 1, ' ') + ) + const file = originalFile + .split('\n') + .map((line, idx) => { + const currentLineNumber = idx + 1 + const formattedLine = ` ${currentLineNumber} | ${line}` + if (currentLineNumber === parseInt(lineNumber)) { + return ['>' + formattedLine, lineBelowError] + } + return ' ' + formattedLine + }) + .flat() + .slice(parseInt(lineNumber) - 3, parseInt(lineNumber) + 3) + .join('\n') + + const relativePath = path + .relative(process.cwd(), fileName) + .split(path.sep) + .join('/') + + res.send({ file, relativePath }) + }) +} diff --git a/server/server.js b/server/server.js index 45e73617..feb91cab 100644 --- a/server/server.js +++ b/server/server.js @@ -17,6 +17,7 @@ import generateRobots from './robots' import template from './template' import { generateServiceWorker } from './worker' import { load } from './lazy' +import addDevRoutes from './devRoutes' const server = express() @@ -101,6 +102,8 @@ server.start = function () { response.contentType('application/json') response.send(generateFile(`${request.params.number}.client.css.map`, server)) }) + + addDevRoutes(server) } server.get(`/manifest.webmanifest`, (request, response) => { From 5d79106aed684bdf375b54f6a4cbe85b153fab06 Mon Sep 17 00:00:00 2001 From: Guilherme Correia Date: Tue, 8 Aug 2023 16:43:42 -0300 Subject: [PATCH 3/5] =?UTF-8?q?=E2=9C=85=20Update=20UndefinedNodes=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/src/UndefinedNodes.njs | 3 +-- tests/src/UndefinedNodes.test.js | 15 ++++----------- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/tests/src/UndefinedNodes.njs b/tests/src/UndefinedNodes.njs index ac78cbfc..47c39633 100644 --- a/tests/src/UndefinedNodes.njs +++ b/tests/src/UndefinedNodes.njs @@ -16,8 +16,7 @@ class UndefinedNodes extends Nullstack { return (
{params.withoutReturn && } - {params.withoutUndefinedReturn && } - {params.withoutRetunr && } + {params.withUndefinedReturn && } {params.forgotToImport && } {params.undeclaredVariable &&
{this.undeclaredVariable}
}
diff --git a/tests/src/UndefinedNodes.test.js b/tests/src/UndefinedNodes.test.js index 1fdca547..f09126d6 100644 --- a/tests/src/UndefinedNodes.test.js +++ b/tests/src/UndefinedNodes.test.js @@ -1,22 +1,15 @@ describe('UndefinedNodes WithoutReturn', () => { test('renderable functions without return should raise an error', async () => { - const response = await page.goto('http://localhost:6969/undefined-nodes?withoutReturn=true') - expect(response.status()).toEqual(500) - }) -}) - -describe('UndefinedNodes WithoutUndefinedReturn', () => { - test('renderable functions with undefined return should raise an error', async () => { - const response = await page.goto('http://localhost:6969/undefined-nodes?withoutUndefinedReturn=true', { + const response = await page.goto('http://localhost:6969/undefined-nodes?withoutReturn=true', { waitUntil: 'networkidle0', }) expect(response.status()).toEqual(500) }) }) -describe('UndefinedNodes WithoutRetunr', () => { - test('tagging a renderable function that does not exist should raise an error', async () => { - const response = await page.goto('http://localhost:6969/undefined-nodes?withoutRetunr=true', { +describe('UndefinedNodes WithUndefinedReturn', () => { + test('renderable functions with undefined return should raise an error', async () => { + const response = await page.goto('http://localhost:6969/undefined-nodes?withUndefinedReturn=true', { waitUntil: 'networkidle0', }) expect(response.status()).toEqual(500) From c9da479dca968150f9ddc20e35798f6516a4a279 Mon Sep 17 00:00:00 2001 From: Guilherme Correia Date: Thu, 10 Aug 2023 21:11:49 -0300 Subject: [PATCH 4/5] =?UTF-8?q?=E2=9A=A1=20Remove=20errorOverlay=20from=20?= =?UTF-8?q?production=20build?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/element.js | 5 ++-- shared/generateTree.js | 16 +++++++++-- shared/runtimeError.js | 63 +++++++++++++++++------------------------- 3 files changed, 41 insertions(+), 43 deletions(-) diff --git a/shared/element.js b/shared/element.js index a0ff7cc5..5d7d4639 100644 --- a/shared/element.js +++ b/shared/element.js @@ -1,5 +1,4 @@ import fragment from './fragment' -import runtimeError from './runtimeError' const seed = Object.freeze([]) @@ -8,8 +7,8 @@ function normalize(child) { } export default function element(type, props, ...children) { - if (type === undefined) { - runtimeError.add(props?.__source, { disableProductionThrow: true }) + if (module.hot && type === undefined) { + require('./runtimeError').add(props?.__source) } children = seed.concat(...children).map(normalize) if (type === 'textarea') { diff --git a/shared/generateTree.js b/shared/generateTree.js index 99c34eba..d7262cf5 100644 --- a/shared/generateTree.js +++ b/shared/generateTree.js @@ -2,13 +2,21 @@ import generateKey from '../shared/generateKey' import { isClass, isFalse, isFunction, isUndefined } from '../shared/nodes' import fragment from './fragment' import { transformNodes } from './plugins' -import runtimeErrors from '../shared/runtimeError' + +export function throwUndefinedNodeProd() { + throw new Error(` + 🚨 An undefined node exist on your application! + 🚨 Access this route on development mode to get the location!`) +} async function generateBranch(siblings, node, depth, scope, parentAttributes) { transformNodes(scope, node, depth) if (isUndefined(node)) { - return runtimeErrors.add(parentAttributes?.__source, { node }) + if (module.hot) { + return require('./runtimeError').add(parentAttributes?.__source, { node }) + } + return throwUndefinedNodeProd() } if (isFalse(node)) { @@ -175,7 +183,9 @@ async function generateBranch(siblings, node, depth, scope, parentAttributes) { } export default async function generateTree(node, scope) { - runtimeErrors.clear() + if (module.hot) { + require('./runtimeError').clear() + } const tree = { type: 'div', attributes: { id: 'application' }, children: [] } await generateBranch(tree.children, node, '0', scope) return tree diff --git a/shared/runtimeError.js b/shared/runtimeError.js index 1c358516..d906ffe8 100644 --- a/shared/runtimeError.js +++ b/shared/runtimeError.js @@ -53,6 +53,8 @@ const msgTextStyle = { fontWeight: 'initial' } +import { throwUndefinedNodeProd } from './generateTree' + /** * @param {{ source: { fileName: string, lineNumber: string, columnNumber: string }, filenameWithLOC: string }} item * @returns {Promise<{ header: string, body: string}>} @@ -179,29 +181,6 @@ function createOverlay() { return { show, clear } } -const overlay = createOverlay() -let storedErrors = [] -let initialRenders = 0 - -/** - * @param {{ fileName: string, lineNumber: string, columnNumber: string }} source - * @param {{ disableProductionThrow: boolean, node: object }} options - */ -async function add(source, options) { - ++initialRenders - if (!isClient()) return throwUndefinedProd(options) - if (!source) return throwUndefinedMain(options) - - const { fileName, lineNumber, columnNumber } = source - const filenameWithLOC = `${fileName}:${lineNumber}:${columnNumber}` - if (storedErrors.includes(filenameWithLOC)) return - storedErrors.push(filenameWithLOC) - await overlay.show({ - source, - filenameWithLOC - }) -} - /** * @param {{ fileName: string, lineNumber: string, columnNumber: string }} source */ @@ -218,16 +197,6 @@ function initialized() { return initialRenders > 2 } -/** - * @param {{ disableProductionThrow: boolean }} options - */ -function throwUndefinedProd(options) { - if (options.disableProductionThrow) return - throw new Error(` - 🚨 An undefined node exist on your application! - 🚨 Access this route on development mode to get the location!`) -} - /** * @param {{ node: object | undefined }} options */ @@ -236,7 +205,27 @@ function throwUndefinedMain(options) { throw new Error('Your main component is trying to render an undefined node!') } -module.exports = { - add, - clear: overlay.clear, -} \ No newline at end of file +const overlay = createOverlay() +let storedErrors = [] +let initialRenders = 0 + +/** + * @param {{ fileName: string, lineNumber: string, columnNumber: string }} source + * @param {{ node: object }} options + */ +export async function add(source, options) { + ++initialRenders + if (!isClient()) return throwUndefinedNodeProd(options) + if (!source) return throwUndefinedMain(options) + + const { fileName, lineNumber, columnNumber } = source + const filenameWithLOC = `${fileName}:${lineNumber}:${columnNumber}` + if (storedErrors.includes(filenameWithLOC)) return + storedErrors.push(filenameWithLOC) + await overlay.show({ + source, + filenameWithLOC + }) +} + +export const clear = overlay.clear From 3b279910bae80b68fd3375745ce764a8286a287f Mon Sep 17 00:00:00 2001 From: Guilherme Correia Date: Thu, 31 Aug 2023 13:48:34 -0300 Subject: [PATCH 5/5] =?UTF-8?q?=E2=9C=A8=20Improve=20undefined-nodes=20ove?= =?UTF-8?q?rlay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/generateTree.js | 18 +++++++++--------- shared/runtimeError.js | 41 ++++++++++++++++++++++++++++------------- 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/shared/generateTree.js b/shared/generateTree.js index d7262cf5..179f9516 100644 --- a/shared/generateTree.js +++ b/shared/generateTree.js @@ -9,12 +9,15 @@ export function throwUndefinedNodeProd() { 🚨 Access this route on development mode to get the location!`) } -async function generateBranch(siblings, node, depth, scope, parentAttributes) { +async function generateBranch(siblings, node, depth, scope, parent) { transformNodes(scope, node, depth) if (isUndefined(node)) { if (module.hot) { - return require('./runtimeError').add(parentAttributes?.__source, { node }) + scope.skipHotReplacement = true + return require('./runtimeError').add(parent.attributes?.__source, { + node + }) } return throwUndefinedNodeProd() } @@ -102,7 +105,7 @@ async function generateBranch(siblings, node, depth, scope, parentAttributes) { } node.children = [].concat(children) for (let i = 0; i < node.children.length; i++) { - await generateBranch(siblings, node.children[i], `${depth}-${i}`, scope) + await generateBranch(siblings, node.children[i], `${depth}-${i}`, scope, node) } return } @@ -146,7 +149,7 @@ async function generateBranch(siblings, node, depth, scope, parentAttributes) { const children = node.type(context) node.children = [].concat(children) for (let i = 0; i < node.children.length; i++) { - await generateBranch(siblings, node.children[i], `${depth}-${i}`, scope, node.attributes) + await generateBranch(siblings, node.children[i], `${depth}-${i}`, scope, node) } return } @@ -159,7 +162,7 @@ async function generateBranch(siblings, node, depth, scope, parentAttributes) { }) for (let i = 0; i < node.children.length; i++) { const id = `${depth}-${i}` - await generateBranch(scope.nextHead, node.children[i], id, scope) + await generateBranch(scope.nextHead, node.children[i], id, scope, node) scope.nextHead[scope.nextHead.length - 1].attributes.id ??= id } } else if (node.children) { @@ -169,7 +172,7 @@ async function generateBranch(siblings, node, depth, scope, parentAttributes) { children: [], } for (let i = 0; i < node.children.length; i++) { - await generateBranch(branch.children, node.children[i], `${depth}-${i}`, scope) + await generateBranch(branch.children, node.children[i], `${depth}-${i}`, scope, node) } siblings.push(branch) } @@ -183,9 +186,6 @@ async function generateBranch(siblings, node, depth, scope, parentAttributes) { } export default async function generateTree(node, scope) { - if (module.hot) { - require('./runtimeError').clear() - } const tree = { type: 'div', attributes: { id: 'application' }, children: [] } await generateBranch(tree.children, node, '0', scope) return tree diff --git a/shared/runtimeError.js b/shared/runtimeError.js index d906ffe8..b689cc01 100644 --- a/shared/runtimeError.js +++ b/shared/runtimeError.js @@ -25,7 +25,7 @@ const containerStyle = { const headerStyle = { color: '#EF5350', fontSize: '2em', - margin: '0 2rem 2rem 0' + marginRight: '1rem' } const dismissButtonStyle = { color: '#ffffff', @@ -40,6 +40,12 @@ const dismissButtonStyle = { border: 'none', fontFamily: 'Menlo, Consolas, monospace' } +const explanationStyle = { + fontSize: '12px', + fontWeight: 'lighter', + color: '#d19aac', + margin: '1rem 0' +} const msgTypeStyle = { color: '#EF5350', fontSize: '1.1rem', @@ -61,13 +67,17 @@ import { throwUndefinedNodeProd } from './generateTree' */ async function formatProblem(item) { const { file, relativePath } = await getFile(item.source) - const message = `Undefined node at:\n${file}` const linkStyle = 'all:unset;margin-left:.3em;color:#F48FB1;font-size:14px;' const openEditor = `Open in Editor >` const { lineNumber, columnNumber } = item.source + const relativeLOC = `${relativePath}:${lineNumber}:${columnNumber}` + console.error( + `Error: Attempting to render an undefined node at\n%O`, + relativeLOC + ) return { - header: `${relativePath}:${lineNumber}:${columnNumber} ${openEditor}`, - body: message || '' + header: `${relativeLOC} ${openEditor}`, + body: file || '' } } @@ -105,15 +115,23 @@ function createOverlay() { id: 'nullstack-dev-server-client-overlay-div' }) const headerElement = createEl('div', headerStyle, { - innerText: 'Runtime errors:' + innerText: 'Undefined nodes found:' }) const closeButtonElement = createEl('button', dismissButtonStyle, { innerText: 'x', ariaLabel: 'Dismiss' }) + const explanationElement = createEl('p', explanationStyle, { + innerText: 'Tip: This error means a missing return statement around JSX, returning undefined from a renderable function, a missing component import or a typo on it\'s tag name.' + }) closeButtonElement.addEventListener('click', () => clear(true)) containerElement = createEl('div') - contentElement.append(headerElement, closeButtonElement, containerElement) + contentElement.append( + headerElement, + closeButtonElement, + explanationElement, + containerElement + ) overlayElement.appendChild(contentElement) onLoad(containerElement) @@ -197,11 +215,7 @@ function initialized() { return initialRenders > 2 } -/** - * @param {{ node: object | undefined }} options - */ -function throwUndefinedMain(options) { - if (options.node !== undefined) return +function throwUndefinedMain() { throw new Error('Your main component is trying to render an undefined node!') } @@ -211,12 +225,13 @@ let initialRenders = 0 /** * @param {{ fileName: string, lineNumber: string, columnNumber: string }} source - * @param {{ node: object }} options + * @param {{ node?: object }} options */ export async function add(source, options) { ++initialRenders + if (options?.node !== undefined) return if (!isClient()) return throwUndefinedNodeProd(options) - if (!source) return throwUndefinedMain(options) + if (!source) return throwUndefinedMain() const { fileName, lineNumber, columnNumber } = source const filenameWithLOC = `${fileName}:${lineNumber}:${columnNumber}`