From 143437f55cba7324ec6f366a4483c7f8740dcffb Mon Sep 17 00:00:00 2001 From: William Reiske Date: Tue, 25 Nov 2025 01:19:46 -0700 Subject: [PATCH 01/10] 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 02/10] 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'); From 0530e6fbc3a7d7b56bed2ad0fd13ee99c1df1b96 Mon Sep 17 00:00:00 2001 From: William Reiske Date: Fri, 3 Apr 2026 10:53:54 -0700 Subject: [PATCH 03/10] fix: preserve original throw behavior in lookup.js Address review feedback from @italojs to avoid breaking changes: - Restore original throw behavior for missing templates, functions, directives, and non-function calls in lookup.js - Remove explicit _reportException calls from lookup.js (errors are already caught by _wrapCatchingExceptions in the rendering pipeline and reported via _reportException, which feeds the error indicator) - Restore original caching-html-compiler compile error handling (return null on CompileError) to prevent build system conflicts The error indicator still works through two channels: 1. _reportException in exceptions.js feeds caught errors to the indicator 2. window.addEventListener('error') in errorIndicator.js catches uncaught Blaze-related errors All 416 tests pass. --- packages/blaze/lookup.js | 17 +++-------- .../caching-html-compiler.js | 28 +------------------ 2 files changed, 5 insertions(+), 40 deletions(-) diff --git a/packages/blaze/lookup.js b/packages/blaze/lookup.js index c2d7d75f2..486c9cce6 100644 --- a/packages/blaze/lookup.js +++ b/packages/blaze/lookup.js @@ -234,14 +234,9 @@ Blaze.View.prototype.lookup = function (name, _options) { const x = data && data[name]; if (! x) { if (lookupTemplate) { - 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); + throw new Error(`No such template: ${name}`); } else if (isCalledAsFunction) { - const error = new Error(`No such function: ${name}`); - Blaze._reportException(error, 'Function lookup error:'); - return null; // Return null instead of throwing for missing functions + throw new Error(`No such function: ${name}`); } else if (name.charAt(0) === '@' && ((x === null) || (x === undefined))) { // Throw an error if the user tries to use a `@directive` @@ -250,9 +245,7 @@ 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. - const error = new Error(`Unsupported directive: ${name}`); - Blaze._reportException(error, 'Directive lookup error:'); - throw error; + throw new Error(`Unsupported directive: ${name}`); } } if (! data) { @@ -260,9 +253,7 @@ Blaze.View.prototype.lookup = function (name, _options) { } if (typeof x !== 'function') { if (isCalledAsFunction) { - const error = new Error(`Can't call non-function: ${x}`); - Blaze._reportException(error, 'Function call error:'); - throw error; + throw new Error(`Can't call non-function: ${x}`); } return x; } diff --git a/packages/caching-html-compiler/caching-html-compiler.js b/packages/caching-html-compiler/caching-html-compiler.js index 2445656ba..b7b04c383 100644 --- a/packages/caching-html-compiler/caching-html-compiler.js +++ b/packages/caching-html-compiler/caching-html-compiler.js @@ -70,37 +70,11 @@ 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 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: {} - }; + return null; } throw e; } From eeb3b4253e613121654503d2280b430db0df4615 Mon Sep 17 00:00:00 2001 From: William Reiske Date: Fri, 3 Apr 2026 11:17:14 -0700 Subject: [PATCH 04/10] fix(spacebars): Gracefully handle missing template errors in Spacebars.include Wrap templateOrFunction() in a try-catch inside the Spacebars.include autorun so that errors like 'No such template: broken' no longer crash the entire page render. Instead, the error is stored in the reactive var, reported via Blaze._reportException (which feeds the error indicator), and an inline error placeholder is rendered in place of the missing template. --- packages/spacebars/spacebars-runtime.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/spacebars/spacebars-runtime.js b/packages/spacebars/spacebars-runtime.js index 86e979849..a4edebf62 100644 --- a/packages/spacebars/spacebars-runtime.js +++ b/packages/spacebars/spacebars-runtime.js @@ -21,6 +21,19 @@ Spacebars.include = function (templateOrFunction, contentFunc, elseFunc) { if (template === null) return null; + // Error state: render inline error placeholder instead of crashing + if (template instanceof Error) { + if (typeof Blaze._errorIndicator !== 'undefined' && Blaze._errorIndicator) { + return HTML.SPAN({ + style: 'display: block; padding: 8px 12px; margin: 4px 0; ' + + 'background-color: #fee; border: 1px solid #fcc; ' + + 'border-left: 4px solid #dc3545; color: #721c24; ' + + 'font-family: monospace; font-size: 13px; border-radius: 4px;' + }, '\u26A0 ' + template.message); + } + return null; + } + if (! Blaze.isTemplate(template)) throw new Error(`Expected template or null, found: ${template}`); @@ -29,7 +42,12 @@ Spacebars.include = function (templateOrFunction, contentFunc, elseFunc) { view.__templateVar = templateVar; view.onViewCreated(function () { this.autorun(function () { - templateVar.set(templateOrFunction()); + try { + templateVar.set(templateOrFunction()); + } catch (e) { + templateVar.set(e); + Blaze._reportException(e, 'Exception in template inclusion:'); + } }); }); view.__startsNewLexicalScope = true; From 54d3adeb96c51fb933edd6f62e084a08651cf8f5 Mon Sep 17 00:00:00 2001 From: William Reiske Date: Fri, 3 Apr 2026 11:28:28 -0700 Subject: [PATCH 05/10] fix(templating-tools): Gracefully handle Spacebars compile errors instead of crashing the build When a template has a Spacebars parse error (e.g. using {{#if}} blocks inside HTML attributes), the build now continues instead of failing with 'Errors prevented startup'. The broken template is replaced with a fallback that renders an inline error placeholder showing the compile error details, and the error is reported to Blaze._errorIndicator at runtime. Other templates in the same file compile normally. A console.warn is emitted during build so the error is still visible in the server terminal. --- .../compile-tags-with-spacebars.js | 43 ++++++++++++++++++- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/packages/templating-tools/compile-tags-with-spacebars.js b/packages/templating-tools/compile-tags-with-spacebars.js index 0b8b23471..c0355037c 100644 --- a/packages/templating-tools/compile-tags-with-spacebars.js +++ b/packages/templating-tools/compile-tags-with-spacebars.js @@ -84,8 +84,47 @@ class SpacebarsTagCompiler { } } catch (e) { if (e.scanner) { - // The error came from Spacebars - this.throwCompileError(e.message, this.tag.contentsStartIndex + e.offset); + // Instead of crashing the build, generate fallback code that + // shows the compile error in the client-side error indicator + const errorMessage = e.message; + const errorOffset = this.tag.contentsStartIndex + e.offset; + const errorLine = this.tag.fileContents + .substring(0, errorOffset).split('\n').length; + + const sourceName = this.tag.tagName === 'template' + ? `Template "${this.tag.attribs.name}"` + : ''; + + console.warn( + `Warning: Compile error in ${this.tag.sourceName}:${errorLine}: ${errorMessage}\n` + + `The app will still run, but the affected template will show an error placeholder.` + ); + + const fullError = `Compile error in ${sourceName} (${this.tag.sourceName}:${errorLine}): ${errorMessage}`; + const renderFuncCode = `function () { + var view = this; + return HTML.SPAN({ + "style": "display:block;padding:8px 12px;margin:4px 0;background-color:#fee;border:1px solid #fcc;border-left:4px solid #dc3545;color:#721c24;font-family:monospace;font-size:13px;border-radius:4px;white-space:pre-wrap" + }, "\u26A0 " + ${JSON.stringify(fullError)}); +}`; + + if (this.tag.tagName === 'template') { + const name = this.tag.attribs.name; + if (name) { + this.results.js += generateTemplateJS( + name, renderFuncCode, hmrAvailable); + } + } else if (this.tag.tagName === 'body') { + this.results.js += generateBodyJS(renderFuncCode, hmrAvailable); + } + + // Report to error indicator at runtime + this.results.js += `\nif (typeof Blaze !== "undefined" && Blaze._errorIndicator) {\n` + + ` Blaze._errorIndicator.addError(\n` + + ` new Error(${JSON.stringify(fullError)}),\n` + + ` "Template compile error:"\n` + + ` );\n` + + `}\n`; } else { throw e; } From c15b72e51ad561122e95e34c546e795672b97027 Mon Sep 17 00:00:00 2001 From: William Reiske Date: Fri, 3 Apr 2026 11:44:50 -0700 Subject: [PATCH 06/10] fix(blaze): Gracefully handle render errors in view autorun Wrap view._render() in a try-catch inside the doRender autorun so that runtime errors (e.g. 'No such function: test' from missing helpers) render an inline error placeholder instead of crashing the entire page. The error is reported via Blaze._reportException which feeds the error indicator. --- packages/blaze/view.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/blaze/view.js b/packages/blaze/view.js index 482034c9f..e8979d371 100644 --- a/packages/blaze/view.js +++ b/packages/blaze/view.js @@ -349,7 +349,24 @@ Blaze._materializeView = function (view, parentView, _workStack, _intoArray) { view._isInRender = true; // Any dependencies that should invalidate this Computation come // from this line: - const htmljs = view._render(); + let htmljs; + try { + htmljs = view._render(); + } catch (e) { + view._isInRender = false; + Blaze._reportException(e, 'Exception in template render:'); + // Render an inline error placeholder instead of crashing + if (typeof Blaze._errorIndicator !== 'undefined' && Blaze._errorIndicator) { + htmljs = HTML.SPAN({ + style: 'display:block;padding:8px 12px;margin:4px 0;' + + 'background-color:#fee;border:1px solid #fcc;' + + 'border-left:4px solid #dc3545;color:#721c24;' + + 'font-family:monospace;font-size:13px;border-radius:4px' + }, '\u26A0 ' + (e.message || String(e))); + } else { + htmljs = null; + } + } view._isInRender = false; if (! c.firstRun && ! Blaze._isContentEqual(lastHtmljs, htmljs)) { From 4454e0c51ac3996f8a0cbe3c40bddaf4084a9201 Mon Sep 17 00:00:00 2001 From: William Reiske Date: Fri, 3 Apr 2026 12:14:09 -0700 Subject: [PATCH 07/10] fix: gate graceful error fallback to development only, catch binding errors - compile-tags-with-spacebars: Only generate inline error placeholders when HMR is available (development). During meteor build, compilation errors now properly throw CompileError and fail the build as expected. - builtins: Wrap _createBinding autorun in try/catch so runtime errors (e.g. 'No such function: test') are caught and reported via Blaze._reportException instead of crashing as uncaught exceptions. This routes them to the error indicator during development. --- packages/blaze/builtins.js | 8 +++++++- packages/templating-tools/compile-tags-with-spacebars.js | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/blaze/builtins.js b/packages/blaze/builtins.js index b2d73df1f..3aeb8b328 100644 --- a/packages/blaze/builtins.js +++ b/packages/blaze/builtins.js @@ -77,7 +77,13 @@ function _createBinding(view, binding, displayName, mapper) { const reactiveVar = new ReactiveVar(undefined, _isEqualBinding); if (typeof binding === 'function') { view.autorun( - () => _setBindingValue(reactiveVar, binding(), mapper), + () => { + try { + _setBindingValue(reactiveVar, binding(), mapper); + } catch (e) { + Blaze._reportException(e, 'Exception in template binding:'); + } + }, view.parentView, displayName, ); diff --git a/packages/templating-tools/compile-tags-with-spacebars.js b/packages/templating-tools/compile-tags-with-spacebars.js index c0355037c..03a011fa6 100644 --- a/packages/templating-tools/compile-tags-with-spacebars.js +++ b/packages/templating-tools/compile-tags-with-spacebars.js @@ -84,7 +84,13 @@ class SpacebarsTagCompiler { } } catch (e) { if (e.scanner) { - // Instead of crashing the build, generate fallback code that + // In production builds (no HMR), throw a proper compile error + // so the build fails with a clear error message as expected + if (!hmrAvailable) { + this.throwCompileError(e.message, this.tag.contentsStartIndex + e.offset); + } + + // In development (HMR available), generate fallback code that // shows the compile error in the client-side error indicator const errorMessage = e.message; const errorOffset = this.tag.contentsStartIndex + e.offset; From fa7b5db0c248baf90e83d3b8dace82600ffad58d Mon Sep 17 00:00:00 2001 From: William Reiske Date: Fri, 3 Apr 2026 12:56:39 -0700 Subject: [PATCH 08/10] refactor: DRY up error placeholders, fix memory leak, fix test failures - Replace unused Blaze._errorPlaceholder with shared _renderErrorPlaceholder helper - Extract _ERROR_PLACEHOLDER_STYLE constant used by view.js, spacebars-runtime.js, and compile-tags-with-spacebars.js (was duplicated 3x with inconsistencies) - Fix memory leak in errorIndicator.js: destroy() now properly removes window 'error' and 'unhandledrejection' listeners - Fix clearAll duplication: clear button handler now calls clearAll() directly - Revert overly-broad try/catch in view.js and builtins.js that swallowed errors the existing Blaze error pipeline already handles - Add _throwNextException check in spacebars-runtime.js catch block so test assertions continue to work - Update 'unfound template' tests to verify graceful degradation behavior All 416 tests pass. --- packages/blaze/builtins.js | 8 +-- packages/blaze/errorIndicator.js | 26 +++++--- packages/blaze/preamble.js | 60 ++++++------------- packages/blaze/view.js | 19 +----- .../spacebars-tests/old_templates_tests.js | 10 +++- packages/spacebars-tests/template_tests.js | 10 +++- packages/spacebars/spacebars-runtime.js | 12 ++-- .../compile-tags-with-spacebars.js | 5 +- 8 files changed, 58 insertions(+), 92 deletions(-) diff --git a/packages/blaze/builtins.js b/packages/blaze/builtins.js index 3aeb8b328..b2d73df1f 100644 --- a/packages/blaze/builtins.js +++ b/packages/blaze/builtins.js @@ -77,13 +77,7 @@ function _createBinding(view, binding, displayName, mapper) { const reactiveVar = new ReactiveVar(undefined, _isEqualBinding); if (typeof binding === 'function') { view.autorun( - () => { - try { - _setBindingValue(reactiveVar, binding(), mapper); - } catch (e) { - Blaze._reportException(e, 'Exception in template binding:'); - } - }, + () => _setBindingValue(reactiveVar, binding(), mapper), view.parentView, displayName, ); diff --git a/packages/blaze/errorIndicator.js b/packages/blaze/errorIndicator.js index 5d2122256..922a526d4 100644 --- a/packages/blaze/errorIndicator.js +++ b/packages/blaze/errorIndicator.js @@ -60,6 +60,8 @@ var isInitialized = false; var styleEl = null; var isEnabled = true; + var handleGlobalError = null; + var handleUnhandledRejection = null; // ============================================ // Utility Functions @@ -500,11 +502,7 @@ }; } if (clearBtn) { - clearBtn.onclick = function() { - errors = []; - isModalOpen = false; - render(); - }; + clearBtn.onclick = clearAll; } } @@ -608,20 +606,22 @@ document.addEventListener('keydown', handleKeydown); // Listen for global errors that might be Blaze-related - window.addEventListener('error', function(event) { + handleGlobalError = function(event) { if (isBlazeRelatedError(event.message, event.error)) { addError(event.error || new Error(event.message), 'Uncaught template error:'); } - }); + }; + window.addEventListener('error', handleGlobalError); - window.addEventListener('unhandledrejection', function(event) { + handleUnhandledRejection = 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:'); } - }); + }; + window.addEventListener('unhandledrejection', handleUnhandledRejection); } /** @@ -629,6 +629,14 @@ */ function destroy() { document.removeEventListener('keydown', handleKeydown); + if (handleGlobalError) { + window.removeEventListener('error', handleGlobalError); + handleGlobalError = null; + } + if (handleUnhandledRejection) { + window.removeEventListener('unhandledrejection', handleUnhandledRejection); + handleUnhandledRejection = null; + } removeContainer(); removeStyles(); errors = []; diff --git a/packages/blaze/preamble.js b/packages/blaze/preamble.js index d3b9332e5..f46a3b3fa 100644 --- a/packages/blaze/preamble.js +++ b/packages/blaze/preamble.js @@ -34,51 +34,27 @@ 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. + * Shared inline style for error placeholders rendered when templates fail. + * Used by view.js, spacebars-runtime.js, and generated fallback code. + * @private + */ +Blaze._ERROR_PLACEHOLDER_STYLE = 'display:block;padding:8px 12px;margin:4px 0;' + + 'background-color:#fee;border:1px solid #fcc;' + + 'border-left:4px solid #dc3545;color:#721c24;' + + 'font-family:monospace;font-size:13px;border-radius:4px;white-space:pre-wrap'; + +/** + * Renders an inline error placeholder SPAN using htmljs. + * Used by the rendering pipeline to show errors without crashing the page. * - * @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 + * @param {string} message - The error message to display + * @returns {Object} An htmljs SPAN node * @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) - ]); - }); +Blaze._renderErrorPlaceholder = function (message) { + return HTML.SPAN({ + style: Blaze._ERROR_PLACEHOLDER_STYLE + }, '\u26A0 ' + message); }; const nativeBind = Function.prototype.bind; diff --git a/packages/blaze/view.js b/packages/blaze/view.js index e8979d371..482034c9f 100644 --- a/packages/blaze/view.js +++ b/packages/blaze/view.js @@ -349,24 +349,7 @@ Blaze._materializeView = function (view, parentView, _workStack, _intoArray) { view._isInRender = true; // Any dependencies that should invalidate this Computation come // from this line: - let htmljs; - try { - htmljs = view._render(); - } catch (e) { - view._isInRender = false; - Blaze._reportException(e, 'Exception in template render:'); - // Render an inline error placeholder instead of crashing - if (typeof Blaze._errorIndicator !== 'undefined' && Blaze._errorIndicator) { - htmljs = HTML.SPAN({ - style: 'display:block;padding:8px 12px;margin:4px 0;' + - 'background-color:#fee;border:1px solid #fcc;' + - 'border-left:4px solid #dc3545;color:#721c24;' + - 'font-family:monospace;font-size:13px;border-radius:4px' - }, '\u26A0 ' + (e.message || String(e))); - } else { - htmljs = null; - } - } + const htmljs = view._render(); view._isInRender = false; if (! c.firstRun && ! Blaze._isContentEqual(lastHtmljs, htmljs)) { diff --git a/packages/spacebars-tests/old_templates_tests.js b/packages/spacebars-tests/old_templates_tests.js index e2d6406de..30bdb3c64 100644 --- a/packages/spacebars-tests/old_templates_tests.js +++ b/packages/spacebars-tests/old_templates_tests.js @@ -1790,9 +1790,13 @@ Tinytest.add( Tinytest.add( 'spacebars-tests - old - template_tests - unfound template', function (test) { - test.throws(function () { - renderToDiv(Template.old_spacebars_test_nonexistent_template); - }, /No such template/); + // Missing templates now render gracefully with an error placeholder + // instead of throwing (see errorIndicator.js). + Blaze.clearErrors(); + var div = renderToDiv(Template.old_spacebars_test_nonexistent_template); + var errors = Blaze.getErrors(); + test.isTrue(errors.length > 0, 'Expected at least one error'); + test.isTrue(/No such template/.test(errors[0].error), 'Expected "No such template" error'); } ); diff --git a/packages/spacebars-tests/template_tests.js b/packages/spacebars-tests/template_tests.js index 022e06cef..c6cb74ad4 100644 --- a/packages/spacebars-tests/template_tests.js +++ b/packages/spacebars-tests/template_tests.js @@ -2192,9 +2192,13 @@ Tinytest.add('spacebars-tests - template_tests - attributes', function (test) { Tinytest.add( 'spacebars-tests - template_tests - unfound template', function (test) { - test.throws(function () { - renderToDiv(Template.spacebars_test_nonexistent_template); - }, /No such template/); + // Missing templates now render gracefully with an error placeholder + // instead of throwing (see errorIndicator.js). + Blaze.clearErrors(); + var div = renderToDiv(Template.spacebars_test_nonexistent_template); + var errors = Blaze.getErrors(); + test.isTrue(errors.length > 0, 'Expected at least one error'); + test.isTrue(/No such template/.test(errors[0].error), 'Expected "No such template" error'); } ); diff --git a/packages/spacebars/spacebars-runtime.js b/packages/spacebars/spacebars-runtime.js index a4edebf62..95d209769 100644 --- a/packages/spacebars/spacebars-runtime.js +++ b/packages/spacebars/spacebars-runtime.js @@ -24,12 +24,7 @@ Spacebars.include = function (templateOrFunction, contentFunc, elseFunc) { // Error state: render inline error placeholder instead of crashing if (template instanceof Error) { if (typeof Blaze._errorIndicator !== 'undefined' && Blaze._errorIndicator) { - return HTML.SPAN({ - style: 'display: block; padding: 8px 12px; margin: 4px 0; ' + - 'background-color: #fee; border: 1px solid #fcc; ' + - 'border-left: 4px solid #dc3545; color: #721c24; ' + - 'font-family: monospace; font-size: 13px; border-radius: 4px;' - }, '\u26A0 ' + template.message); + return Blaze._renderErrorPlaceholder(template.message); } return null; } @@ -45,6 +40,11 @@ Spacebars.include = function (templateOrFunction, contentFunc, elseFunc) { try { templateVar.set(templateOrFunction()); } catch (e) { + // If _throwNextException is set (e.g., in tests), let the error + // propagate normally so test assertions work + if (Blaze._throwNextException) { + throw e; + } templateVar.set(e); Blaze._reportException(e, 'Exception in template inclusion:'); } diff --git a/packages/templating-tools/compile-tags-with-spacebars.js b/packages/templating-tools/compile-tags-with-spacebars.js index 03a011fa6..137bbf33a 100644 --- a/packages/templating-tools/compile-tags-with-spacebars.js +++ b/packages/templating-tools/compile-tags-with-spacebars.js @@ -108,10 +108,7 @@ class SpacebarsTagCompiler { const fullError = `Compile error in ${sourceName} (${this.tag.sourceName}:${errorLine}): ${errorMessage}`; const renderFuncCode = `function () { - var view = this; - return HTML.SPAN({ - "style": "display:block;padding:8px 12px;margin:4px 0;background-color:#fee;border:1px solid #fcc;border-left:4px solid #dc3545;color:#721c24;font-family:monospace;font-size:13px;border-radius:4px;white-space:pre-wrap" - }, "\u26A0 " + ${JSON.stringify(fullError)}); + return Blaze._renderErrorPlaceholder(${JSON.stringify(fullError)}); }`; if (this.tag.tagName === 'template') { From b042391bb4623bed0885e1c795f756d05023179d Mon Sep 17 00:00:00 2001 From: William Reiske Date: Fri, 3 Apr 2026 13:03:32 -0700 Subject: [PATCH 09/10] fix: restore try/catch in builtins.js _createBinding with _throwNextException guard Binding errors (e.g. calling a non-existent function in {{#if test ...}}) were crashing the entire page render. Restore the try/catch but add the _throwNextException check so tests still work. All 416 tests pass. --- packages/blaze/builtins.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/blaze/builtins.js b/packages/blaze/builtins.js index b2d73df1f..803600df0 100644 --- a/packages/blaze/builtins.js +++ b/packages/blaze/builtins.js @@ -77,7 +77,16 @@ function _createBinding(view, binding, displayName, mapper) { const reactiveVar = new ReactiveVar(undefined, _isEqualBinding); if (typeof binding === 'function') { view.autorun( - () => _setBindingValue(reactiveVar, binding(), mapper), + () => { + try { + _setBindingValue(reactiveVar, binding(), mapper); + } catch (e) { + if (Blaze._throwNextException) { + throw e; + } + Blaze._reportException(e, 'Exception in template binding:'); + } + }, view.parentView, displayName, ); From 15c3b0c11655afb45b4dedbb695ae770adcfbdff Mon Sep 17 00:00:00 2001 From: William Reiske Date: Thu, 9 Apr 2026 07:21:31 -0700 Subject: [PATCH 10/10] fix: revert version number bumps per maintainer request Version bumps will be handled during the 3.1.0 alpha/beta/RC release process. --- packages/blaze/package.js | 2 +- packages/caching-html-compiler/package.js | 4 ++-- packages/html-tools/package.js | 2 +- packages/spacebars-compiler/package.js | 4 ++-- packages/templating-tools/package.js | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/blaze/package.js b/packages/blaze/package.js index e8b9d9b6a..64f605849 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.1.0', + version: '3.0.2', git: 'https://github.com/meteor/blaze.git' }); diff --git a/packages/caching-html-compiler/package.js b/packages/caching-html-compiler/package.js index 63bf9b183..10cc812d6 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.1', + version: '2.0.0', git: 'https://github.com/meteor/blaze.git', }); @@ -14,7 +14,7 @@ Package.onUse(function(api) { api.export('CachingHtmlCompiler', 'server'); - api.use(['templating-tools@2.0.1']); + api.use(['templating-tools@2.0.0']); api.addFiles(['caching-html-compiler.js'], 'server'); }); diff --git a/packages/html-tools/package.js b/packages/html-tools/package.js index f2ea926ce..e9d0da6ce 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.1', + version: '2.0.0', git: 'https://github.com/meteor/blaze.git' }); diff --git a/packages/spacebars-compiler/package.js b/packages/spacebars-compiler/package.js index 4d882fdf0..19b0c7a35 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.1', + version: '2.0.0', git: 'https://github.com/meteor/blaze.git' }); @@ -9,7 +9,7 @@ Package.onUse(function (api) { api.use('ecmascript@0.16.9'); api.use('htmljs@2.0.1'); - api.use('html-tools@2.0.1'); + api.use('html-tools@2.0.0'); api.use('blaze-tools@2.0.0'); api.export('SpacebarsCompiler'); diff --git a/packages/templating-tools/package.js b/packages/templating-tools/package.js index 040dabef2..2f9d26bb4 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.1', + version: '2.0.0', git: 'https://github.com/meteor/blaze.git' }); @@ -13,7 +13,7 @@ Package.onUse(function(api) { api.export('TemplatingTools'); api.use([ - 'spacebars-compiler@2.0.1' + 'spacebars-compiler@2.0.0' ]); api.mainModule('templating-tools.js');