Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"express": "4.18.2",
"fs-extra": "11.1.0",
"lightningcss": "1.21.5",
"launch-editor": "2.6.0",
"mini-css-extract-plugin": "2.7.2",
"node-fetch": "2.6.7",
"nodemon-webpack-plugin": "4.8.1",
Expand Down
49 changes: 49 additions & 0 deletions server/devRoutes.js
Original file line number Diff line number Diff line change
@@ -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 })
})
}
3 changes: 3 additions & 0 deletions server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import generateRobots from './robots'
import template from './template'
import { generateServiceWorker } from './worker'
import { load } from './lazy'
import addDevRoutes from './devRoutes'

const server = express()

Expand Down Expand Up @@ -102,6 +103,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) => {
Expand Down
3 changes: 3 additions & 0 deletions shared/element.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ function normalize(child) {
}

export default function element(type, props, ...children) {
if (module.hot && type === undefined) {
require('./runtimeError').add(props?.__source)
}
children = seed.concat(...children).map(normalize)
if (type === 'textarea') {
children = [children.join('')]
Expand Down
29 changes: 17 additions & 12 deletions shared/generateTree.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,23 @@ import { isClass, isFalse, isFunction, isUndefined } from '../shared/nodes'
import fragment from './fragment'
import { transformNodes } from './plugins'

async function generateBranch(siblings, node, depth, scope) {
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, parent) {
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'
if (module.hot) {
scope.skipHotReplacement = true
return require('./runtimeError').add(parent.attributes?.__source, {
node
})
}
throw new Error(message)
return throwUndefinedNodeProd()
}

if (isFalse(node)) {
Expand Down Expand Up @@ -100,7 +105,7 @@ async function generateBranch(siblings, node, depth, scope) {
}
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
}
Expand Down Expand Up @@ -144,7 +149,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)
}
return
}
Expand All @@ -157,7 +162,7 @@ async function generateBranch(siblings, node, depth, scope) {
})
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) {
Expand All @@ -167,7 +172,7 @@ async function generateBranch(siblings, node, depth, scope) {
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)
}
Expand Down
246 changes: 246 additions & 0 deletions shared/runtimeError.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
// 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',
marginRight: '1rem'
}
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 explanationStyle = {
fontSize: '12px',
fontWeight: 'lighter',
color: '#d19aac',
margin: '1rem 0'
}
const msgTypeStyle = {
color: '#EF5350',
fontSize: '1.1rem',
marginBottom: '1rem',
cursor: 'pointer'
}
const msgTextStyle = {
lineHeight: '1.5',
fontSize: '1rem',
fontFamily: 'Menlo, Consolas, monospace',
fontWeight: 'initial'
}

import { throwUndefinedNodeProd } from './generateTree'

/**
* @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 linkStyle = 'all:unset;margin-left:.3em;color:#F48FB1;font-size:14px;'
const openEditor = `<a style="${linkStyle}">Open in Editor ></a>`
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: `${relativeLOC} ${openEditor}`,
body: file || ''
}
}

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: '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,
explanationElement,
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 }
}

/**
* @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
}

function throwUndefinedMain() {
throw new Error('Your main component is trying to render an undefined node!')
}

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 (options?.node !== undefined) return
if (!isClient()) return throwUndefinedNodeProd(options)
if (!source) return throwUndefinedMain()

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
Loading