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. 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');