From 9fbda46cf9aa1f6997a9f1695d1ac8d21dae867b Mon Sep 17 00:00:00 2001 From: nityam Date: Wed, 22 Apr 2026 18:24:31 +0530 Subject: [PATCH 01/16] forward jshint syntax errors to the sketch console --- client/modules/Preview/EmbedFrame.jsx | 38 ++++++++++++++++++++++----- client/utils/previewEntry.js | 23 ++++++++++++++++ 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/client/modules/Preview/EmbedFrame.jsx b/client/modules/Preview/EmbedFrame.jsx index 2b6ac16720..3b008f51ec 100644 --- a/client/modules/Preview/EmbedFrame.jsx +++ b/client/modules/Preview/EmbedFrame.jsx @@ -21,6 +21,8 @@ import resolvePathsForElementsWithAttribute from '../../../server/utils/resolveU let objectUrls = {}; let objectPaths = {}; +let jshintErrors = []; +let jshintErrorKeys = new Set(); const Frame = styled.iframe` min-height: 100%; @@ -56,7 +58,7 @@ function resolveCSSLinksInString(content, files) { return newContent; } -function jsPreprocess(jsText) { +function jsPreprocess(jsText, fileName) { let newContent = jsText; // check the code for js errors before sending it to strip comments // or loops. @@ -68,11 +70,26 @@ function jsPreprocess(jsText) { space: true }); newContent = loopProtect(newContent); + } else { + JSHINT.errors.forEach((err) => { + if (!err) return; + const key = `${fileName}:${err.line}:${err.character}:${err.reason}`; + if (jshintErrorKeys.has(key)) return; + jshintErrorKeys.add(key); + jshintErrors.push({ + file: fileName, + line: err.line, + character: err.character, + reason: err.reason, + evidence: err.evidence, + code: err.code + }); + }); } return newContent; } -function resolveJSLinksInString(content, files) { +function resolveJSLinksInString(content, files, fileName) { let newContent = content; let jsFileStrings = content.match(STRING_REGEX); jsFileStrings = jsFileStrings || []; @@ -98,7 +115,7 @@ function resolveJSLinksInString(content, files) { } }); - return jsPreprocess(newContent); + return jsPreprocess(newContent, fileName); } function resolveScripts(sketchDoc, files) { @@ -136,7 +153,7 @@ function resolveScripts(sketchDoc, files) { ) !== null ) { script.setAttribute('crossorigin', ''); - script.innerHTML = resolveJSLinksInString(script.innerHTML, files); // eslint-disable-line + script.innerHTML = resolveJSLinksInString(script.innerHTML, files, 'index.html'); // eslint-disable-line } }); } @@ -175,7 +192,11 @@ function resolveJSAndCSSLinks(files) { files.forEach((file) => { const newFile = { ...file }; if (file.name.match(/.*\.js$/i)) { - newFile.content = resolveJSLinksInString(newFile.content, files); + newFile.content = resolveJSLinksInString( + newFile.content, + files, + file.name + ); } else if (file.name.match(/.*\.css$/i)) { newFile.content = resolveCSSLinksInString(newFile.content, files); } @@ -188,7 +209,7 @@ function addLoopProtect(sketchDoc) { const scriptsInHTML = sketchDoc.getElementsByTagName('script'); const scriptsInHTMLArray = Array.prototype.slice.call(scriptsInHTML); scriptsInHTMLArray.forEach((script) => { - script.innerHTML = jsPreprocess(script.innerHTML); // eslint-disable-line + script.innerHTML = jsPreprocess(script.innerHTML, 'index.html'); // eslint-disable-line }); } @@ -197,6 +218,8 @@ function injectLocalFiles(files, htmlFile, options) { let scriptOffs = []; objectUrls = {}; objectPaths = {}; + jshintErrors = []; + jshintErrorKeys = new Set(); const resolvedFiles = resolveJSAndCSSLinks(files); const parser = new DOMParser(); const sketchDoc = parser.parseFromString(htmlFile.content, 'text/html'); @@ -241,13 +264,14 @@ p5.prototype.registerMethod('afterSetup', p5.prototype.ensureAccessibleCanvas);` const sketchDocString = `\n${sketchDoc.documentElement.outerHTML}`; scriptOffs = getAllScriptOffsets(sketchDocString); const consoleErrorsScript = sketchDoc.createElement('script'); + addLoopProtect(sketchDoc); consoleErrorsScript.innerHTML = ` window.offs = ${JSON.stringify(scriptOffs)}; window.objectUrls = ${JSON.stringify(objectUrls)}; window.objectPaths = ${JSON.stringify(objectPaths)}; + window.__jshintErrors = ${JSON.stringify(jshintErrors)}; window.editorOrigin = '${getConfig('EDITOR_URL')}'; `; - addLoopProtect(sketchDoc); sketchDoc.head.prepend(consoleErrorsScript); return `\n${sketchDoc.documentElement.outerHTML}`; diff --git a/client/utils/previewEntry.js b/client/utils/previewEntry.js index 4292e5a83b..6628ad9811 100644 --- a/client/utils/previewEntry.js +++ b/client/utils/previewEntry.js @@ -35,6 +35,29 @@ setInterval(() => { } }, LOGWAIT); +if (Array.isArray(window.__jshintErrors) && window.__jshintErrors.length > 0) { + const errorLogs = window.__jshintErrors.map((err) => { + const location = `${err.file}:${err.line}:${err.character}`; + const data = `SyntaxError: ${err.reason}\n at ${location}`; + return { + log: [ + { + method: 'error', + data: [data], + id: `${Date.now()}-${err.file}-${err.line}-${err.character}` + } + ] + }; + }); + editor.postMessage( + { + source: 'sketch', + messages: errorLogs + }, + editorOrigin + ); +} + function handleMessageEvent(e) { // maybe don't need this?? idk! if (window.origin !== e.origin) return; From 4279ff3f9bbf57389fdd808e3bc6b8439a17d21f Mon Sep 17 00:00:00 2001 From: nityam Date: Wed, 22 Apr 2026 18:29:01 +0530 Subject: [PATCH 02/16] use stacktrace.fromerror for runtime error stacks --- client/utils/previewEntry.js | 119 ++++++++++++++++++----------------- 1 file changed, 61 insertions(+), 58 deletions(-) diff --git a/client/utils/previewEntry.js b/client/utils/previewEntry.js index 6628ad9811..e38ba9a8cb 100644 --- a/client/utils/previewEntry.js +++ b/client/utils/previewEntry.js @@ -84,29 +84,20 @@ function handleMessageEvent(e) { window.addEventListener('message', handleMessageEvent); -// catch reference errors, via http://stackoverflow.com/a/12747364/2994108 -window.onerror = async function onError( - msg, - source, - lineNumber, - columnNo, - error -) { - // maybe i can use error.stack sometime but i'm having a hard time triggering - // this function - let data; - if (!error) { - data = msg; - } else { - data = `${error.name}: ${error.message}`; - const resolvedFileName = window.objectUrls[source]; - let resolvedLineNo = lineNumber; - if (window.objectUrls[source] === 'index.html') { - resolvedLineNo = lineNumber - htmlOffset; +function formatStackLine({ fileName, functionName, lineNumber, columnNumber }) { + const resolvedFileName = window.objectUrls[fileName] || fileName; + const resolvedFuncName = functionName || '(anonymous function)'; + if (lineNumber && columnNumber) { + let resolvedLineNumber = lineNumber; + if (resolvedFileName === 'index.html') { + resolvedLineNumber = lineNumber - htmlOffset; } - const line = `\n at ${resolvedFileName}:${resolvedLineNo}:${columnNo}`; - data = data.concat(line); + return `\n at ${resolvedFuncName} (${resolvedFileName}:${resolvedLineNumber}:${columnNumber})`; } + return `\n at ${resolvedFuncName} (${resolvedFileName})`; +} + +function postErrorMessage(data) { editor.postMessage( { source: 'sketch', @@ -124,50 +115,62 @@ window.onerror = async function onError( }, editorOrigin ); +} + +// catch reference errors, via http://stackoverflow.com/a/12747364/2994108 +window.onerror = async function onError( + msg, + source, + lineNumber, + columnNo, + error +) { + if (!error) { + postErrorMessage(msg); + return false; + } + let data = `${error.name}: ${error.message}`; + let stackLines = []; + if (error.stack) { + try { + stackLines = await StackTrace.fromError(error); + } catch (e) { + stackLines = []; + } + } + if (stackLines.length > 0) { + stackLines.forEach((stackLine) => { + data = data.concat(formatStackLine(stackLine)); + }); + } else { + data = data.concat( + formatStackLine({ + fileName: source, + functionName: null, + lineNumber, + columnNumber: columnNo + }) + ); + } + postErrorMessage(data); return false; }; // catch rejected promises window.onunhandledrejection = async function onUnhandledRejection(event) { - if (event.reason && event.reason.message) { - let stackLines = []; - if (event.reason.stack) { + if (!event.reason || !event.reason.message) return; + let stackLines = []; + if (event.reason.stack) { + try { stackLines = await StackTrace.fromError(event.reason); + } catch (e) { + stackLines = []; } - let data = `${event.reason.name}: ${event.reason.message}`; - stackLines.forEach((stackLine) => { - const { fileName, functionName, lineNumber, columnNumber } = stackLine; - const resolvedFileName = window.objectUrls[fileName] || fileName; - const resolvedFuncName = functionName || '(anonymous function)'; - let line; - if (lineNumber && columnNumber) { - let resolvedLineNumber = lineNumber; - if (resolvedFileName === 'index.html') { - resolvedLineNumber = lineNumber - htmlOffset; - } - line = `\n at ${resolvedFuncName} (${resolvedFileName}:${resolvedLineNumber}:${columnNumber})`; - } else { - line = `\n at ${resolvedFuncName} (${resolvedFileName})`; - } - data = data.concat(line); - }); - editor.postMessage( - { - source: 'sketch', - messages: [ - { - log: [ - { - method: 'error', - data: [data], - id: Date.now().toString() - } - ] - } - ] - }, - editorOrigin - ); } + let data = `${event.reason.name}: ${event.reason.message}`; + stackLines.forEach((stackLine) => { + data = data.concat(formatStackLine(stackLine)); + }); + postErrorMessage(data); }; // Monkeypatch p5._friendlyError From 719483272f94aad9b1838ca940573c30b24f109c Mon Sep 17 00:00:00 2001 From: nityam Date: Wed, 22 Apr 2026 18:36:48 +0530 Subject: [PATCH 03/16] send structured error meta alongside log strings --- .../modules/IDE/components/Editor/index.jsx | 53 +++++--- client/utils/previewEntry.js | 125 ++++++++++++------ 2 files changed, 115 insertions(+), 63 deletions(-) diff --git a/client/modules/IDE/components/Editor/index.jsx b/client/modules/IDE/components/Editor/index.jsx index 23f1c69900..cb634e16f2 100644 --- a/client/modules/IDE/components/Editor/index.jsx +++ b/client/modules/IDE/components/Editor/index.jsx @@ -148,27 +148,42 @@ function Editor({ useEffect(() => { const consoleErrors = consoleEvents.filter((e) => e.method === 'error'); - if (consoleErrors.length > 0) { - const firstError = consoleErrors[0]; - const errorObj = { stack: firstError.data[0].toString() }; - StackTrace.fromError(errorObj).then((stackLines) => { - expandConsole(); - const line = stackLines.find( - (l) => l.fileName && l.fileName.startsWith('/') - ); - if (!line) return; - const fileNameArray = line.fileName.split('/'); - const fileName = fileNameArray.slice(-1)[0]; - const filePath = fileNameArray.slice(0, -1).join('/'); - const fileWithError = files.find( - (f) => f.name === fileName && f.filePath === filePath - ); - setSelectedFile(fileWithError.id); - addErrorDecoration(codemirrorView.current, line.lineNumber); - }); - } else { + if (consoleErrors.length === 0) { removeErrorDecorations(codemirrorView.current); + return; } + + const applyDecoration = (frame) => { + if (!frame || !frame.fileName) return; + expandConsole(); + const fileNameArray = frame.fileName.split('/'); + const fileName = fileNameArray.slice(-1)[0]; + const filePath = fileNameArray.slice(0, -1).join('/'); + const fileWithError = files.find( + (f) => f.name === fileName && f.filePath === filePath + ); + if (!fileWithError) return; + setSelectedFile(fileWithError.id); + addErrorDecoration(codemirrorView.current, frame.lineNumber); + }; + + const firstError = consoleErrors[0]; + const metaStack = firstError.meta && firstError.meta.stack; + if (Array.isArray(metaStack) && metaStack.length > 0) { + const frame = + metaStack.find((f) => f.fileName && f.fileName.startsWith('/')) || + metaStack[0]; + applyDecoration(frame); + return; + } + + const errorObj = { stack: firstError.data[0].toString() }; + StackTrace.fromError(errorObj).then((stackLines) => { + const frame = stackLines.find( + (l) => l.fileName && l.fileName.startsWith('/') + ); + applyDecoration(frame); + }); }, [consoleEvents]); const editorSectionClass = classNames({ diff --git a/client/utils/previewEntry.js b/client/utils/previewEntry.js index e38ba9a8cb..09540166b3 100644 --- a/client/utils/previewEntry.js +++ b/client/utils/previewEntry.js @@ -39,15 +39,24 @@ if (Array.isArray(window.__jshintErrors) && window.__jshintErrors.length > 0) { const errorLogs = window.__jshintErrors.map((err) => { const location = `${err.file}:${err.line}:${err.character}`; const data = `SyntaxError: ${err.reason}\n at ${location}`; - return { - log: [ - { - method: 'error', - data: [data], - id: `${Date.now()}-${err.file}-${err.line}-${err.character}` - } - ] + const log = { + method: 'error', + data: [data], + id: `${Date.now()}-${err.file}-${err.line}-${err.character}`, + meta: { + name: 'SyntaxError', + message: err.reason, + stack: [ + { + fileName: err.file, + functionName: null, + lineNumber: err.line, + columnNumber: err.character + } + ] + } }; + return { log: [log] }; }); editor.postMessage( { @@ -84,32 +93,51 @@ function handleMessageEvent(e) { window.addEventListener('message', handleMessageEvent); -function formatStackLine({ fileName, functionName, lineNumber, columnNumber }) { +function resolveStackFrame({ + fileName, + functionName, + lineNumber, + columnNumber +}) { const resolvedFileName = window.objectUrls[fileName] || fileName; - const resolvedFuncName = functionName || '(anonymous function)'; + let resolvedLineNumber = lineNumber; + if (resolvedFileName === 'index.html' && lineNumber) { + resolvedLineNumber = lineNumber - htmlOffset; + } + return { + fileName: resolvedFileName, + functionName: functionName || null, + lineNumber: resolvedLineNumber || null, + columnNumber: columnNumber || null + }; +} + +function formatStackFrame({ + fileName, + functionName, + lineNumber, + columnNumber +}) { + const name = functionName || '(anonymous function)'; if (lineNumber && columnNumber) { - let resolvedLineNumber = lineNumber; - if (resolvedFileName === 'index.html') { - resolvedLineNumber = lineNumber - htmlOffset; - } - return `\n at ${resolvedFuncName} (${resolvedFileName}:${resolvedLineNumber}:${columnNumber})`; + return `\n at ${name} (${fileName}:${lineNumber}:${columnNumber})`; } - return `\n at ${resolvedFuncName} (${resolvedFileName})`; + return `\n at ${name} (${fileName})`; } -function postErrorMessage(data) { +function postErrorMessage(data, meta) { + const log = { + method: 'error', + data: [data], + id: Date.now().toString() + }; + if (meta) log.meta = meta; editor.postMessage( { source: 'sketch', messages: [ { - log: [ - { - method: 'error', - data: [data], - id: Date.now().toString() - } - ] + log: [log] } ] }, @@ -129,48 +157,57 @@ window.onerror = async function onError( postErrorMessage(msg); return false; } - let data = `${error.name}: ${error.message}`; - let stackLines = []; + let rawStack = []; if (error.stack) { try { - stackLines = await StackTrace.fromError(error); + rawStack = await StackTrace.fromError(error); } catch (e) { - stackLines = []; + rawStack = []; } } - if (stackLines.length > 0) { - stackLines.forEach((stackLine) => { - data = data.concat(formatStackLine(stackLine)); - }); - } else { - data = data.concat( - formatStackLine({ + if (rawStack.length === 0) { + rawStack = [ + { fileName: source, functionName: null, lineNumber, columnNumber: columnNo - }) - ); + } + ]; } - postErrorMessage(data); + const resolvedStack = rawStack.map(resolveStackFrame); + let data = `${error.name}: ${error.message}`; + resolvedStack.forEach((frame) => { + data = data.concat(formatStackFrame(frame)); + }); + postErrorMessage(data, { + name: error.name, + message: error.message, + stack: resolvedStack + }); return false; }; // catch rejected promises window.onunhandledrejection = async function onUnhandledRejection(event) { if (!event.reason || !event.reason.message) return; - let stackLines = []; + let rawStack = []; if (event.reason.stack) { try { - stackLines = await StackTrace.fromError(event.reason); + rawStack = await StackTrace.fromError(event.reason); } catch (e) { - stackLines = []; + rawStack = []; } } + const resolvedStack = rawStack.map(resolveStackFrame); let data = `${event.reason.name}: ${event.reason.message}`; - stackLines.forEach((stackLine) => { - data = data.concat(formatStackLine(stackLine)); + resolvedStack.forEach((frame) => { + data = data.concat(formatStackFrame(frame)); + }); + postErrorMessage(data, { + name: event.reason.name, + message: event.reason.message, + stack: resolvedStack }); - postErrorMessage(data); }; // Monkeypatch p5._friendlyError From f9a35f39c4266d172bbfa9ea244f17fa75e96338 Mon Sep 17 00:00:00 2001 From: nityam Date: Wed, 22 Apr 2026 19:06:53 +0530 Subject: [PATCH 04/16] fix jshint noise and inline script line mapping --- client/modules/Preview/EmbedFrame.jsx | 50 ++++++++++++++++++++------- client/utils/previewEntry.js | 12 ++++++- 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/client/modules/Preview/EmbedFrame.jsx b/client/modules/Preview/EmbedFrame.jsx index 3b008f51ec..451d283f9a 100644 --- a/client/modules/Preview/EmbedFrame.jsx +++ b/client/modules/Preview/EmbedFrame.jsx @@ -58,27 +58,38 @@ function resolveCSSLinksInString(content, files) { return newContent; } -function jsPreprocess(jsText, fileName) { +const JSHINT_OPTIONS = { + esversion: 11, + asi: true, + laxbreak: true, + laxcomma: true +}; + +function jsPreprocess(jsText, fileName, lineOffset = 0) { let newContent = jsText; // check the code for js errors before sending it to strip comments // or loops. - JSHINT(newContent); + JSHINT(newContent, JSHINT_OPTIONS); + + const fatal = JSHINT.errors.filter( + (err) => err && (!err.code || err.code.startsWith('E')) + ); - if (JSHINT.errors.length === 0) { + if (fatal.length === 0) { newContent = decomment(newContent, { ignore: /\/\/\s*noprotect/g, space: true }); newContent = loopProtect(newContent); } else { - JSHINT.errors.forEach((err) => { - if (!err) return; - const key = `${fileName}:${err.line}:${err.character}:${err.reason}`; + fatal.forEach((err) => { + const line = (err.line || 1) + lineOffset; + const key = `${fileName}:${line}:${err.character}:${err.reason}`; if (jshintErrorKeys.has(key)) return; jshintErrorKeys.add(key); jshintErrors.push({ file: fileName, - line: err.line, + line, character: err.character, reason: err.reason, evidence: err.evidence, @@ -89,7 +100,7 @@ function jsPreprocess(jsText, fileName) { return newContent; } -function resolveJSLinksInString(content, files, fileName) { +function resolveJSLinksInString(content, files, fileName, lineOffset = 0) { let newContent = content; let jsFileStrings = content.match(STRING_REGEX); jsFileStrings = jsFileStrings || []; @@ -115,10 +126,17 @@ function resolveJSLinksInString(content, files, fileName) { } }); - return jsPreprocess(newContent, fileName); + return jsPreprocess(newContent, fileName, lineOffset); } -function resolveScripts(sketchDoc, files) { +function getInlineScriptLineOffset(htmlContent, scriptInner) { + if (!htmlContent || !scriptInner) return 0; + const ix = htmlContent.indexOf(scriptInner); + if (ix < 0) return 0; + return htmlContent.substring(0, ix).split('\n').length - 1; +} + +function resolveScripts(sketchDoc, files, htmlContent) { const scriptsInHTML = sketchDoc.getElementsByTagName('script'); const scriptsInHTMLArray = Array.prototype.slice.call(scriptsInHTML); scriptsInHTMLArray.forEach((script) => { @@ -153,7 +171,12 @@ function resolveScripts(sketchDoc, files) { ) !== null ) { script.setAttribute('crossorigin', ''); - script.innerHTML = resolveJSLinksInString(script.innerHTML, files, 'index.html'); // eslint-disable-line + const inlineOffset = getInlineScriptLineOffset( + htmlContent, + script.innerHTML + ); + script.innerHTML = resolveJSLinksInString(script.innerHTML, files, 'index.html', inlineOffset); // eslint-disable-line + script.dataset.inlineOffset = String(inlineOffset); } }); } @@ -209,7 +232,8 @@ function addLoopProtect(sketchDoc) { const scriptsInHTML = sketchDoc.getElementsByTagName('script'); const scriptsInHTMLArray = Array.prototype.slice.call(scriptsInHTML); scriptsInHTMLArray.forEach((script) => { - script.innerHTML = jsPreprocess(script.innerHTML, 'index.html'); // eslint-disable-line + const inlineOffset = parseInt(script.dataset.inlineOffset || '0', 10); + script.innerHTML = jsPreprocess(script.innerHTML, 'index.html', inlineOffset); // eslint-disable-line }); } @@ -232,7 +256,7 @@ function injectLocalFiles(files, htmlFile, options) { resolvePathsForElementsWithAttribute('href', sketchDoc, resolvedFiles); // should also include background, data, poster, but these are used way less often - resolveScripts(sketchDoc, resolvedFiles); + resolveScripts(sketchDoc, resolvedFiles, htmlFile.content); resolveStyles(sketchDoc, resolvedFiles); if (textOutput || gridOutput) { diff --git a/client/utils/previewEntry.js b/client/utils/previewEntry.js index 09540166b3..016d0039b5 100644 --- a/client/utils/previewEntry.js +++ b/client/utils/previewEntry.js @@ -93,13 +93,23 @@ function handleMessageEvent(e) { window.addEventListener('message', handleMessageEvent); +function resolveFileName(rawName) { + if (!rawName) return rawName; + if (window.objectUrls[rawName]) return window.objectUrls[rawName]; + const withBlob = `blob:${rawName}`; + if (window.objectUrls[withBlob]) return window.objectUrls[withBlob]; + const segment = rawName.split('/').pop(); + if (window.objectPaths[segment]) return window.objectPaths[segment]; + return rawName; +} + function resolveStackFrame({ fileName, functionName, lineNumber, columnNumber }) { - const resolvedFileName = window.objectUrls[fileName] || fileName; + const resolvedFileName = resolveFileName(fileName); let resolvedLineNumber = lineNumber; if (resolvedFileName === 'index.html' && lineNumber) { resolvedLineNumber = lineNumber - htmlOffset; From 36aa31e79184c74031bbc29bc63449b58984a681 Mon Sep 17 00:00:00 2001 From: nityam Date: Wed, 22 Apr 2026 19:16:42 +0530 Subject: [PATCH 05/16] suppress broken inline script to avoid duplicate parse error --- client/modules/Preview/EmbedFrame.jsx | 32 +++++++++++++-------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/client/modules/Preview/EmbedFrame.jsx b/client/modules/Preview/EmbedFrame.jsx index 451d283f9a..fc4b76453a 100644 --- a/client/modules/Preview/EmbedFrame.jsx +++ b/client/modules/Preview/EmbedFrame.jsx @@ -81,23 +81,23 @@ function jsPreprocess(jsText, fileName, lineOffset = 0) { space: true }); newContent = loopProtect(newContent); - } else { - fatal.forEach((err) => { - const line = (err.line || 1) + lineOffset; - const key = `${fileName}:${line}:${err.character}:${err.reason}`; - if (jshintErrorKeys.has(key)) return; - jshintErrorKeys.add(key); - jshintErrors.push({ - file: fileName, - line, - character: err.character, - reason: err.reason, - evidence: err.evidence, - code: err.code - }); - }); + return newContent; } - return newContent; + fatal.forEach((err) => { + const line = (err.line || 1) + lineOffset; + const key = `${fileName}:${line}:${err.character}:${err.reason}`; + if (jshintErrorKeys.has(key)) return; + jshintErrorKeys.add(key); + jshintErrors.push({ + file: fileName, + line, + character: err.character, + reason: err.reason, + evidence: err.evidence, + code: err.code + }); + }); + return `/* p5 sketch suppressed due to syntax errors in ${fileName}, see console */`; } function resolveJSLinksInString(content, files, fileName, lineOffset = 0) { From 50dd5e4c2b57d324d485442aae027e5985708b3f Mon Sep 17 00:00:00 2001 From: nityam Date: Wed, 22 Apr 2026 19:20:56 +0530 Subject: [PATCH 06/16] dedupe jshint errors by file line and reason --- client/modules/Preview/EmbedFrame.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/modules/Preview/EmbedFrame.jsx b/client/modules/Preview/EmbedFrame.jsx index fc4b76453a..95cfae45a2 100644 --- a/client/modules/Preview/EmbedFrame.jsx +++ b/client/modules/Preview/EmbedFrame.jsx @@ -85,7 +85,7 @@ function jsPreprocess(jsText, fileName, lineOffset = 0) { } fatal.forEach((err) => { const line = (err.line || 1) + lineOffset; - const key = `${fileName}:${line}:${err.character}:${err.reason}`; + const key = `${fileName}:${line}:${err.reason}`; if (jshintErrorKeys.has(key)) return; jshintErrorKeys.add(key); jshintErrors.push({ From 278b9c93ee59b0c80d10b6f4895d630677cedd8c Mon Sep 17 00:00:00 2001 From: nityam Date: Wed, 22 Apr 2026 19:25:34 +0530 Subject: [PATCH 07/16] emit friendly p5 fes style message for jshint syntax errors --- client/utils/previewEntry.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/client/utils/previewEntry.js b/client/utils/previewEntry.js index 016d0039b5..78c036bd6c 100644 --- a/client/utils/previewEntry.js +++ b/client/utils/previewEntry.js @@ -58,6 +58,17 @@ if (Array.isArray(window.__jshintErrors) && window.__jshintErrors.length > 0) { }; return { log: [log] }; }); + const first = window.__jshintErrors[0]; + const friendlyText = `🌸 p5.js says:\nSyntax Error - Symbol present at a place that wasn't expected.\n[${first.file}, line ${first.line}] Usually this is due to a typo. Check the line number in the error for anything missing/extra.\n\n+ More info: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Unexpected_token#What_went_wrong`; + errorLogs.push({ + log: [ + { + method: 'log', + data: [friendlyText], + id: `${Date.now()}-jshint-friendly` + } + ] + }); editor.postMessage( { source: 'sketch', From 3076ec80e277bd05e17f37d82a5ecde69ef86f75 Mon Sep 17 00:00:00 2001 From: nityam Date: Wed, 22 Apr 2026 19:27:10 +0530 Subject: [PATCH 08/16] Revert "emit friendly p5 fes style message for jshint syntax errors" This reverts commit 278b9c93ee59b0c80d10b6f4895d630677cedd8c. --- client/utils/previewEntry.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/client/utils/previewEntry.js b/client/utils/previewEntry.js index 78c036bd6c..016d0039b5 100644 --- a/client/utils/previewEntry.js +++ b/client/utils/previewEntry.js @@ -58,17 +58,6 @@ if (Array.isArray(window.__jshintErrors) && window.__jshintErrors.length > 0) { }; return { log: [log] }; }); - const first = window.__jshintErrors[0]; - const friendlyText = `🌸 p5.js says:\nSyntax Error - Symbol present at a place that wasn't expected.\n[${first.file}, line ${first.line}] Usually this is due to a typo. Check the line number in the error for anything missing/extra.\n\n+ More info: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Unexpected_token#What_went_wrong`; - errorLogs.push({ - log: [ - { - method: 'log', - data: [friendlyText], - id: `${Date.now()}-jshint-friendly` - } - ] - }); editor.postMessage( { source: 'sketch', From 6bbb3380c21dc48db873560b71e26a7e3fbf015e Mon Sep 17 00:00:00 2001 From: nityam Date: Wed, 22 Apr 2026 19:35:18 +0530 Subject: [PATCH 09/16] render jshint errors inline with dynamic friendly hints --- client/utils/previewEntry.js | 94 ++++++++++++++++++++++++++++-------- 1 file changed, 74 insertions(+), 20 deletions(-) diff --git a/client/utils/previewEntry.js b/client/utils/previewEntry.js index 016d0039b5..8c458b8106 100644 --- a/client/utils/previewEntry.js +++ b/client/utils/previewEntry.js @@ -35,33 +35,87 @@ setInterval(() => { } }, LOGWAIT); +function friendlyHintForJshint(err) { + const prefix = `[${err.file}, line ${err.line}]`; + const reason = err.reason || ''; + if (/expected an identifier/i.test(reason)) { + return `${prefix} a value or variable name is missing before the symbol here.`; + } + if (/missing semicolon/i.test(reason)) { + return `${prefix} a ';' might be missing at the end of this statement.`; + } + if (/unclosed (string|regular expression)/i.test(reason)) { + return `${prefix} a string or regular expression is not closed with a matching quote or delimiter.`; + } + if (/missing '\)'/i.test(reason)) { + return `${prefix} a closing ')' is missing. check for unbalanced parentheses.`; + } + if (/missing '\}'/i.test(reason)) { + return `${prefix} a closing '}' is missing. check for unbalanced braces.`; + } + if (/unmatched '/i.test(reason)) { + return `${prefix} a bracket, brace, or parenthesis on this line does not have a matching partner.`; + } + if (/unexpected early end/i.test(reason)) { + return `${prefix} the code ended while a block was still open. a '}' or ')' may be missing.`; + } + if ( + /unexpected '?;'?/i.test(reason) || + /unexpected token ';'/i.test(reason) + ) { + return `${prefix} there is an extra or misplaced ';' on this line.`; + } + if ( + /use of const before it was defined|'[a-z_$][\w$]*' was used before it was defined/i.test( + reason + ) + ) { + return `${prefix} a name is being used before it is declared.`; + } + return `${prefix} ${reason}`; +} + if (Array.isArray(window.__jshintErrors) && window.__jshintErrors.length > 0) { - const errorLogs = window.__jshintErrors.map((err) => { + const messagesBatch = []; + window.__jshintErrors.forEach((err) => { const location = `${err.file}:${err.line}:${err.character}`; - const data = `SyntaxError: ${err.reason}\n at ${location}`; - const log = { - method: 'error', - data: [data], - id: `${Date.now()}-${err.file}-${err.line}-${err.character}`, - meta: { - name: 'SyntaxError', - message: err.reason, - stack: [ - { - fileName: err.file, - functionName: null, - lineNumber: err.line, - columnNumber: err.character + const data = `SyntaxError: ${err.reason} at ${location}`; + const id = `${Date.now()}-${err.file}-${err.line}-${err.character}`; + messagesBatch.push({ + log: [ + { + method: 'error', + data: [data], + id, + meta: { + name: 'SyntaxError', + message: err.reason, + stack: [ + { + fileName: err.file, + functionName: null, + lineNumber: err.line, + columnNumber: err.character + } + ] } - ] - } - }; - return { log: [log] }; + } + ] + }); + messagesBatch.push({ + log: [ + { + method: 'log', + data: [`🌸 p5.js says: ${friendlyHintForJshint(err)}`], + id: `${id}-hint` + } + ] + }); }); editor.postMessage( { source: 'sketch', - messages: errorLogs + messages: messagesBatch }, editorOrigin ); From 20737cd6b2b16f11a92defe528d872ca80b855e2 Mon Sep 17 00:00:00 2001 From: nityam Date: Wed, 22 Apr 2026 19:49:32 +0530 Subject: [PATCH 10/16] add mdn reference link to jshint friendly hint --- client/utils/previewEntry.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/utils/previewEntry.js b/client/utils/previewEntry.js index 8c458b8106..3edee7ea81 100644 --- a/client/utils/previewEntry.js +++ b/client/utils/previewEntry.js @@ -102,11 +102,15 @@ if (Array.isArray(window.__jshintErrors) && window.__jshintErrors.length > 0) { } ] }); + const mdn = + 'https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Unexpected_token#What_went_wrong'; messagesBatch.push({ log: [ { method: 'log', - data: [`🌸 p5.js says: ${friendlyHintForJshint(err)}`], + data: [ + `🌸 p5.js says: ${friendlyHintForJshint(err)} + More info: ${mdn}` + ], id: `${id}-hint` } ] From e80800876b29ea0f38ac8ce0546c7be7dad9537c Mon Sep 17 00:00:00 2001 From: nityam Date: Wed, 22 Apr 2026 19:51:01 +0530 Subject: [PATCH 11/16] put mdn link on its own line in the friendly hint --- client/utils/previewEntry.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/utils/previewEntry.js b/client/utils/previewEntry.js index 3edee7ea81..21028eb2a0 100644 --- a/client/utils/previewEntry.js +++ b/client/utils/previewEntry.js @@ -109,7 +109,9 @@ if (Array.isArray(window.__jshintErrors) && window.__jshintErrors.length > 0) { { method: 'log', data: [ - `🌸 p5.js says: ${friendlyHintForJshint(err)} + More info: ${mdn}` + `🌸 p5.js says: ${friendlyHintForJshint( + err + )}\n\n+ More info: ${mdn}` ], id: `${id}-hint` } From 2db472d23e7f9a85169db7adf79dc81a4d4bde5c Mon Sep 17 00:00:00 2001 From: nityam Date: Wed, 22 Apr 2026 19:55:24 +0530 Subject: [PATCH 12/16] emit mdn reference link only once per batch of jshint errors --- client/utils/previewEntry.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/client/utils/previewEntry.js b/client/utils/previewEntry.js index 21028eb2a0..fd3e1dc538 100644 --- a/client/utils/previewEntry.js +++ b/client/utils/previewEntry.js @@ -102,22 +102,27 @@ if (Array.isArray(window.__jshintErrors) && window.__jshintErrors.length > 0) { } ] }); - const mdn = - 'https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Unexpected_token#What_went_wrong'; messagesBatch.push({ log: [ { method: 'log', - data: [ - `🌸 p5.js says: ${friendlyHintForJshint( - err - )}\n\n+ More info: ${mdn}` - ], + data: [`🌸 p5.js says: ${friendlyHintForJshint(err)}`], id: `${id}-hint` } ] }); }); + const mdn = + 'https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Unexpected_token#What_went_wrong'; + messagesBatch.push({ + log: [ + { + method: 'log', + data: [`+ More info: ${mdn}`], + id: `${Date.now()}-jshint-more-info` + } + ] + }); editor.postMessage( { source: 'sketch', From 81aea2a9b9a7e47d29c4ee08f38545623a52a508 Mon Sep 17 00:00:00 2001 From: nityam Date: Wed, 22 Apr 2026 19:58:19 +0530 Subject: [PATCH 13/16] consolidate jshint friendly hints into single p5 says block --- client/utils/previewEntry.js | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/client/utils/previewEntry.js b/client/utils/previewEntry.js index fd3e1dc538..25816be331 100644 --- a/client/utils/previewEntry.js +++ b/client/utils/previewEntry.js @@ -102,24 +102,18 @@ if (Array.isArray(window.__jshintErrors) && window.__jshintErrors.length > 0) { } ] }); - messagesBatch.push({ - log: [ - { - method: 'log', - data: [`🌸 p5.js says: ${friendlyHintForJshint(err)}`], - id: `${id}-hint` - } - ] - }); }); const mdn = 'https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/Unexpected_token#What_went_wrong'; + const hintLines = window.__jshintErrors + .map((err) => friendlyHintForJshint(err)) + .join('\n'); messagesBatch.push({ log: [ { method: 'log', - data: [`+ More info: ${mdn}`], - id: `${Date.now()}-jshint-more-info` + data: [`🌸 p5.js says:\n${hintLines}\n\n+ More info: ${mdn}`], + id: `${Date.now()}-jshint-friendly` } ] }); From 70822d735c049f1dcd55e7785a93d9b898d0426c Mon Sep 17 00:00:00 2001 From: nityam Date: Wed, 22 Apr 2026 20:04:05 +0530 Subject: [PATCH 14/16] detect missing equals in let const var declarations --- client/utils/previewEntry.js | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/client/utils/previewEntry.js b/client/utils/previewEntry.js index 25816be331..913369610e 100644 --- a/client/utils/previewEntry.js +++ b/client/utils/previewEntry.js @@ -35,9 +35,32 @@ setInterval(() => { } }, LOGWAIT); +function detectMissingEquals(err) { + const evidence = err.evidence || ''; + const match = /(let|const|var)\s+([a-z_$][\w$]*)\s+(\d|['"`]|true\b|false\b|null\b)/i.exec( + evidence + ); + if (!match) return null; + return { keyword: match[1], name: match[2] }; +} + +function refineJshintReason(err) { + if (/missing semicolon/i.test(err.reason || '')) { + const eq = detectMissingEquals(err); + if (eq) { + return `Missing '=' in ${eq.keyword} declaration of '${eq.name}'`; + } + } + return err.reason; +} + function friendlyHintForJshint(err) { const prefix = `[${err.file}, line ${err.line}]`; const reason = err.reason || ''; + const eq = detectMissingEquals(err); + if (eq && /missing semicolon/i.test(reason)) { + return `${prefix} a '=' is missing between the variable name '${eq.name}' and its value.`; + } if (/expected an identifier/i.test(reason)) { return `${prefix} a value or variable name is missing before the symbol here.`; } @@ -78,8 +101,9 @@ function friendlyHintForJshint(err) { if (Array.isArray(window.__jshintErrors) && window.__jshintErrors.length > 0) { const messagesBatch = []; window.__jshintErrors.forEach((err) => { + const refinedReason = refineJshintReason(err); const location = `${err.file}:${err.line}:${err.character}`; - const data = `SyntaxError: ${err.reason} at ${location}`; + const data = `SyntaxError: ${refinedReason} at ${location}`; const id = `${Date.now()}-${err.file}-${err.line}-${err.character}`; messagesBatch.push({ log: [ @@ -89,7 +113,7 @@ if (Array.isArray(window.__jshintErrors) && window.__jshintErrors.length > 0) { id, meta: { name: 'SyntaxError', - message: err.reason, + message: refinedReason, stack: [ { fileName: err.file, From e010707099533680cb2a133c30a6b5ced973bbde Mon Sep 17 00:00:00 2001 From: nityam Date: Wed, 22 Apr 2026 20:09:17 +0530 Subject: [PATCH 15/16] decorate every error line in the selected file --- .../modules/IDE/components/Editor/index.jsx | 65 ++++++++++--------- 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/client/modules/IDE/components/Editor/index.jsx b/client/modules/IDE/components/Editor/index.jsx index cb634e16f2..73191729c9 100644 --- a/client/modules/IDE/components/Editor/index.jsx +++ b/client/modules/IDE/components/Editor/index.jsx @@ -148,41 +148,46 @@ function Editor({ useEffect(() => { const consoleErrors = consoleEvents.filter((e) => e.method === 'error'); - if (consoleErrors.length === 0) { - removeErrorDecorations(codemirrorView.current); - return; - } + removeErrorDecorations(codemirrorView.current); - const applyDecoration = (frame) => { - if (!frame || !frame.fileName) return; - expandConsole(); - const fileNameArray = frame.fileName.split('/'); - const fileName = fileNameArray.slice(-1)[0]; - const filePath = fileNameArray.slice(0, -1).join('/'); - const fileWithError = files.find( - (f) => f.name === fileName && f.filePath === filePath + if (consoleErrors.length === 0) return; + + const resolveFrameFromError = (errorEntry) => { + const metaStack = errorEntry.meta && errorEntry.meta.stack; + if (Array.isArray(metaStack) && metaStack.length > 0) { + return Promise.resolve( + metaStack.find((f) => f.fileName && f.fileName.startsWith('/')) || + metaStack[0] + ); + } + return StackTrace.fromError({ + stack: errorEntry.data[0].toString() + }).then((stackLines) => + stackLines.find((l) => l.fileName && l.fileName.startsWith('/')) ); - if (!fileWithError) return; - setSelectedFile(fileWithError.id); - addErrorDecoration(codemirrorView.current, frame.lineNumber); }; - const firstError = consoleErrors[0]; - const metaStack = firstError.meta && firstError.meta.stack; - if (Array.isArray(metaStack) && metaStack.length > 0) { - const frame = - metaStack.find((f) => f.fileName && f.fileName.startsWith('/')) || - metaStack[0]; - applyDecoration(frame); - return; - } + const matchFile = (frame) => { + if (!frame || !frame.fileName) return null; + const parts = frame.fileName.split('/'); + const fileName = parts.slice(-1)[0]; + const filePath = parts.slice(0, -1).join('/'); + return files.find((f) => f.name === fileName && f.filePath === filePath); + }; - const errorObj = { stack: firstError.data[0].toString() }; - StackTrace.fromError(errorObj).then((stackLines) => { - const frame = stackLines.find( - (l) => l.fileName && l.fileName.startsWith('/') - ); - applyDecoration(frame); + Promise.all(consoleErrors.map(resolveFrameFromError)).then((frames) => { + const pairs = frames + .map((frame) => ({ frame, file: matchFile(frame) })) + .filter((p) => p.frame && p.file); + if (pairs.length === 0) return; + expandConsole(); + const targetFileId = pairs[0].file.id; + setSelectedFile(targetFileId); + pairs + .filter((p) => p.file.id === targetFileId) + .forEach((p) => + addErrorDecoration(codemirrorView.current, p.frame.lineNumber) + ); }); }, [consoleEvents]); From 2e54aa05d4eb6fd83e100f82f304eeb2a50fe56d Mon Sep 17 00:00:00 2001 From: nityam Date: Sat, 25 Apr 2026 00:32:50 +0530 Subject: [PATCH 16/16] fix html line offset mismatch and guard decoration range --- .../modules/IDE/components/Editor/consoleErrorDecoration.js | 4 ++++ client/utils/previewEntry.js | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/client/modules/IDE/components/Editor/consoleErrorDecoration.js b/client/modules/IDE/components/Editor/consoleErrorDecoration.js index ce3890bcca..5023df978f 100644 --- a/client/modules/IDE/components/Editor/consoleErrorDecoration.js +++ b/client/modules/IDE/components/Editor/consoleErrorDecoration.js @@ -36,6 +36,9 @@ const ERROR_DECORATION = Decoration.line({ // Add an error decoration to a specific line number export function addErrorDecoration(view, lineNumber) { + if (!view || !lineNumber) return; + const totalLines = view.state.doc.lines; + if (lineNumber < 1 || lineNumber > totalLines) return; const docLineNumber = view.state.doc.line(lineNumber); view.dispatch({ effects: ADD_ERROR_DECORATION.of([ @@ -46,6 +49,7 @@ export function addErrorDecoration(view, lineNumber) { // Remove all error decorations export function removeErrorDecorations(view) { + if (!view) return; view.dispatch({ effects: FILTER_ERROR_DECORATION.of(() => false) }); diff --git a/client/utils/previewEntry.js b/client/utils/previewEntry.js index 913369610e..d366d8b895 100644 --- a/client/utils/previewEntry.js +++ b/client/utils/previewEntry.js @@ -194,7 +194,9 @@ function resolveStackFrame({ }) { const resolvedFileName = resolveFileName(fileName); let resolvedLineNumber = lineNumber; - if (resolvedFileName === 'index.html' && lineNumber) { + const isIndexHtml = + resolvedFileName === 'index.html' || resolvedFileName === '/index.html'; + if (isIndexHtml && lineNumber) { resolvedLineNumber = lineNumber - htmlOffset; } return {