Skip to content

Commit 0a7972e

Browse files
authored
Dev Tools: Show warnings for Compile-Time Optimization Mismatches (#1660)
This pull request adds compile-time optimization mismatch detection and reporting to the Herb dev tools. When `verify_optimizations` is enabled in ReActionView, the template handler compiles each template twice on first render, once with optimizations and once without, and compares the output. If the results differ, a `<template data-herb-optimization-mismatch>` marker is injected into the page. The dev tools detect these markers and display a warning badge (⚠️) next to the Herb floating menu. Clicking the badge shows a panel listing the affected templates by relative path. Full diff details are logged to the Rails server log. <img width="902" height="814" alt="CleanShot 2026-04-20 at 13 08 24@2x" src="https://github.com/user-attachments/assets/5d5d772e-9d5b-4577-a660-b4307f986e6b" />
1 parent 37a1bea commit 0a7972e

2 files changed

Lines changed: 191 additions & 1 deletion

File tree

javascript/packages/dev-tools/src/error-overlay.ts

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,194 @@ export interface ValidationData {
1414
timestamp: string;
1515
}
1616

17+
const optimizationMismatches: Set<string> = new Set();
18+
let optimizationBadgeInitialized = false;
19+
20+
function scanForOptimizationMismatches() {
21+
const templates = document.querySelectorAll('template[data-herb-optimization-mismatch]') as NodeListOf<HTMLTemplateElement>;
22+
23+
templates.forEach((template) => {
24+
optimizationMismatches.add(template.getAttribute('data-filename') || '(unknown)');
25+
template.remove();
26+
});
27+
28+
if (optimizationMismatches.size > 0) {
29+
renderOptimizationBadge();
30+
}
31+
}
32+
33+
function renderOptimizationBadge() {
34+
document.querySelector('.herb-optimization-badge')?.remove();
35+
document.querySelector('.herb-optimization-panel')?.remove();
36+
37+
const filenames = Array.from(optimizationMismatches);
38+
const projectPath = document.querySelector('meta[name="herb-project-path"]')?.getAttribute('content') || '';
39+
const displayNames = filenames.map(f => projectPath && f.startsWith(projectPath) ? f.slice(projectPath.length).replace(/^\//, '') : f);
40+
const title = `\u26A0\uFE0F ${filenames.length} Compile-Time Optimization Mismatch${filenames.length === 1 ? '' : 'es'}`;
41+
42+
if (!optimizationBadgeInitialized) {
43+
optimizationBadgeInitialized = true;
44+
45+
const style = document.createElement('style');
46+
style.className = 'herb-optimization-badge-style';
47+
48+
style.textContent = `
49+
.herb-floating-menu {
50+
display: flex;
51+
flex-direction: row;
52+
align-items: flex-start;
53+
}
54+
55+
.herb-optimization-badge {
56+
background: #fffbeb;
57+
color: #92400e;
58+
font-size: 11px;
59+
font-weight: 600;
60+
padding: 4px 7px;
61+
border-radius: 0 0 0 10px;
62+
border: 1px solid #f59e0b;
63+
border-top: none;
64+
border-right: none;
65+
cursor: pointer;
66+
text-align: center;
67+
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
68+
box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.1);
69+
z-index: 2147483640;
70+
transition: all 0.2s ease;
71+
order: -1;
72+
}
73+
74+
.herb-optimization-badge:hover {
75+
background: #fef3c7;
76+
border-color: #d97706;
77+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
78+
}
79+
80+
.herb-floating-menu .herb-optimization-badge + .herb-menu-trigger {
81+
border-radius: 0 0 0 0;
82+
}
83+
84+
.herb-optimization-panel {
85+
position: fixed;
86+
top: 30px;
87+
right: 8px;
88+
background: white;
89+
border: 1px solid #e5e7eb;
90+
border-radius: 8px;
91+
width: 420px;
92+
max-height: 400px;
93+
overflow-y: auto;
94+
z-index: 2147483642;
95+
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
96+
font-size: 12px;
97+
color: #374151;
98+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
99+
display: none;
100+
}
101+
102+
.herb-optimization-panel.visible {
103+
display: block;
104+
}
105+
106+
.herb-optimization-panel-header {
107+
background: #fffbeb;
108+
padding: 10px 14px;
109+
color: #92400e;
110+
font-weight: 600;
111+
font-size: 13px;
112+
display: flex;
113+
justify-content: space-between;
114+
align-items: center;
115+
border-bottom: 1px solid #fde68a;
116+
border-radius: 8px 8px 0 0;
117+
}
118+
119+
.herb-optimization-panel-close {
120+
background: none;
121+
border: none;
122+
color: #92400e;
123+
cursor: pointer;
124+
font-size: 16px;
125+
padding: 0 4px;
126+
}
127+
128+
.herb-optimization-panel-close:hover {
129+
color: #78350f;
130+
}
131+
132+
.herb-optimization-panel-list {
133+
padding: 4px 0;
134+
}
135+
136+
.herb-optimization-panel-item {
137+
padding: 6px 14px;
138+
color: #6b7280;
139+
border-bottom: 1px solid #f3f4f6;
140+
word-break: break-all;
141+
font-family: 'SF Mono', Monaco, Consolas, monospace;
142+
font-size: 11px;
143+
}
144+
145+
.herb-optimization-panel-item:last-child {
146+
border-bottom: none;
147+
}
148+
149+
.herb-optimization-panel-hint {
150+
padding: 8px 14px;
151+
color: #9ca3af;
152+
font-size: 11px;
153+
border-top: 1px solid #e5e7eb;
154+
background: #f9fafb;
155+
border-radius: 0 0 8px 8px;
156+
}
157+
`;
158+
159+
document.head.appendChild(style);
160+
}
161+
162+
const panel = document.createElement('div');
163+
panel.className = 'herb-optimization-panel';
164+
165+
panel.innerHTML = `
166+
<div class="herb-optimization-panel-header">
167+
<span>${title}</span>
168+
<button class="herb-optimization-panel-close">&times;</button>
169+
</div>
170+
171+
<div class="herb-optimization-panel-list">
172+
${displayNames.map(f => `<div class="herb-optimization-panel-item">${f.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</div>`).join('')}
173+
</div>
174+
175+
<div class="herb-optimization-panel-hint">
176+
Check Rails log for details. Disable with <code>config.verify_optimizations = false</code>.
177+
</div>
178+
`;
179+
document.body.appendChild(panel);
180+
181+
panel.querySelector('.herb-optimization-panel-close')?.addEventListener('click', () => {
182+
panel.classList.remove('visible');
183+
});
184+
185+
const badge = document.createElement('div');
186+
badge.className = 'herb-optimization-badge';
187+
badge.textContent = `\u26A0\uFE0F ${filenames.length}`;
188+
badge.title = title;
189+
190+
badge.addEventListener('click', () => {
191+
panel.classList.toggle('visible');
192+
});
193+
194+
const menu = document.querySelector('.herb-floating-menu');
195+
if (menu) {
196+
menu.prepend(badge);
197+
} else {
198+
badge.style.position = 'fixed';
199+
badge.style.top = '0';
200+
badge.style.right = '0';
201+
document.body.appendChild(badge);
202+
}
203+
}
204+
17205
export class ErrorOverlay {
18206
private overlay: HTMLElement | null = null;
19207
private allValidationData: ValidationData[] = [];
@@ -25,6 +213,7 @@ export class ErrorOverlay {
25213

26214
private init() {
27215
this.detectValidationErrors();
216+
scanForOptimizationMismatches();
28217

29218
const hasParserErrors = document.querySelector('.herb-parser-error-overlay') !== null;
30219

javascript/packages/dev-tools/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ if (typeof window !== 'undefined' && typeof document !== 'undefined') {
1414
const hasValidationErrors = document.querySelector('template[data-herb-validation-errors]') !== null;
1515
const hasValidationError = document.querySelector('template[data-herb-validation-error]') !== null;
1616
const hasParserErrors = document.querySelector('template[data-herb-parser-error]') !== null;
17-
const shouldAutoInit = hasDebugMode || hasDebugErb || hasValidationErrors || hasValidationError || hasParserErrors;
17+
const hasOptimizationMismatches = document.querySelector('template[data-herb-optimization-mismatch]') !== null;
18+
const shouldAutoInit = hasDebugMode || hasDebugErb || hasValidationErrors || hasValidationError || hasParserErrors || hasOptimizationMismatches;
1819

1920
if (shouldAutoInit) {
2021
document.addEventListener('DOMContentLoaded', () => {

0 commit comments

Comments
 (0)