From 143437f55cba7324ec6f366a4483c7f8740dcffb Mon Sep 17 00:00:00 2001 From: William Reiske Date: Tue, 25 Nov 2025 01:19:46 -0700 Subject: [PATCH 1/2] feat(blaze): Add visual error indicator for development - Add error badge in bottom-left corner showing error count - Add modal with detailed error information and stack traces - Implement graceful failure with inline placeholders for missing templates - Add error deduplication to prevent duplicate entries - Integrate with HMR to auto-clear errors when templates are fixed - Add accessibility features (ARIA labels, keyboard navigation) - Auto-disable in production mode - Add TypeScript definitions for new public API BREAKING CHANGE: Missing template errors now show placeholders instead of throwing, which may change error handling behavior in some edge cases. --- packages/blaze-hot/update-templates.js | 18 + packages/blaze/blaze.d.ts | 23 + packages/blaze/errorIndicator.js | 712 +++++++++++++++++++++++++ packages/blaze/exceptions.js | 5 + packages/blaze/lookup.js | 17 +- packages/blaze/package.js | 9 +- packages/blaze/preamble.js | 48 ++ 7 files changed, 827 insertions(+), 5 deletions(-) create mode 100644 packages/blaze/errorIndicator.js diff --git a/packages/blaze-hot/update-templates.js b/packages/blaze-hot/update-templates.js index 57a0757ca..0556c5858 100644 --- a/packages/blaze-hot/update-templates.js +++ b/packages/blaze-hot/update-templates.js @@ -45,6 +45,24 @@ let templateViewPrefix = 'Template.'; // Overrides the default _applyHmrChanges with one that updates the specific // views for modified templates instead of updating everything. Template._applyHmrChanges = function (templateName = UpdateAll) { + // Integration with Blaze Error Indicator: + // When templates are updated via HMR, clear any related errors. + // This provides a better developer experience by automatically + // clearing "No such template" errors when the missing template is added. + if (typeof Blaze !== 'undefined' && Blaze._errorIndicator) { + if (templateName === UpdateAll) { + // Full update: clear all errors since the entire view tree is refreshed + if (typeof Blaze._errorIndicator.clearAll === 'function') { + Blaze._errorIndicator.clearAll(); + } + } else { + // Targeted update: only clear errors related to this specific template + if (typeof Blaze._errorIndicator.removeTemplateError === 'function') { + Blaze._errorIndicator.removeTemplateError(templateName); + } + } + } + if (templateName === UpdateAll || lastUpdateFailed) { lastUpdateFailed = false; clearTimeout(timeout); diff --git a/packages/blaze/blaze.d.ts b/packages/blaze/blaze.d.ts index 9a2896ca6..35db51497 100644 --- a/packages/blaze/blaze.d.ts +++ b/packages/blaze/blaze.d.ts @@ -123,5 +123,28 @@ declare module 'meteor/blaze' { function toHTML(templateOrView: Template | View): string; function toHTMLWithData(templateOrView: Template | View, data: Object | Function): string; + + /** + * Enable or disable the visual error indicator for Blaze errors. + * @param enabled Whether to enable the error indicator (default: true) + */ + function showErrorIndicator(enabled?: boolean): void; + + /** + * Clear all errors from the error indicator. + */ + function clearErrors(): void; + + /** + * Get the current list of Blaze errors. + * @returns Array of error objects + */ + function getErrors(): Array<{ + id: number; + message: string; + error: string; + stack: string; + time: string; + }>; } } diff --git a/packages/blaze/errorIndicator.js b/packages/blaze/errorIndicator.js new file mode 100644 index 000000000..5d2122256 --- /dev/null +++ b/packages/blaze/errorIndicator.js @@ -0,0 +1,712 @@ +/** + * Blaze Error Indicator + * + * A visual error indicator for Blaze template errors during development. + * Displays a badge in the bottom-left corner showing the number of errors, + * with a modal that shows detailed error information when clicked. + * + * Features: + * - Catches and displays Blaze/template-related errors + * - Graceful failure with inline error placeholders + * - Error deduplication + * - HMR integration for automatic error clearing + * - Accessible (ARIA labels, keyboard navigation) + * - Production mode detection (disabled by default in production) + * + * @fileoverview Client-side error indicator for Blaze templates + */ + +(function() { + 'use strict'; + + // Only run on client + if (typeof window === 'undefined' || typeof document === 'undefined') { + return; + } + + // ============================================ + // Configuration + // ============================================ + + var CONFIG = { + // Error deduplication window in milliseconds + DEDUPE_WINDOW_MS: 100, + // CSS class prefix for namespacing + CSS_PREFIX: 'blaze-error', + // Z-index for indicator and modal + Z_INDEX_BADGE: 99999, + Z_INDEX_MODAL: 100000, + // Check for production mode (Meteor sets this) + isProduction: function() { + return typeof Meteor !== 'undefined' && Meteor.isProduction; + } + }; + + // Keywords that identify Blaze-related errors + var BLAZE_KEYWORDS = [ + 'Template', 'Blaze', 'Spacebars', 'No such template', + 'No such function', 'htmljs', 'Can\'t render', + 'Expected Template or View', 'DOMRange', 'parentElement', + 'Unsupported directive', 'Can\'t call non-function' + ]; + + // ============================================ + // State + // ============================================ + + var errors = []; + var isModalOpen = false; + var containerEl = null; + var isInitialized = false; + var styleEl = null; + var isEnabled = true; + + // ============================================ + // Utility Functions + // ============================================ + + /** + * Checks if an error message or stack trace is related to Blaze + * @param {string} message - The error message + * @param {Error} error - The error object + * @returns {boolean} True if the error is Blaze-related + */ + function isBlazeRelatedError(message, error) { + var stack = (error && error.stack) ? error.stack : ''; + var fullText = (message || '') + ' ' + stack; + + for (var i = 0; i < BLAZE_KEYWORDS.length; i++) { + if (fullText.indexOf(BLAZE_KEYWORDS[i]) !== -1) { + return true; + } + } + return false; + } + + /** + * Escapes HTML special characters to prevent XSS + * @param {string} text - Text to escape + * @returns {string} Escaped HTML string + */ + function escapeHtml(text) { + var div = document.createElement('div'); + div.textContent = text || ''; + return div.innerHTML; + } + + /** + * Formats a timestamp to locale time string + * @returns {string} Formatted time string + */ + function formatTime() { + return new Date().toLocaleTimeString(); + } + + // ============================================ + // Styles + // ============================================ + + /** + * CSS styles for the error indicator + * Using template literals would be nice but we need ES5 compatibility + */ + function getStyles() { + var prefix = CONFIG.CSS_PREFIX; + return [ + '.' + prefix + '-indicator {', + ' position: fixed;', + ' bottom: 20px;', + ' left: 20px;', + ' z-index: ' + CONFIG.Z_INDEX_BADGE + ';', + ' display: flex;', + ' align-items: center;', + ' gap: 8px;', + ' padding: 10px 16px;', + ' background-color: #dc3545;', + ' color: white;', + ' border-radius: 8px;', + ' cursor: pointer;', + ' font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;', + ' font-size: 14px;', + ' font-weight: 500;', + ' box-shadow: 0 4px 12px rgba(220, 53, 69, 0.4);', + ' transition: transform 0.2s, box-shadow 0.2s;', + ' border: none;', + '}', + '.' + prefix + '-indicator:hover {', + ' transform: translateY(-2px);', + ' box-shadow: 0 6px 16px rgba(220, 53, 69, 0.5);', + '}', + '.' + prefix + '-indicator:focus {', + ' outline: 2px solid #fff;', + ' outline-offset: 2px;', + '}', + '.' + prefix + '-indicator .error-count {', + ' background-color: rgba(255, 255, 255, 0.2);', + ' padding: 2px 8px;', + ' border-radius: 12px;', + ' font-size: 12px;', + '}', + '.' + prefix + '-modal-overlay {', + ' position: fixed;', + ' top: 0;', + ' left: 0;', + ' right: 0;', + ' bottom: 0;', + ' background-color: rgba(0, 0, 0, 0.5);', + ' z-index: ' + CONFIG.Z_INDEX_MODAL + ';', + ' display: flex;', + ' align-items: center;', + ' justify-content: center;', + ' padding: 20px;', + ' font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;', + '}', + '.' + prefix + '-modal {', + ' background-color: #1e1e1e;', + ' color: #d4d4d4;', + ' border-radius: 12px;', + ' max-width: 800px;', + ' width: 100%;', + ' max-height: 80vh;', + ' display: flex;', + ' flex-direction: column;', + ' box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);', + '}', + '.' + prefix + '-modal-header {', + ' display: flex;', + ' align-items: center;', + ' justify-content: space-between;', + ' padding: 16px 20px;', + ' border-bottom: 1px solid #333;', + ' background-color: #dc3545;', + ' color: white;', + ' border-radius: 12px 12px 0 0;', + '}', + '.' + prefix + '-modal-header h3 {', + ' margin: 0;', + ' font-size: 18px;', + '}', + '.' + prefix + '-modal-close {', + ' background: none;', + ' border: none;', + ' color: white;', + ' font-size: 28px;', + ' cursor: pointer;', + ' padding: 0 4px;', + ' line-height: 1;', + ' border-radius: 4px;', + '}', + '.' + prefix + '-modal-close:hover {', + ' background-color: rgba(255, 255, 255, 0.2);', + '}', + '.' + prefix + '-modal-close:focus {', + ' outline: 2px solid #fff;', + ' outline-offset: 2px;', + '}', + '.' + prefix + '-modal-body {', + ' padding: 20px;', + ' overflow-y: auto;', + ' flex: 1;', + '}', + '.' + prefix + '-item {', + ' background-color: #2d2d2d;', + ' border-radius: 8px;', + ' padding: 16px;', + ' margin-bottom: 12px;', + ' border-left: 4px solid #dc3545;', + '}', + '.' + prefix + '-item:last-child {', + ' margin-bottom: 0;', + '}', + '.' + prefix + '-item-header {', + ' display: flex;', + ' justify-content: space-between;', + ' align-items: flex-start;', + ' margin-bottom: 8px;', + ' gap: 12px;', + '}', + '.' + prefix + '-item-title {', + ' font-weight: 600;', + ' color: #f87171;', + ' font-size: 14px;', + '}', + '.' + prefix + '-item-time {', + ' font-size: 12px;', + ' color: #888;', + ' white-space: nowrap;', + '}', + '.' + prefix + '-item-message {', + ' font-family: "SF Mono", Monaco, Menlo, Consolas, monospace;', + ' font-size: 13px;', + ' color: #e5e5e5;', + ' white-space: pre-wrap;', + ' word-break: break-word;', + '}', + '.' + prefix + '-item-stack {', + ' margin-top: 12px;', + ' padding-top: 12px;', + ' border-top: 1px solid #444;', + ' font-family: "SF Mono", Monaco, Menlo, Consolas, monospace;', + ' font-size: 11px;', + ' color: #888;', + ' white-space: pre-wrap;', + ' word-break: break-word;', + ' max-height: 150px;', + ' overflow-y: auto;', + '}', + '.' + prefix + '-modal-footer {', + ' padding: 12px 20px;', + ' border-top: 1px solid #333;', + ' display: flex;', + ' justify-content: flex-end;', + ' gap: 10px;', + '}', + '.' + prefix + '-btn {', + ' padding: 8px 16px;', + ' border-radius: 6px;', + ' border: none;', + ' cursor: pointer;', + ' font-size: 14px;', + ' font-weight: 500;', + ' transition: background-color 0.15s;', + '}', + '.' + prefix + '-btn:focus {', + ' outline: 2px solid #fff;', + ' outline-offset: 2px;', + '}', + '.' + prefix + '-btn-secondary {', + ' background-color: #333;', + ' color: #d4d4d4;', + '}', + '.' + prefix + '-btn-secondary:hover {', + ' background-color: #444;', + '}', + '.' + prefix + '-btn-primary {', + ' background-color: #dc3545;', + ' color: white;', + '}', + '.' + prefix + '-btn-primary:hover {', + ' background-color: #c82333;', + '}' + ].join('\n'); + } + + /** + * Injects styles into the document head + */ + function injectStyles() { + if (styleEl) return; + + styleEl = document.createElement('style'); + styleEl.id = CONFIG.CSS_PREFIX + '-styles'; + styleEl.textContent = getStyles(); + document.head.appendChild(styleEl); + } + + /** + * Removes injected styles from the document + */ + function removeStyles() { + if (styleEl && styleEl.parentNode) { + styleEl.parentNode.removeChild(styleEl); + styleEl = null; + } + } + + // ============================================ + // DOM Management + // ============================================ + + /** + * Creates the container element for the error indicator + */ + function createContainer() { + if (containerEl) return; + if (!document.body) return; + + containerEl = document.createElement('div'); + containerEl.id = CONFIG.CSS_PREFIX + '-container'; + // Set ARIA live region for screen reader announcements + containerEl.setAttribute('aria-live', 'polite'); + containerEl.setAttribute('aria-atomic', 'true'); + document.body.appendChild(containerEl); + } + + /** + * Removes the container element from the document + */ + function removeContainer() { + if (containerEl && containerEl.parentNode) { + containerEl.parentNode.removeChild(containerEl); + containerEl = null; + } + } + + /** + * Renders the error indicator badge + * @returns {string} HTML string for the badge + */ + function renderBadge() { + var prefix = CONFIG.CSS_PREFIX; + var count = errors.length; + var label = count === 1 ? '1 Blaze error' : count + ' Blaze errors'; + + return [ + '' + ].join(''); + } + + /** + * Renders the error modal + * @returns {string} HTML string for the modal + */ + function renderModal() { + var prefix = CONFIG.CSS_PREFIX; + var html = []; + + html.push( + '' + ); + + return html.join(''); + } + + /** + * Renders the complete UI based on current state + */ + function render() { + if (!containerEl || !isEnabled) return; + + var html = ''; + + if (errors.length > 0) { + html = renderBadge(); + + if (isModalOpen) { + html += renderModal(); + } + } + + containerEl.innerHTML = html; + attachEventListeners(); + } + + // ============================================ + // Event Handlers + // ============================================ + + /** + * Opens the error modal + */ + function openModal() { + isModalOpen = true; + render(); + // Focus the close button for keyboard accessibility + var closeBtn = document.getElementById(CONFIG.CSS_PREFIX + '-close'); + if (closeBtn) closeBtn.focus(); + } + + /** + * Closes the error modal + */ + function closeModal() { + isModalOpen = false; + render(); + // Return focus to the indicator button + var btn = document.getElementById(CONFIG.CSS_PREFIX + '-btn'); + if (btn) btn.focus(); + } + + /** + * Handles keyboard events for accessibility + * @param {KeyboardEvent} e - The keyboard event + */ + function handleKeydown(e) { + if (!isModalOpen) return; + + // Close on Escape key + if (e.key === 'Escape' || e.keyCode === 27) { + e.preventDefault(); + closeModal(); + } + } + + /** + * Attaches event listeners to rendered elements + */ + function attachEventListeners() { + var prefix = CONFIG.CSS_PREFIX; + + var btn = document.getElementById(prefix + '-btn'); + if (btn) { + btn.onclick = openModal; + } + + var closeBtn = document.getElementById(prefix + '-close'); + var closeBtnFooter = document.getElementById(prefix + '-close-btn'); + var overlay = document.getElementById(prefix + '-overlay'); + var clearBtn = document.getElementById(prefix + '-clear'); + + if (closeBtn) { + closeBtn.onclick = closeModal; + } + if (closeBtnFooter) { + closeBtnFooter.onclick = closeModal; + } + if (overlay) { + overlay.onclick = function(e) { + if (e.target === overlay) { + closeModal(); + } + }; + } + if (clearBtn) { + clearBtn.onclick = function() { + errors = []; + isModalOpen = false; + render(); + }; + } + } + + // ============================================ + // Public API Functions + // ============================================ + + /** + * Adds an error to the indicator + * @param {Error|string} error - The error object or message + * @param {string} [msg] - Optional context message + */ + function addError(error, msg) { + if (!isEnabled) return; + + // Ensure we're initialized + if (!isInitialized) { + init(); + } + + var errorMessage = (error && error.message) ? error.message : String(error); + + // Deduplicate: don't add if we have this exact error message recently + var now = Date.now(); + for (var i = errors.length - 1; i >= 0; i--) { + if (errors[i].error === errorMessage && + (now - errors[i].timestamp) < CONFIG.DEDUPE_WINDOW_MS) { + return; + } + } + + errors.push({ + id: now, + message: msg || 'Exception caught in template:', + error: errorMessage, + stack: (error && error.stack) ? error.stack : '', + time: formatTime(), + timestamp: now + }); + + render(); + } + + /** + * Removes errors related to a specific template name + * Used by HMR when a missing template is added + * @param {string} templateName - The name of the template + */ + function removeTemplateError(templateName) { + var pattern = 'No such template: ' + templateName; + var hadErrors = errors.length > 0; + + errors = errors.filter(function(err) { + return err.error.indexOf(pattern) === -1; + }); + + if (hadErrors && errors.length === 0) { + isModalOpen = false; + } + + render(); + } + + /** + * Clears all errors + */ + function clearAll() { + errors = []; + isModalOpen = false; + render(); + } + + /** + * Initializes the error indicator + */ + function init() { + if (isInitialized) return; + + // Skip initialization in production mode by default + if (CONFIG.isProduction()) { + isEnabled = false; + return; + } + + if (!document.body) { + // Wait for body to be available + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + setTimeout(init, 10); + } + return; + } + + isInitialized = true; + injectStyles(); + createContainer(); + render(); + + // Add keyboard event listener for accessibility + document.addEventListener('keydown', handleKeydown); + + // Listen for global errors that might be Blaze-related + window.addEventListener('error', function(event) { + if (isBlazeRelatedError(event.message, event.error)) { + addError(event.error || new Error(event.message), 'Uncaught template error:'); + } + }); + + window.addEventListener('unhandledrejection', function(event) { + var errorMsg = (event.reason && event.reason.message) + ? event.reason.message + : String(event.reason); + if (isBlazeRelatedError(errorMsg, event.reason)) { + addError(event.reason, 'Unhandled template promise rejection:'); + } + }); + } + + /** + * Destroys the error indicator and cleans up resources + */ + function destroy() { + document.removeEventListener('keydown', handleKeydown); + removeContainer(); + removeStyles(); + errors = []; + isModalOpen = false; + isInitialized = false; + isEnabled = false; + } + + // ============================================ + // Blaze Integration + // ============================================ + + /** + * Internal API for Blaze integration + * @private + */ + Blaze._errorIndicator = { + addError: addError, + removeTemplateError: removeTemplateError, + clearAll: clearAll, + init: init, + destroy: destroy + }; + + /** + * Enable or disable the error indicator + * @param {boolean} [enabled=true] - Whether to enable the indicator + * @memberof Blaze + * @example + * // Disable the error indicator + * Blaze.showErrorIndicator(false); + * + * // Enable the error indicator + * Blaze.showErrorIndicator(true); + */ + Blaze.showErrorIndicator = function(enabled) { + if (enabled !== false) { + isEnabled = true; + init(); + } else { + destroy(); + } + }; + + /** + * Clear all errors from the indicator + * @memberof Blaze + * @example + * Blaze.clearErrors(); + */ + Blaze.clearErrors = function() { + clearAll(); + }; + + /** + * Get a copy of the current errors array + * @returns {Array} Array of error objects + * @memberof Blaze + * @example + * const errors = Blaze.getErrors(); + * console.log('There are', errors.length, 'errors'); + */ + Blaze.getErrors = function() { + return errors.slice(); + }; + + // ============================================ + // Auto-initialization + // ============================================ + + // Auto-initialize when DOM is ready (in development mode only) + if (!CONFIG.isProduction()) { + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + // Use setTimeout to ensure this runs after all scripts are loaded + setTimeout(init, 0); + } + } + +})(); diff --git a/packages/blaze/exceptions.js b/packages/blaze/exceptions.js index b99ce4c95..2a7d5db1f 100644 --- a/packages/blaze/exceptions.js +++ b/packages/blaze/exceptions.js @@ -40,6 +40,11 @@ Blaze._reportException = function (e, msg) { // and contains a stack trace. Furthermore, `console.log` makes it clickable. // `console.log` supplies the space between the two arguments. debugFunc()(msg || 'Exception caught in template:', e.stack || e.message || e); + + // Report to visual error indicator (client-side only) + if (typeof Blaze._errorIndicator !== 'undefined' && Blaze._errorIndicator) { + Blaze._errorIndicator.addError(e, msg); + } }; // It's meant to be used in `Promise` chains to report the error while not diff --git a/packages/blaze/lookup.js b/packages/blaze/lookup.js index 682e8b98f..97c813adb 100644 --- a/packages/blaze/lookup.js +++ b/packages/blaze/lookup.js @@ -238,9 +238,14 @@ Blaze.View.prototype.lookup = function (name, _options) { const x = data && data[name]; if (! x) { if (lookupTemplate) { - throw new Error("No such template: " + name); + const error = new Error("No such template: " + name); + Blaze._reportException(error, 'Template lookup error:'); + // Return an error placeholder template instead of throwing + return Blaze._errorPlaceholder(name, error); } else if (isCalledAsFunction) { - throw new Error("No such function: " + name); + const error = new Error("No such function: " + name); + Blaze._reportException(error, 'Function lookup error:'); + return null; // Return null instead of throwing for missing functions } else if (name.charAt(0) === '@' && ((x === null) || (x === undefined))) { // Throw an error if the user tries to use a `@directive` @@ -249,7 +254,9 @@ Blaze.View.prototype.lookup = function (name, _options) { // if we fail silently. On the other hand, we want to // throw late in case some app or package wants to provide // a missing directive. - throw new Error("Unsupported directive: " + name); + const error = new Error("Unsupported directive: " + name); + Blaze._reportException(error, 'Directive lookup error:'); + throw error; } } if (! data) { @@ -257,7 +264,9 @@ Blaze.View.prototype.lookup = function (name, _options) { } if (typeof x !== 'function') { if (isCalledAsFunction) { - throw new Error("Can't call non-function: " + x); + const error = new Error("Can't call non-function: " + x); + Blaze._reportException(error, 'Function call error:'); + throw error; } return x; } diff --git a/packages/blaze/package.js b/packages/blaze/package.js index 209c05833..e5d2995cc 100644 --- a/packages/blaze/package.js +++ b/packages/blaze/package.js @@ -1,7 +1,7 @@ Package.describe({ name: 'blaze', summary: "Meteor Reactive Templating library", - version: '3.0.2', + version: '3.1.0', git: 'https://github.com/meteor/blaze.git' }); @@ -52,6 +52,13 @@ Package.onUse(function (api) { 'template.js', 'backcompat.js' ]); + + // Error indicator for development - provides visual error feedback + // with inline placeholders for missing templates. Client-only and + // automatically disabled in production. See errorIndicator.js for details. + api.addFiles([ + 'errorIndicator.js' + ], 'client'); // Maybe in order to work properly user will need to have Jquery typedefs api.addAssets('blaze.d.ts', 'server'); }); diff --git a/packages/blaze/preamble.js b/packages/blaze/preamble.js index 33b2a6083..c3af5c129 100644 --- a/packages/blaze/preamble.js +++ b/packages/blaze/preamble.js @@ -34,6 +34,54 @@ Blaze._warn = function (msg) { } }; +/** + * Creates an error placeholder template that renders inline error information + * instead of crashing the entire page. This enables graceful degradation when + * a template is missing - the rest of the page continues to render while the + * error is clearly indicated at the location where the missing template was + * expected to appear. + * + * The placeholder includes: + * - A warning icon for visual identification + * - The name of the missing template + * - A tooltip with the full error stack trace + * + * This is an internal API used by the template lookup system. + * + * @param {string} name - The name of the missing template/component + * @param {Error} error - The error that occurred during lookup + * @returns {Blaze.Template} A template that renders an error placeholder + * @private + */ +Blaze._errorPlaceholder = function (name, error) { + var templateName = 'Template._errorPlaceholder_' + name; + + return new Blaze.Template(templateName, function() { + return HTML.DIV({ + 'class': 'blaze-error-placeholder', + 'role': 'alert', + 'aria-live': 'polite', + 'style': [ + 'background-color: #fee2e2', + 'border: 1px solid #fca5a5', + 'border-radius: 4px', + 'padding: 8px 12px', + 'margin: 4px 0', + 'font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', + 'font-size: 13px', + 'color: #991b1b' + ].join('; '), + 'title': error.stack || error.message + }, [ + HTML.SPAN({ 'style': 'margin-right: 6px;', 'aria-hidden': 'true' }, '\u26A0\uFE0F'), + HTML.STRONG({}, 'Missing template: '), + HTML.CODE({ + 'style': 'background-color: #fecaca; padding: 2px 6px; border-radius: 3px; font-size: 12px;' + }, name) + ]); + }); +}; + const nativeBind = Function.prototype.bind; // An implementation of _.bind which allows better optimization. From e2743627b4e09a085dc8a47b7c66bba55145e793 Mon Sep 17 00:00:00 2001 From: William Reiske Date: Wed, 10 Dec 2025 11:13:12 -0700 Subject: [PATCH 2/2] Improve error reporting in HTML compiler and bump versions Enhances Blaze template compilation error handling to display errors in the browser overlay and console, making them more visible to developers. Also fixes an instanceof check in html-scanner-tests.js and ensures parse errors are properly propagated in html-tools. Updates package versions and interdependencies to 2.0.1 for consistency. --- .../caching-html-compiler.js | 28 ++++++++++++++++++- packages/caching-html-compiler/package.js | 4 +-- packages/html-tools/package.js | 2 +- packages/html-tools/parse.js | 13 +++++++-- packages/spacebars-compiler/package.js | 4 +-- .../templating-tools/html-scanner-tests.js | 2 +- packages/templating-tools/package.js | 4 +-- 7 files changed, 45 insertions(+), 12 deletions(-) diff --git a/packages/caching-html-compiler/caching-html-compiler.js b/packages/caching-html-compiler/caching-html-compiler.js index 7661ff1c9..6c0a0d9c6 100644 --- a/packages/caching-html-compiler/caching-html-compiler.js +++ b/packages/caching-html-compiler/caching-html-compiler.js @@ -72,11 +72,37 @@ CachingHtmlCompiler = class CachingHtmlCompiler extends CachingCompiler { return this.tagHandlerFunc(tags, inputFile.hmrAvailable && inputFile.hmrAvailable()); } catch (e) { if (e instanceof TemplatingTools.CompileError) { + // Report the error to Meteor's build system (shows in terminal) inputFile.error({ message: e.message, line: e.line, }); - return null; + + // Return a result that will display the error on the client side + // This ensures the error is visible in the browser's error overlay + const errorMessage = e.message.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n'); + const errorJs = ` +// Blaze template compilation error +Meteor.startup(function() { + var error = new Error('Template compilation error in ${inputPath}${e.line ? ' (line ' + e.line + ')' : ''}: ${errorMessage}'); + error.file = '${inputPath}'; + error.line = ${e.line || 'null'}; + + // Try to use Blaze error indicator if available + if (typeof Blaze !== 'undefined' && Blaze._errorIndicator && typeof Blaze._errorIndicator.addError === 'function') { + Blaze._errorIndicator.addError(error, 'Template compilation failed:'); + } + + // Also log to console for visibility + console.error('[Blaze Compile Error] ' + error.message); +}); +`; + return { + head: '', + body: '', + js: errorJs, + bodyAttrs: {} + }; } throw e; } diff --git a/packages/caching-html-compiler/package.js b/packages/caching-html-compiler/package.js index e920eee6b..cef33ba5b 100644 --- a/packages/caching-html-compiler/package.js +++ b/packages/caching-html-compiler/package.js @@ -2,7 +2,7 @@ Package.describe({ name: 'caching-html-compiler', summary: 'Pluggable class for compiling HTML into templates', - version: '2.0.0', + version: '2.0.1', git: 'https://github.com/meteor/blaze.git', }); @@ -18,7 +18,7 @@ Package.onUse(function(api) { api.export('CachingHtmlCompiler', 'server'); - api.use(['templating-tools@2.0.0']); + api.use(['templating-tools@2.0.1']); api.addFiles(['caching-html-compiler.js'], 'server'); }); diff --git a/packages/html-tools/package.js b/packages/html-tools/package.js index e9d0da6ce..f2ea926ce 100644 --- a/packages/html-tools/package.js +++ b/packages/html-tools/package.js @@ -1,7 +1,7 @@ Package.describe({ name: 'html-tools', summary: "Standards-compliant HTML tools", - version: '2.0.0', + version: '2.0.1', git: 'https://github.com/meteor/blaze.git' }); diff --git a/packages/html-tools/parse.js b/packages/html-tools/parse.js index 248b1ab3c..694545dc9 100644 --- a/packages/html-tools/parse.js +++ b/packages/html-tools/parse.js @@ -47,10 +47,13 @@ export function parseFragment(input, options) { var posBefore = scanner.pos; + var endTag; + var parseError; try { - var endTag = getHTMLToken(scanner); + endTag = getHTMLToken(scanner); } catch (e) { - // ignore errors from getTemplateTag + // Save the error - we may need to report it if we can't provide a better one + parseError = e; } // XXX we make some assumptions about shouldStop here, like that it @@ -68,8 +71,12 @@ export function parseFragment(input, options) { // If no "shouldStop" option was provided, we should have consumed the whole // input. - if (! shouldStop) + if (! shouldStop) { + // If we captured a parse error earlier, throw it instead of a generic "Expected EOF" + if (parseError) + throw parseError; scanner.fatal("Expected EOF"); + } } return result; diff --git a/packages/spacebars-compiler/package.js b/packages/spacebars-compiler/package.js index 5873fe370..4c10b2164 100644 --- a/packages/spacebars-compiler/package.js +++ b/packages/spacebars-compiler/package.js @@ -1,7 +1,7 @@ Package.describe({ name: 'spacebars-compiler', summary: "Compiler for Spacebars template language", - version: '2.0.0', + version: '2.0.1', git: 'https://github.com/meteor/blaze.git' }); @@ -13,7 +13,7 @@ Package.onUse(function (api) { api.use('ecmascript@0.16.9'); api.use('htmljs@2.0.1'); - api.use('html-tools@2.0.0'); + api.use('html-tools@2.0.1'); api.use('blaze-tools@2.0.0'); api.export('SpacebarsCompiler'); diff --git a/packages/templating-tools/html-scanner-tests.js b/packages/templating-tools/html-scanner-tests.js index f7bdde10f..b74f42efa 100644 --- a/packages/templating-tools/html-scanner-tests.js +++ b/packages/templating-tools/html-scanner-tests.js @@ -13,7 +13,7 @@ Tinytest.add("templating-tools - html scanner", function (test) { try { f(); } catch (e) { - if (! e instanceof TemplatingTools.CompileError) { + if (!(e instanceof TemplatingTools.CompileError)) { throw e; } diff --git a/packages/templating-tools/package.js b/packages/templating-tools/package.js index 25031aa72..6bb23ea23 100644 --- a/packages/templating-tools/package.js +++ b/packages/templating-tools/package.js @@ -1,7 +1,7 @@ Package.describe({ name: 'templating-tools', summary: "Tools to scan HTML and compile tags when building a templating package", - version: '2.0.0', + version: '2.0.1', git: 'https://github.com/meteor/blaze.git' }); @@ -17,7 +17,7 @@ Package.onUse(function(api) { api.export('TemplatingTools'); api.use([ - 'spacebars-compiler@2.0.0' + 'spacebars-compiler@2.0.1' ]); api.mainModule('templating-tools.js');