yo-bug 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  export class ChecklistPanel {
2
2
  private container: HTMLDivElement;
3
- private listEl: HTMLDivElement;
4
3
  private headerEl: HTMLDivElement | null = null;
4
+ private itemsWrap: HTMLDivElement | null = null;
5
5
  private pollTimer: ReturnType<typeof setInterval> | null = null;
6
6
  private visible = false;
7
7
  private collapsed = false;
@@ -15,20 +15,8 @@ export class ChecklistPanel {
15
15
  this.onItemFail = onItemFail || null;
16
16
  this.baseUrl = window.location.origin;
17
17
  this.container = document.createElement('div');
18
- this.container.style.cssText = `
19
- position: fixed; top: 16px; right: 80px; width: 300px;
20
- max-height: 70vh; overflow: hidden;
21
- background: #1e293b; border-radius: 10px;
22
- box-shadow: 0 4px 20px rgba(0,0,0,0.35);
23
- pointer-events: auto; display: none;
24
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
25
- font-size: 13px; color: #e2e8f0; z-index: 2147483647;
26
- transition: height 0.2s;
27
- `;
28
-
29
- this.listEl = document.createElement('div');
30
- this.listEl.style.cssText = `max-height: calc(70vh - 44px); overflow-y: auto;`;
31
- this.container.appendChild(this.listEl);
18
+ this.container.className = 'vf-checklist';
19
+ this.container.style.display = 'none';
32
20
  shadowRoot.appendChild(this.container);
33
21
  }
34
22
 
@@ -51,7 +39,7 @@ export class ChecklistPanel {
51
39
  if (data.items && data.items.length > 0) {
52
40
  this.render(data);
53
41
  if (!this.visible) {
54
- this.container.style.display = 'block';
42
+ this.container.style.display = '';
55
43
  this.visible = true;
56
44
  }
57
45
  } else if (this.visible) {
@@ -66,29 +54,25 @@ export class ChecklistPanel {
66
54
  const failed = data.items.filter((i) => i.status === 'failed').length;
67
55
  const total = data.items.length;
68
56
 
69
- this.listEl.innerHTML = '';
57
+ this.container.innerHTML = '';
70
58
 
71
59
  // Header — draggable + collapsible
72
60
  const header = document.createElement('div');
73
- header.style.cssText = `
74
- padding: 10px 14px; border-bottom: 1px solid #334155;
75
- display: flex; justify-content: space-between; align-items: center;
76
- cursor: move; user-select: none;
77
- `;
61
+ header.className = 'vf-checklist-header';
78
62
 
79
63
  const titleWrap = document.createElement('div');
80
- titleWrap.style.cssText = `display:flex;align-items:center;gap:8px;`;
64
+ titleWrap.className = 'vf-checklist-title-wrap';
81
65
 
82
66
  const title = document.createElement('span');
83
- title.style.cssText = `font-weight:600;color:white;font-size:13px;`;
67
+ title.className = 'vf-checklist-title';
84
68
  title.textContent = data.title;
85
69
 
86
70
  const badge = document.createElement('span');
87
- badge.style.cssText = `font-size:11px;color:#94a3b8;`;
71
+ badge.className = 'vf-checklist-badge';
88
72
  badge.textContent = `${passed}/${total}`;
89
73
  if (failed > 0) {
90
74
  const failBadge = document.createElement('span');
91
- failBadge.style.cssText = `color:#ef4444;margin-left:4px;`;
75
+ failBadge.className = 'vf-checklist-badge-fail';
92
76
  failBadge.textContent = `${failed} fail`;
93
77
  badge.appendChild(failBadge);
94
78
  }
@@ -99,15 +83,12 @@ export class ChecklistPanel {
99
83
 
100
84
  // Collapse toggle
101
85
  const toggleBtn = document.createElement('button');
102
- toggleBtn.style.cssText = `
103
- background:none; border:none; color:#94a3b8; cursor:pointer;
104
- font-size:16px; padding:0 4px; line-height:1;
105
- `;
86
+ toggleBtn.className = 'vf-checklist-toggle';
106
87
  toggleBtn.textContent = this.collapsed ? '\u25BC' : '\u25B2';
107
88
  toggleBtn.addEventListener('click', (e) => {
108
89
  e.stopPropagation();
109
90
  this.collapsed = !this.collapsed;
110
- this.listEl.querySelector('.vf-checklist-items')?.toggleAttribute('hidden', this.collapsed);
91
+ if (this.itemsWrap) this.itemsWrap.hidden = this.collapsed;
111
92
  toggleBtn.textContent = this.collapsed ? '\u25BC' : '\u25B2';
112
93
  });
113
94
  header.appendChild(toggleBtn);
@@ -133,66 +114,58 @@ export class ChecklistPanel {
133
114
  document.addEventListener('mouseup', onUp);
134
115
  });
135
116
 
136
- this.listEl.appendChild(header);
117
+ this.container.appendChild(header);
137
118
  this.headerEl = header;
138
119
 
139
120
  // Progress bar
140
121
  const progressWrap = document.createElement('div');
141
- progressWrap.style.cssText = `height:2px;background:#334155;`;
122
+ progressWrap.className = 'vf-checklist-progress';
142
123
  const pct = total > 0 ? (passed / total) * 100 : 0;
143
124
  const progressBar = document.createElement('div');
144
- progressBar.style.cssText = `height:100%;width:${pct}%;background:#22c55e;transition:width 0.3s;`;
125
+ progressBar.className = 'vf-checklist-progress-bar';
126
+ progressBar.style.width = `${pct}%`;
145
127
  progressWrap.appendChild(progressBar);
146
- this.listEl.appendChild(progressWrap);
128
+ this.container.appendChild(progressWrap);
147
129
 
148
130
  // Items container (collapsible)
149
- const itemsWrap = document.createElement('div');
150
- itemsWrap.className = 'vf-checklist-items';
151
- if (this.collapsed) itemsWrap.hidden = true;
131
+ this.itemsWrap = document.createElement('div');
132
+ this.itemsWrap.className = 'vf-checklist-items';
133
+ if (this.collapsed) this.itemsWrap.hidden = true;
152
134
 
153
135
  for (const item of data.items) {
154
136
  const row = document.createElement('div');
155
- row.style.cssText = `
156
- padding: 8px 14px; border-bottom: 1px solid #0f172a;
157
- display: flex; align-items: flex-start; gap: 8px;
158
- `;
137
+ row.className = 'vf-checklist-row';
159
138
 
160
139
  // Status buttons
161
140
  const controls = document.createElement('div');
162
- controls.style.cssText = `display:flex;gap:3px;flex-shrink:0;margin-top:2px;`;
141
+ controls.className = 'vf-checklist-controls';
163
142
 
164
- controls.appendChild(this.createStatusBtn('\u2713', '#22c55e', item.status === 'passed', () => {
143
+ controls.appendChild(this.createStatusBtn('\u2713', 'pass', item.status === 'passed', () => {
165
144
  this.updateItem(item.id, 'passed');
166
145
  }));
167
- controls.appendChild(this.createStatusBtn('\u2717', '#ef4444', item.status === 'failed', () => {
146
+ controls.appendChild(this.createStatusBtn('\u2717', 'fail', item.status === 'failed', () => {
168
147
  this.promptFeedback(item.id);
169
148
  }));
170
149
  row.appendChild(controls);
171
150
 
172
151
  // Content
173
152
  const text = document.createElement('div');
174
- text.style.cssText = `flex:1;line-height:1.4;font-size:12px;`;
153
+ text.className = 'vf-checklist-text';
175
154
 
176
155
  // Badges row
177
156
  const badges = document.createElement('div');
178
- badges.style.cssText = `display:flex;gap:4px;margin-bottom:2px;flex-wrap:wrap;`;
157
+ badges.className = 'vf-checklist-badges';
179
158
 
180
159
  if (item.priority === 'critical') {
181
160
  const badge = document.createElement('span');
182
- badge.style.cssText = `
183
- display:inline-block;font-size:9px;padding:1px 5px;border-radius:3px;
184
- background:rgba(239,68,68,0.15);color:#ef4444;font-weight:600;
185
- `;
161
+ badge.className = 'vf-badge vf-badge-critical';
186
162
  badge.textContent = 'CRITICAL';
187
163
  badges.appendChild(badge);
188
164
  }
189
165
 
190
166
  if (item.dimension) {
191
167
  const dimBadge = document.createElement('span');
192
- dimBadge.style.cssText = `
193
- display:inline-block;font-size:9px;padding:1px 5px;border-radius:3px;
194
- background:rgba(59,130,246,0.12);color:#60a5fa;
195
- `;
168
+ dimBadge.className = 'vf-badge vf-badge-dim';
196
169
  dimBadge.textContent = item.dimension;
197
170
  badges.appendChild(dimBadge);
198
171
  }
@@ -203,59 +176,44 @@ export class ChecklistPanel {
203
176
 
204
177
  // Step (what to do)
205
178
  const stepText = document.createElement('span');
179
+ stepText.className = `vf-checklist-step${item.status === 'passed' ? ' passed' : item.status === 'failed' ? ' failed' : ''}`;
206
180
  stepText.textContent = item.step || item.text || '';
207
181
  text.appendChild(stepText);
208
182
 
209
183
  // Expected result
210
184
  if (item.expect) {
211
185
  const expectEl = document.createElement('div');
212
- expectEl.style.cssText = `font-size:11px;color:#94a3b8;margin-top:2px;`;
186
+ expectEl.className = 'vf-checklist-expect';
213
187
  expectEl.textContent = `\u2192 ${item.expect}`;
214
188
  text.appendChild(expectEl);
215
189
  }
216
190
 
217
- if (item.status === 'passed') {
218
- stepText.style.color = '#22c55e';
219
- stepText.style.textDecoration = 'line-through';
220
- stepText.style.opacity = '0.6';
221
- } else if (item.status === 'failed') {
222
- stepText.style.color = '#ef4444';
223
- }
224
-
225
191
  if (item.feedback) {
226
192
  const fb = document.createElement('div');
227
- fb.style.cssText = `font-size:11px;color:#f59e0b;margin-top:3px;`;
193
+ fb.className = 'vf-checklist-feedback';
228
194
  fb.textContent = item.feedback;
229
195
  text.appendChild(fb);
230
196
  }
231
197
 
232
198
  row.appendChild(text);
233
- itemsWrap.appendChild(row);
199
+ this.itemsWrap.appendChild(row);
234
200
  }
235
201
 
236
- this.listEl.appendChild(itemsWrap);
202
+ this.container.appendChild(this.itemsWrap);
237
203
  }
238
204
 
239
- private createStatusBtn(icon: string, color: string, active: boolean, onClick: () => void): HTMLButtonElement {
205
+ private createStatusBtn(icon: string, type: 'pass' | 'fail', active: boolean, onClick: () => void): HTMLButtonElement {
240
206
  const btn = document.createElement('button');
241
207
  btn.textContent = icon;
242
- btn.style.cssText = `
243
- width: 22px; height: 22px; border-radius: 4px; border: 1px solid ${color};
244
- background: ${active ? color : 'transparent'};
245
- color: ${active ? 'white' : color};
246
- font-size: 12px; cursor: pointer; display: flex;
247
- align-items: center; justify-content: center; padding: 0;
248
- `;
208
+ btn.className = `vf-check-btn vf-check-btn-${type}${active ? ' active' : ''}`;
249
209
  btn.addEventListener('click', onClick);
250
210
  return btn;
251
211
  }
252
212
 
253
213
  private promptFeedback(itemId: number): void {
254
214
  if (this.onItemFail) {
255
- // Enter feedback mode — SDK will handle element selection/annotation
256
215
  this.onItemFail(itemId);
257
216
  } else {
258
- // Fallback: simple prompt
259
217
  const feedback = prompt('Describe the issue (optional):') || '';
260
218
  this.updateItem(itemId, 'failed', feedback);
261
219
  }
@@ -1,5 +1,6 @@
1
1
  import type { ElementInfo } from '../element-mode/inspector.js';
2
2
  import { t } from '../core/i18n.js';
3
+ import { ICON_FLAG, ICON_SCREENSHOT } from '../styles/icons.js';
3
4
 
4
5
  const PROBLEM_TYPES = [
5
6
  { value: 'bug', labelKey: 'type.bug' },
@@ -52,7 +53,12 @@ export class FeedbackPanel {
52
53
  // Title
53
54
  const title = document.createElement('div');
54
55
  title.className = 'vf-panel-title';
55
- title.textContent = mode === 'element' ? t('panel.element.title') : t('panel.annotation.title');
56
+ const titleIcon = document.createElement('span');
57
+ titleIcon.innerHTML = mode === 'element' ? ICON_FLAG : ICON_SCREENSHOT;
58
+ title.appendChild(titleIcon);
59
+ const titleText = document.createElement('span');
60
+ titleText.textContent = mode === 'element' ? t('panel.element.title') : t('panel.annotation.title');
61
+ title.appendChild(titleText);
56
62
  this.container.appendChild(title);
57
63
 
58
64
  // Element info
@@ -1,4 +1,5 @@
1
1
  import { t } from '../core/i18n.js';
2
+ import { ICON_BUG, ICON_CLOSE } from '../styles/icons.js';
2
3
 
3
4
  const SEEN_KEY = 'yo-bug-hint-seen';
4
5
 
@@ -10,7 +11,7 @@ export class FloatingButton {
10
11
  constructor(shadowRoot: ShadowRoot, private onClick: () => void) {
11
12
  this.el = document.createElement('button');
12
13
  this.el.className = 'vf-fab';
13
- this.el.innerHTML = '&#128065;'; // eye
14
+ this.el.innerHTML = ICON_BUG;
14
15
  this.el.title = 'yo-bug';
15
16
  this.el.addEventListener('click', () => {
16
17
  this.dismissHint();
@@ -19,23 +20,8 @@ export class FloatingButton {
19
20
 
20
21
  // Recording indicator dot
21
22
  this.recDot = document.createElement('div');
22
- this.recDot.style.cssText = `
23
- position: absolute; top: 2px; right: 2px;
24
- width: 10px; height: 10px; border-radius: 50%;
25
- background: #ef4444; border: 2px solid #1e293b;
26
- animation: vf-pulse 1.5s infinite;
27
- `;
28
-
29
- const style = document.createElement('style');
30
- style.textContent = `
31
- @keyframes vf-pulse {
32
- 0%, 100% { opacity: 1; }
33
- 50% { opacity: 0.4; }
34
- }
35
- `;
36
- shadowRoot.appendChild(style);
37
-
38
- this.el.style.position = 'relative';
23
+ this.recDot.className = 'vf-rec-dot';
24
+
39
25
  this.el.appendChild(this.recDot);
40
26
  shadowRoot.appendChild(this.el);
41
27
 
@@ -46,7 +32,7 @@ export class FloatingButton {
46
32
  setActive(active: boolean): void {
47
33
  this.active = active;
48
34
  this.el.classList.toggle('active', active);
49
- this.el.innerHTML = active ? '&#10005;' : '&#128065;';
35
+ this.el.innerHTML = active ? ICON_CLOSE : ICON_BUG;
50
36
  this.el.appendChild(this.recDot);
51
37
  }
52
38
 
@@ -66,25 +52,11 @@ export class FloatingButton {
66
52
  } catch {}
67
53
 
68
54
  this.hint = document.createElement('div');
69
- this.hint.style.cssText = `
70
- position: fixed; bottom: 82px; right: 20px;
71
- background: #1e293b; color: #e2e8f0;
72
- padding: 10px 16px; border-radius: 10px;
73
- font-size: 13px; pointer-events: auto;
74
- box-shadow: 0 4px 16px rgba(0,0,0,0.35);
75
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
76
- animation: vf-slide-in 0.3s ease-out;
77
- max-width: 200px; line-height: 1.5;
78
- `;
55
+ this.hint.className = 'vf-hint';
79
56
  this.hint.textContent = t('hint.firstTime');
80
57
 
81
- // Arrow pointing down to the button
82
58
  const arrow = document.createElement('div');
83
- arrow.style.cssText = `
84
- position: absolute; bottom: -6px; right: 22px;
85
- width: 12px; height: 12px; background: #1e293b;
86
- transform: rotate(45deg);
87
- `;
59
+ arrow.className = 'vf-hint-arrow';
88
60
  this.hint.appendChild(arrow);
89
61
 
90
62
  shadowRoot.appendChild(this.hint);
@@ -6,7 +6,18 @@ export function showToast(
6
6
  ): void {
7
7
  const el = document.createElement('div');
8
8
  el.className = `vf-toast${isError ? ' error' : ''}`;
9
- el.textContent = message;
9
+
10
+ // Icon
11
+ const icon = document.createElement('span');
12
+ icon.style.cssText = 'font-size: 14px; flex-shrink: 0;';
13
+ icon.textContent = isError ? '\u26A0' : '\u2713';
14
+ el.appendChild(icon);
15
+
16
+ // Text
17
+ const text = document.createElement('span');
18
+ text.textContent = message;
19
+ el.appendChild(text);
20
+
10
21
  shadowRoot.appendChild(el);
11
22
 
12
23
  setTimeout(() => {
@@ -9,12 +9,7 @@ export class VerifyPanel {
9
9
  constructor(private shadowRoot: ShadowRoot) {
10
10
  this.baseUrl = window.location.origin;
11
11
  this.container = document.createElement('div');
12
- this.container.style.cssText = `
13
- position: fixed; bottom: 84px; left: 16px; width: 300px;
14
- display: flex; flex-direction: column; gap: 8px;
15
- pointer-events: none; z-index: 2147483647;
16
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
17
- `;
12
+ this.container.className = 'vf-verify-stack';
18
13
  shadowRoot.appendChild(this.container);
19
14
  }
20
15
 
@@ -44,47 +39,40 @@ export class VerifyPanel {
44
39
 
45
40
  private showVerifyCard(item: any): void {
46
41
  const card = document.createElement('div');
47
- card.style.cssText = `
48
- background: #1e293b; border-radius: 10px; padding: 14px 16px;
49
- box-shadow: 0 4px 20px rgba(0,0,0,0.35); pointer-events: auto;
50
- color: #e2e8f0; font-size: 13px;
51
- border-left: 3px solid #f59e0b;
52
- animation: vf-slide-in 0.3s ease-out;
53
- `;
42
+ card.className = 'vf-verify-card';
54
43
 
55
44
  const title = document.createElement('div');
56
- title.style.cssText = `font-weight:600;color:#f59e0b;margin-bottom:6px;font-size:12px;`;
45
+ title.className = 'vf-verify-title';
57
46
  title.textContent = t('verify.title');
58
47
  card.appendChild(title);
59
48
 
60
49
  const desc = document.createElement('div');
61
- desc.style.cssText = `margin-bottom:10px;line-height:1.4;`;
50
+ desc.className = 'vf-verify-desc';
62
51
  desc.textContent = item.description || `${item.problemType} on ${item.element || 'page'}`;
63
52
  card.appendChild(desc);
64
53
 
65
54
  const btnRow = document.createElement('div');
66
- btnRow.style.cssText = `display:flex;gap:8px;`;
55
+ btnRow.className = 'vf-verify-btns';
67
56
 
68
57
  const passBtn = document.createElement('button');
69
- passBtn.style.cssText = `
70
- flex:1; padding:6px; border:none; border-radius:6px;
71
- background:#22c55e; color:white; font-size:12px; cursor:pointer; font-weight:500;
72
- `;
58
+ passBtn.className = 'vf-verify-btn vf-verify-btn-pass';
73
59
  passBtn.textContent = '\u2713 ' + t('verify.fixed');
74
60
  passBtn.addEventListener('click', () => {
75
61
  this.respond(item.feedbackId, true);
76
- card.remove();
62
+ card.style.opacity = '0';
63
+ card.style.transition = 'opacity 0.2s, transform 0.2s';
64
+ card.style.transform = 'translateX(-20px)';
65
+ setTimeout(() => card.remove(), 200);
77
66
  });
78
67
 
79
68
  const failBtn = document.createElement('button');
80
- failBtn.style.cssText = `
81
- flex:1; padding:6px; border:none; border-radius:6px;
82
- background:#ef4444; color:white; font-size:12px; cursor:pointer; font-weight:500;
83
- `;
69
+ failBtn.className = 'vf-verify-btn vf-verify-btn-fail';
84
70
  failBtn.textContent = '\u2717 ' + t('verify.broken');
85
71
  failBtn.addEventListener('click', () => {
86
72
  this.respond(item.feedbackId, false);
87
- card.remove();
73
+ card.style.opacity = '0';
74
+ card.style.transition = 'opacity 0.2s';
75
+ setTimeout(() => card.remove(), 200);
88
76
  });
89
77
 
90
78
  btnRow.appendChild(passBtn);
@@ -1,13 +1,13 @@
1
1
  import express from 'express';
2
2
  import cors from 'cors';
3
3
  import httpProxy from 'http-proxy';
4
- import type { Server } from 'http';
4
+ import type { Server, IncomingMessage, ServerResponse } from 'http';
5
5
  import { FeedbackStore } from '../storage/store.js';
6
6
  import { createFeedbackRouter } from './feedback-api.js';
7
7
  import { createSdkRouter } from './sdk-serve.js';
8
- import { createHtmlInjector } from './html-injector.js';
9
8
 
10
9
  const PROXY_PORT = 3695;
10
+ const INJECT_SCRIPT = `<script defer src="/vibe-feedback.js"></script>`;
11
11
 
12
12
  let server: Server | null = null;
13
13
  let proxy: httpProxy | null = null;
@@ -24,52 +24,86 @@ export async function startProxyServer(
24
24
  const proxyUrl = `http://localhost:${PROXY_PORT}`;
25
25
 
26
26
  const app = express();
27
-
28
- // CORS for SDK to submit feedback
29
27
  app.use(cors());
30
-
31
- // Parse JSON for feedback API
32
28
  app.use(express.json({ limit: '10mb' }));
33
29
 
34
- // Serve SDK JS file
30
+ // Our own routes — served directly, not proxied
35
31
  app.use(createSdkRouter());
36
-
37
- // Feedback API routes
38
32
  app.use(createFeedbackRouter(store));
39
33
 
40
- // Create proxy
34
+ // Create proxy — let it handle responses by default (no selfHandleResponse)
41
35
  proxy = httpProxy.createProxyServer({
42
36
  target: targetUrl,
43
- ws: true, // WebSocket support for HMR
37
+ ws: true,
44
38
  changeOrigin: true,
45
39
  });
46
40
 
41
+ // Prevent upstream from sending compressed responses — we need raw HTML to inject SDK
42
+ proxy.on('proxyReq', (proxyReq) => {
43
+ proxyReq.setHeader('Accept-Encoding', 'identity');
44
+ });
45
+
47
46
  proxy.on('error', (_err, _req, res) => {
48
47
  if (res && 'writeHead' in res) {
49
- (res as any).writeHead?.(502, { 'Content-Type': 'text/plain' });
50
- (res as any).end?.('Dev server not responding');
48
+ try {
49
+ (res as any).writeHead?.(502, { 'Content-Type': 'text/plain' });
50
+ (res as any).end?.('Dev server not responding');
51
+ } catch {}
51
52
  }
52
53
  });
53
54
 
54
- // Intercept proxy responses to inject SDK into HTML
55
- proxy.on('proxyRes', (proxyRes, _req, res) => {
56
- const injector = createHtmlInjector(proxyRes, res as any);
57
- if (injector) {
58
- // HTML response — buffer and inject
59
- proxyRes.on('data', (chunk) => injector.onData(chunk));
60
- proxyRes.on('end', () => injector.onEnd());
55
+ // Inject SDK into HTML responses using the modifyResponse approach
56
+ // We check Accept header on REQUEST to decide if we need to intercept
57
+ app.all('*', (req, res) => {
58
+ const accept = req.headers['accept'] || '';
59
+ const isHtmlRequest = accept.includes('text/html');
60
+
61
+ if (isHtmlRequest) {
62
+ // Potentially an HTML page — intercept to inject SDK
63
+ proxy!.web(req, res, { selfHandleResponse: true });
61
64
  } else {
62
- // Non-HTML or redirectpipe through with original status + headers
63
- res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
64
- proxyRes.pipe(res as any);
65
+ // Static asset (JS/CSS/images/fonts/etc)let proxy handle directly
66
+ proxy!.web(req, res);
65
67
  }
66
68
  });
67
69
 
68
- // All other requests proxy to dev server
69
- app.all('*', (req, res) => {
70
- proxy!.web(req, res, {
71
- selfHandleResponse: true, // We handle response in proxyRes event
72
- });
70
+ // Handle HTML injection in proxyRes (only fires for selfHandleResponse requests)
71
+ proxy.on('proxyRes', (proxyRes, _req, res) => {
72
+ const statusCode = proxyRes.statusCode || 200;
73
+ const contentType = proxyRes.headers['content-type'] || '';
74
+
75
+ // Only inject into 2xx HTML responses
76
+ if (statusCode >= 200 && statusCode < 300 && contentType.includes('text/html')) {
77
+ // Buffer response, inject SDK, send
78
+ const chunks: Buffer[] = [];
79
+ proxyRes.on('data', (chunk) => chunks.push(chunk));
80
+ proxyRes.on('end', () => {
81
+ let html = Buffer.concat(chunks).toString('utf-8');
82
+
83
+ if (html.includes('</body>')) {
84
+ html = html.replace('</body>', `${INJECT_SCRIPT}\n</body>`);
85
+ } else if (html.includes('</html>')) {
86
+ html = html.replace('</html>', `${INJECT_SCRIPT}\n</html>`);
87
+ } else {
88
+ html += `\n${INJECT_SCRIPT}`;
89
+ }
90
+
91
+ // Copy headers except content-length (body size changed)
92
+ const headers = { ...proxyRes.headers };
93
+ delete headers['content-length'];
94
+ // Remove content-encoding since we decoded the buffer as utf-8
95
+ delete headers['content-encoding'];
96
+ delete headers['transfer-encoding'];
97
+ headers['content-length'] = String(Buffer.byteLength(html));
98
+
99
+ res.writeHead(statusCode, headers);
100
+ res.end(html);
101
+ });
102
+ } else {
103
+ // Redirect or non-HTML — pass through as-is
104
+ res.writeHead(statusCode, proxyRes.headers);
105
+ proxyRes.pipe(res);
106
+ }
73
107
  });
74
108
 
75
109
  // Start server
@@ -7,15 +7,24 @@ export function createSdkRouter(): Router {
7
7
  const router = Router();
8
8
 
9
9
  router.get('/vibe-feedback.js', (_req, res) => {
10
- // Resolve SDK path relative to this file's location
11
10
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
- const sdkPath = path.resolve(__dirname, '../../dist/vibe-feedback.js');
13
11
 
14
- if (!fs.existsSync(sdkPath)) {
15
- return res.status(404).send('// SDK not built yet');
12
+ // Try multiple possible locations:
13
+ // 1. Running from dist/src/server/ → ../../vibe-feedback.js (compiled, npm global)
14
+ // 2. Running from src/server/ via tsx → ../../dist/vibe-feedback.js (dev mode)
15
+ const candidates = [
16
+ path.resolve(__dirname, '../../vibe-feedback.js'), // dist/vibe-feedback.js from dist/src/server/
17
+ path.resolve(__dirname, '../../dist/vibe-feedback.js'), // from src/server/ (tsx dev)
18
+ path.resolve(__dirname, '../../../dist/vibe-feedback.js'), // fallback
19
+ ];
20
+
21
+ const sdkPath = candidates.find((p) => fs.existsSync(p));
22
+
23
+ if (!sdkPath) {
24
+ return res.status(404).send(`// SDK not found. Searched: ${candidates.join(', ')}`);
16
25
  }
17
26
 
18
- res.setHeader('Content-Type', 'application/javascript');
27
+ res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
19
28
  res.setHeader('Cache-Control', 'no-cache');
20
29
  fs.createReadStream(sdkPath).pipe(res);
21
30
  });
@@ -1,46 +0,0 @@
1
- import type { IncomingMessage, ServerResponse } from 'http';
2
-
3
- const INJECT_SCRIPT = `<script src="/vibe-feedback.js"></script>`;
4
-
5
- /**
6
- * Middleware that intercepts HTML responses from the proxy
7
- * and injects the vibe-feedback SDK script before </body>.
8
- * Only intercepts 2xx HTML responses — redirects and errors pass through.
9
- */
10
- export function createHtmlInjector(proxyRes: IncomingMessage, res: ServerResponse) {
11
- const statusCode = proxyRes.statusCode || 200;
12
- const contentType = proxyRes.headers['content-type'] || '';
13
-
14
- // Only inject into 2xx HTML responses
15
- // Don't touch redirects (3xx), errors (4xx/5xx), or non-HTML
16
- if (statusCode < 200 || statusCode >= 300 || !contentType.includes('text/html')) {
17
- return null;
18
- }
19
-
20
- // Copy all response headers except content-length (we'll modify the body)
21
- const headers = Object.fromEntries(
22
- Object.entries(proxyRes.headers).filter(([key]) => key !== 'content-length')
23
- );
24
- res.writeHead(statusCode, headers);
25
-
26
- const chunks: Buffer[] = [];
27
-
28
- return {
29
- onData(chunk: Buffer) {
30
- chunks.push(chunk);
31
- },
32
- onEnd() {
33
- let html = Buffer.concat(chunks).toString('utf-8');
34
-
35
- if (html.includes('</body>')) {
36
- html = html.replace('</body>', `${INJECT_SCRIPT}\n</body>`);
37
- } else if (html.includes('</html>')) {
38
- html = html.replace('</html>', `${INJECT_SCRIPT}\n</html>`);
39
- } else {
40
- html += `\n${INJECT_SCRIPT}`;
41
- }
42
-
43
- res.end(html);
44
- },
45
- };
46
- }