1+ /**
2+ * HAR Page Viewer Test Server
3+ *
4+ * This Express server serves recorded HAR files as live web pages for testing.
5+ * It provides two viewing modes: 'clean' (original page) and 'gitcasso' (with extension injected).
6+ *
7+ * Key components:
8+ * - Loads HAR files from ./har/ directory based on PAGES index in `./har/_har_index.ts`
9+ * - Patches URLs in HTML to serve assets locally from HAR data
10+ * - Handles asset serving by matching HAR entries to requested paths
11+ *
12+ * Development notes:
13+ * - Injects Gitcasso content script in 'gitcasso' mode with location patching
14+ * - Location patching uses history.pushState to simulate original URLs
15+ * - Chrome APIs are mocked for extension testing outside browser context
16+ * - Extension assets served from `./output/chrome-mv3-dev` via `/chrome-mv3-dev` route
17+ * - Floating rebuild button in gitcasso mode triggers `npx wxt build --mode development` and then refresh
18+ */
19+
20+ import { spawn } from 'node:child_process'
121import { error } from 'node:console'
222import fs from 'node:fs/promises'
323import path from 'node:path'
@@ -10,6 +30,9 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url))
1030const app = express ( )
1131const PORT = 3001
1232
33+ // Middleware to parse JSON bodies
34+ app . use ( express . json ( ) )
35+
1336// Store HAR json
1437const harCache = new Map < keyof typeof PAGES , Har > ( )
1538
@@ -25,19 +48,6 @@ function getUrlParts(key: keyof typeof PAGES) {
2548 }
2649}
2750
28- // Check if WXT dev server is running
29- async function checkDevServer ( ) : Promise < boolean > {
30- try {
31- const response = await fetch ( 'http://localhost:3000/@vite/client' , {
32- method : 'HEAD' ,
33- signal : AbortSignal . timeout ( 2000 ) ,
34- } )
35- return response . ok
36- } catch {
37- return false
38- }
39- }
40-
4151// Load and cache HAR file
4252async function loadHar ( key : keyof typeof PAGES ) : Promise < Har > {
4353 if ( harCache . has ( key ) ) {
@@ -65,16 +75,6 @@ app.get('/', async (_req, res) => {
6575 const harDir = path . join ( __dirname , 'har' )
6676 const files = await fs . readdir ( harDir )
6777 const harFiles = files . filter ( ( file ) => file . endsWith ( '.har' ) )
68- const devServerRunning = await checkDevServer ( )
69-
70- const devServerWarning = ! devServerRunning
71- ? `
72- <div style="background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 6px; padding: 15px; margin-bottom: 20px;">
73- <strong>⚠️ Warning:</strong> WXT dev server is not running on localhost:3000<br>
74- <small>Gitcasso-enabled links won't work. Run <code>npm run dev</code> to start the server and <strong>then refresh this page</strong>.</small>
75- </div>
76- `
77- : ''
7878
7979 const links = harFiles
8080 . map ( ( file ) => {
@@ -84,9 +84,7 @@ app.get('/', async (_req, res) => {
8484 <div style="margin-bottom: 10px; font-weight: bold; color: #555;">${ basename } </div>
8585 <div style="display: flex; gap: 10px;">
8686 <a href="/har/${ basename } /clean" style="flex: 1; text-align: center;">🔍 Clean</a>
87- <a href="/har/${ basename } /gitcasso" style="flex: 1; text-align: center; ${ ! devServerRunning ? 'opacity: 0.5; pointer-events: none;' : '' } ">
88- 🚀 Gitcasso-enabled
89- </a>
87+ <a href="/har/${ basename } /gitcasso" style="flex: 1; text-align: center;">🚀 Gitcasso</a>
9088 </div>
9189 </li>
9290 `
@@ -124,7 +122,6 @@ app.get('/', async (_req, res) => {
124122</head>
125123<body>
126124 <h1>📄 HAR Page Viewer</h1>
127- ${ devServerWarning }
128125 <p>Select a recorded page to view:</p>
129126 <ul>${ links } </ul>
130127</body>
@@ -245,6 +242,64 @@ app.get('/asset/:key/*', async (req, res) => {
245242 return res . status ( 404 ) . send ( 'Asset not found' )
246243 }
247244} )
245+ // Serve extension assets from filesystem
246+ app . use ( '/chrome-mv3-dev' , express . static ( path . join ( __dirname , '..' , '.output' , 'chrome-mv3-dev' ) ) )
247+
248+ // Rebuild endpoint
249+ app . post ( '/rebuild' , async ( _req , res ) => {
250+ try {
251+ console . log ( 'Rebuild triggered via API' )
252+
253+ // Run npx wxt build --mode development
254+ const buildProcess = spawn ( 'npx' , [ 'wxt' , 'build' , '--mode' , 'development' ] , {
255+ cwd : path . join ( __dirname , '..' ) ,
256+ stdio : [ 'pipe' , 'pipe' , 'pipe' ] ,
257+ } )
258+
259+ let stdout = ''
260+ let stderr = ''
261+
262+ buildProcess . stdout . on ( 'data' , ( data ) => {
263+ stdout += data . toString ( )
264+ console . log ( '[BUILD]' , data . toString ( ) . trim ( ) )
265+ } )
266+
267+ buildProcess . stderr . on ( 'data' , ( data ) => {
268+ stderr += data . toString ( )
269+ console . error ( '[BUILD ERROR]' , data . toString ( ) . trim ( ) )
270+ } )
271+
272+ buildProcess . on ( 'close' , ( code ) => {
273+ if ( code === 0 ) {
274+ console . log ( 'Build completed successfully' )
275+ res . json ( { message : 'Build completed successfully' , success : true } )
276+ } else {
277+ console . error ( 'Build failed with code:' , code )
278+ res . status ( 500 ) . json ( {
279+ error : stderr || stdout ,
280+ message : 'Build failed' ,
281+ success : false ,
282+ } )
283+ }
284+ } )
285+
286+ buildProcess . on ( 'error' , ( error ) => {
287+ console . error ( 'Failed to start build process:' , error )
288+ res . status ( 500 ) . json ( {
289+ error : error . message ,
290+ message : 'Failed to start build process' ,
291+ success : false ,
292+ } )
293+ } )
294+ } catch ( error ) {
295+ console . error ( 'Rebuild endpoint error:' , error )
296+ res . status ( 500 ) . json ( {
297+ error : error instanceof Error ? error . message : 'Unknown error' ,
298+ message : 'Internal server error' ,
299+ success : false ,
300+ } )
301+ }
302+ } )
248303
249304app . listen ( PORT , ( ) => {
250305 console . log ( `HAR Page Viewer running at http://localhost:${ PORT } ` )
@@ -271,7 +326,7 @@ function injectGitcassoScript(key: keyof typeof PAGES, html: string) {
271326 });
272327
273328 // Fetch and patch the content script to remove webextension-polyfill issues
274- fetch('http://localhost:3000/.output /chrome-mv3-dev/content-scripts/content.js')
329+ fetch('/chrome-mv3-dev/content-scripts/content.js')
275330 .then(response => response.text())
276331 .then(code => {
277332 console.log('Fetched content script, patching webextension-polyfill...');
@@ -303,6 +358,90 @@ function injectGitcassoScript(key: keyof typeof PAGES, html: string) {
303358 .catch(error => {
304359 console.error('Failed to load and patch content script:', error);
305360 });
361+
362+ // Create floating rebuild button
363+ const rebuildButton = document.createElement('div');
364+ rebuildButton.id = 'gitcasso-rebuild-btn';
365+ rebuildButton.innerHTML = '🔄';
366+ rebuildButton.title = 'Rebuild Extension';
367+ rebuildButton.style.cssText = \`
368+ position: fixed;
369+ top: 20px;
370+ right: 20px;
371+ width: 50px;
372+ height: 50px;
373+ background: #007acc;
374+ color: white;
375+ border-radius: 50%;
376+ display: flex;
377+ align-items: center;
378+ justify-content: center;
379+ font-size: 20px;
380+ cursor: pointer;
381+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
382+ z-index: 999999;
383+ user-select: none;
384+ transition: all 0.2s ease;
385+ font-family: system-ui, -apple-system, sans-serif;
386+ \`;
387+
388+ rebuildButton.addEventListener('mouseenter', () => {
389+ rebuildButton.style.transform = 'scale(1.1)';
390+ rebuildButton.style.backgroundColor = '#005a9e';
391+ });
392+
393+ rebuildButton.addEventListener('mouseleave', () => {
394+ rebuildButton.style.transform = 'scale(1)';
395+ rebuildButton.style.backgroundColor = '#007acc';
396+ });
397+
398+ rebuildButton.addEventListener('click', async () => {
399+ try {
400+ rebuildButton.innerHTML = '⏳';
401+ rebuildButton.style.backgroundColor = '#ffa500';
402+ rebuildButton.title = 'Rebuilding...';
403+
404+ const response = await fetch('/rebuild', {
405+ method: 'POST',
406+ headers: { 'Content-Type': 'application/json' }
407+ });
408+
409+ const result = await response.json();
410+
411+ if (result.success) {
412+ rebuildButton.innerHTML = '✅';
413+ rebuildButton.style.backgroundColor = '#28a745';
414+ rebuildButton.title = 'Build successful! Reloading...';
415+
416+ setTimeout(() => {
417+ location.reload(true);
418+ }, 1000);
419+ } else {
420+ rebuildButton.innerHTML = '❌';
421+ rebuildButton.style.backgroundColor = '#dc3545';
422+ rebuildButton.title = 'Build failed: ' + (result.error || result.message);
423+
424+ setTimeout(() => {
425+ rebuildButton.innerHTML = '🔄';
426+ rebuildButton.style.backgroundColor = '#007acc';
427+ rebuildButton.title = 'Rebuild Extension';
428+ }, 3000);
429+ }
430+ } catch (error) {
431+ console.error('Rebuild failed:', error);
432+ rebuildButton.innerHTML = '❌';
433+ rebuildButton.style.backgroundColor = '#dc3545';
434+ rebuildButton.title = 'Network error: ' + error.message;
435+
436+ setTimeout(() => {
437+ rebuildButton.innerHTML = '🔄';
438+ rebuildButton.style.backgroundColor = '#007acc';
439+ rebuildButton.title = 'Rebuild Extension';
440+ }, 3000);
441+ }
442+ });
443+
444+ document.body.appendChild(rebuildButton);
306445 </script>
307446 `
308447 if ( ! html . includes ( '</body>' ) ) {
0 commit comments