ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto

fix: popup build modes

mock vs dev vs production now possible

byarielm.fyi ade26c9b 7f2dcdab

verified
+172 -118
+72 -10
packages/extension/README.md
··· 4 4 5 5 ## Development 6 6 7 - **Prerequisites:** 8 - - ATlast dev server must be running at `http://127.0.0.1:8888` 9 - - You must be logged in to ATlast before using the extension 7 + ### Build Modes 8 + 9 + The extension supports three build modes: 10 + 11 + 1. **Mock Mode** (`pnpm run build:mock`) 12 + - For UI/UX testing only 13 + - Shows dev toolbar with state toggle buttons 14 + - No backend or browser API checks 15 + - Uses fake data for all operations 16 + - Perfect for rapid UI development 17 + 18 + 2. **Dev Mode** (`pnpm run build:dev` or `pnpm run build`) 19 + - Full extension functionality with local backend 20 + - Connects to `http://127.0.0.1:8888` 21 + - Requires ATlast dev server running 22 + - Checks server health on startup 23 + - Must be logged in to ATlast 24 + 25 + 3. **Production Mode** (`pnpm run build:prod`) 26 + - Full extension functionality with production backend 27 + - Connects to `https://atlast.byarielm.fyi` 28 + - No server health checks (assumes always online) 29 + - Must be logged in to production ATlast 10 30 11 31 ### Build Extension 12 32 ··· 14 34 # From project root: 15 35 cd packages/extension 16 36 pnpm install 17 - pnpm run build # Dev build (uses http://127.0.0.1:8888) 18 - pnpm run build:prod # Production build (uses https://atlast.byarielm.fyi) 37 + 38 + # Choose your build mode: 39 + pnpm run build:mock # Mock mode - UI testing only 40 + pnpm run build:dev # Dev mode - local backend (default) 41 + pnpm run build:prod # Production mode - production backend 19 42 ``` 20 43 21 - The built extension will be in `dist/chrome/`. 44 + The built extension will be in: 45 + - Chrome: `dist/chrome/` 46 + - Firefox: `dist/firefox/` 22 47 23 - ### Load in Chrome for Testing 48 + ### Load in Browser for Testing 24 49 50 + **Chrome/Edge:** 25 51 1. Open Chrome and navigate to `chrome://extensions` 26 52 2. Enable **Developer mode** (toggle in top right) 27 53 3. Click **Load unpacked** 28 54 4. Select the `packages/extension/dist/chrome/` directory 29 55 5. The extension should now appear in your extensions list 30 56 57 + **Firefox:** 58 + 1. Open Firefox and navigate to `about:debugging#/runtime/this-firefox` 59 + 2. Click **Load Temporary Add-on** 60 + 3. Navigate to `packages/extension/dist/firefox/` and select `manifest.json` 61 + 4. The extension will appear in your extensions list 62 + 63 + **Important:** After rebuilding, click the reload button next to the extension in the browser's extension management page. 64 + 31 65 ### Testing the Extension 32 66 33 - #### Step 0: Start ATlast Dev Server 67 + #### Testing Mock Mode 68 + 69 + Mock mode is perfect for UI testing without backend dependencies: 70 + 71 + 1. Build in mock mode: `pnpm run build:mock` 72 + 2. Load extension in browser 73 + 3. Open popup - you'll see a dev toolbar at the bottom 74 + 4. Click buttons to toggle between states (Idle, Ready, Scraping, Complete, Error, etc.) 75 + 5. Test UI layouts, colors, and interactions 76 + 77 + #### Testing Dev/Prod Mode 78 + 79 + For full functionality testing: 80 + 81 + **Step 0: Start ATlast Dev Server** (Dev mode only) 34 82 35 83 ```bash 36 84 # From project root: ··· 103 151 [Popup] Ready 104 152 ``` 105 153 154 + #### Error Handling 155 + 156 + The extension now shows proper error states in the UI instead of browser alerts: 157 + 158 + - **Upload Errors**: Shows specific error message with troubleshooting tips based on build mode 159 + - **Scraping Errors**: Guides user to correct page and provides context-specific help 160 + - **Server Offline** (Dev mode only): Shows instructions to start local server 161 + - **Not Logged In**: Provides button to open ATlast login page 162 + 163 + All errors include: 164 + - User-friendly message explaining what went wrong 165 + - Specific troubleshooting steps to resolve the issue 166 + - Technical details in collapsible section for debugging 167 + 106 168 #### Common Issues 107 169 108 170 **Issue: Extension shows "Not logged in to ATlast"** 109 171 110 172 Solution: 111 - 1. Open `http://127.0.0.1:8888` in a new tab 173 + 1. Click "Open ATlast" button in the error state 112 174 2. Log in with your Bluesky handle 113 175 3. Return to extension and click "Check Again" 114 176 115 - **Issue: Extension shows "ATlast server not running"** 177 + **Issue: Extension shows "Server not available"** (Dev mode only) 116 178 117 179 Solution: 118 180 1. Start dev server: `npx netlify-cli dev --filter @atlast/web`
+14 -6
packages/extension/build.js
··· 9 9 const __dirname = path.dirname(fileURLToPath(import.meta.url)); 10 10 11 11 const watch = process.argv.includes('--watch'); 12 - const isProd = process.argv.includes('--prod') || process.env.NODE_ENV === 'production'; 13 - const mode = isProd ? 'production' : 'development'; 12 + 13 + // Determine build mode: mock, dev, or prod 14 + let mode = 'dev'; // default 15 + if (process.argv.includes('--mock')) { 16 + mode = 'mock'; 17 + } else if (process.argv.includes('--prod') || process.env.NODE_ENV === 'production') { 18 + mode = 'prod'; 19 + } else if (process.argv.includes('--dev')) { 20 + mode = 'dev'; 21 + } 14 22 15 23 // Environment-specific configuration 16 - const ATLAST_API_URL = mode === 'production' 24 + const ATLAST_API_URL = mode === 'prod' 17 25 ? 'https://atlast.byarielm.fyi' 18 26 : 'http://127.0.0.1:8888'; 19 27 ··· 30 38 // Build configuration base 31 39 const buildConfigBase = { 32 40 bundle: true, 33 - minify: !watch, 34 - sourcemap: watch ? 'inline' : false, 41 + minify: mode === 'prod' && !watch, 42 + sourcemap: mode !== 'prod' || watch ? 'inline' : false, 35 43 target: 'es2020', 36 44 format: 'esm', 37 45 define: { ··· 113 121 114 122 // Import cssnano dynamically for production minification 115 123 const plugins = [tailwindcss, autoprefixer]; 116 - if (isProd) { 124 + if (mode === 'prod') { 117 125 const cssnano = (await import('cssnano')).default; 118 126 plugins.push(cssnano); 119 127 }
-44
packages/extension/manifest.json
··· 1 - { 2 - "manifest_version": 3, 3 - "name": "ATlast Importer", 4 - "version": "1.0.0", 5 - "description": "Import your Twitter/X follows to find them on Bluesky", 6 - "permissions": [ 7 - "activeTab", 8 - "storage" 9 - ], 10 - "host_permissions": [ 11 - "https://twitter.com/*", 12 - "https://x.com/*", 13 - "http://127.0.0.1:8888/*", 14 - "http://localhost:8888/*", 15 - "https://atlast.byarielm.fyi/*" 16 - ], 17 - "background": { 18 - "service_worker": "background/service-worker.js", 19 - "type": "module" 20 - }, 21 - "content_scripts": [ 22 - { 23 - "matches": [ 24 - "https://twitter.com/*", 25 - "https://x.com/*" 26 - ], 27 - "js": ["content/index.js"], 28 - "run_at": "document_idle" 29 - } 30 - ], 31 - "action": { 32 - "default_popup": "popup/popup.html", 33 - "default_icon": { 34 - "16": "assets/icon-16.png", 35 - "48": "assets/icon-48.png", 36 - "128": "assets/icon-128.png" 37 - } 38 - }, 39 - "icons": { 40 - "16": "assets/icon-16.png", 41 - "48": "assets/icon-48.png", 42 - "128": "assets/icon-128.png" 43 - } 44 - }
+4 -2
packages/extension/package.json
··· 5 5 "private": true, 6 6 "type": "module", 7 7 "scripts": { 8 - "build": "node build.js", 8 + "build": "node build.js --dev", 9 + "build:mock": "node build.js --mock", 10 + "build:dev": "node build.js --dev", 9 11 "build:prod": "node build.js --prod", 10 - "dev": "node build.js --watch", 12 + "dev": "node build.js --dev --watch", 11 13 "dev:popup": "vite", 12 14 "package:chrome": "cd dist/chrome && zip -r ../chrome.zip .", 13 15 "package:firefox": "cd dist/firefox && zip -r ../firefox.zip .",
+17 -20
packages/extension/src/popup/popup.html
··· 30 30 <!-- Idle state --> 31 31 <div 32 32 id="state-idle" 33 - class="w-full text-center flex flex-col justify-center" 33 + class="hidden w-full text-center flex flex-col justify-center" 34 34 > 35 35 <div class="text-5xl mb-4">🔍</div> 36 36 <p class="text-base mb-3 text-purple-950 dark:text-cyan-50"> ··· 48 48 <!-- Ready state --> 49 49 <div 50 50 id="state-ready" 51 - class="w-full text-center flex flex-col justify-center" 51 + class="hidden w-full text-center flex flex-col justify-center" 52 52 > 53 53 <div class="text-5xl mb-4">✅</div> 54 54 <p class="text-base mb-3 text-purple-950 dark:text-cyan-50"> ··· 65 65 <!-- Scraping state --> 66 66 <div 67 67 id="state-scraping" 68 - class="w-full text-center flex flex-col justify-center" 68 + class="hidden w-full text-center flex flex-col justify-center" 69 69 > 70 70 <div class="text-5xl mb-4 spinner">⏳</div> 71 71 <p class="text-base mb-3 text-purple-950 dark:text-cyan-50"> ··· 91 91 <!-- Complete state --> 92 92 <div 93 93 id="state-complete" 94 - class="w-full text-center flex flex-col justify-center" 94 + class="hidden w-full text-center flex flex-col justify-center" 95 95 > 96 96 <div class="text-5xl mb-4">🎉</div> 97 97 <p class="text-base mb-3 text-purple-950 dark:text-cyan-50"> ··· 117 117 <!-- Uploading state --> 118 118 <div 119 119 id="state-uploading" 120 - class="w-full text-center flex flex-col justify-center" 120 + class="hidden w-full text-center flex flex-col justify-center" 121 121 > 122 122 <div class="text-5xl mb-4 spinner">📤</div> 123 123 <p class="text-base mb-3 text-purple-950 dark:text-cyan-50"> ··· 128 128 <!-- Error state --> 129 129 <div 130 130 id="state-error" 131 - class="w-full text-center flex flex-col justify-center" 131 + class="hidden w-full text-center flex flex-col justify-center" 132 132 > 133 133 <div class="text-5xl mb-4">⚠️</div> 134 134 <p ··· 140 140 class="text-left text-[13px] text-purple-900 dark:text-cyan-100 mt-2 p-3 bg-white/50 dark:bg-slate-900/50 rounded border-l-[3px] border-orange-600" 141 141 > 142 142 <p class="font-semibold mb-2">Try these steps:</p> 143 - <ul id="error-tips-list" class="list-disc pl-4 space-y-1"> 144 - </ul> 143 + <ul 144 + id="error-tips-list" 145 + class="list-disc pl-4 space-y-1" 146 + ></ul> 145 147 </div> 146 148 <details class="mt-3 text-left"> 147 149 <summary ··· 165 167 <!-- Server offline state --> 166 168 <div 167 169 id="state-offline" 168 - class="w-full flex flex-col justify-center text-center" 170 + class="hidden w-full flex flex-col justify-center text-center" 169 171 > 170 172 <div class="text-5xl mb-4">🔌</div> 171 173 <p class="text-base mb-3 text-purple-950 dark:text-cyan-50"> ··· 183 185 </p> 184 186 <button 185 187 id="btn-check-server" 186 - class="w-full px-6 py-2 bg-orange-600 hover:bg-orange-500 text-white rounded-lg font-medium shadow-md hover:shadow-lg transition-all mt-4" 188 + class="w-full px-6 py-2 bg-orange-600 hover:bg-orange-500 text-white rounded-lg font-bold shadow-md hover:shadow-lg transition-all mt-4" 187 189 > 188 190 Check Again 189 191 </button> ··· 192 194 <!-- Not logged in state --> 193 195 <div 194 196 id="state-not-logged-in" 195 - class="w-full text-center flex flex-col justify-center" 197 + class="hidden w-full text-center flex flex-col justify-center" 196 198 > 197 199 <div class="text-5xl mb-4">🔐</div> 198 200 <p class="text-base mb-3 text-purple-950 dark:text-cyan-50"> 199 - Not logged in to ATlast 200 - </p> 201 - <p 202 - class="text-[13px] text-red-600 dark:text-red-400 mt-2 p-3 bg-white-50 dark:bg-slate-950/50 rounded border-l-2 border-red-600" 203 - > 204 - Please log in to ATlast first, then return here to scan. 201 + Log in to ATlast to begin 205 202 </p> 206 203 <button 207 204 id="btn-open-atlast" ··· 211 208 </button> 212 209 <button 213 210 id="btn-retry-login" 214 - class="w-full px-4 py-2 text-purple-750 dark:text-cyan-250 hover:text-purple-950 dark:hover:text-cyan-50 transition-colors mt-2" 211 + class="w-full px-4 pt-2 text-purple-750 dark:text-cyan-250 hover:text-purple-950 dark:hover:text-cyan-50 transition-colors mt-2" 215 212 > 216 213 Check Again 217 214 </button> ··· 220 217 221 218 <footer class="text-left"> 222 219 <div 223 - class="mt-4 px-10 py-2 -indent-6 border-orange-650/50 dark:border-amber-400/50 bg-white/50 dark:bg-slate-900/50" 220 + class="mt-2 px-10 py-2 -indent-6 border-orange-650/50 dark:border-amber-400/50 bg-white/50 dark:bg-slate-900/50" 224 221 > 225 222 <p class="text-sm text-purple-900 dark:text-cyan-100"> 226 223 💡 <strong>Need help?</strong> Contact @byarielm.fyi on ··· 242 239 </footer> 243 240 </div> 244 241 245 - <script type="module" src="popup.ts"></script> 242 + <script type="module" src="popup.js"></script> 246 243 </body> 247 244 </html>
+65 -36
packages/extension/src/popup/popup.ts
··· 7 7 } from "../lib/messaging.js"; 8 8 9 9 // Build mode injected at build time 10 - declare const __BUILD_MODE__: string; 11 - const IS_DEV_MODE = __BUILD_MODE__ === "development"; 10 + declare const __BUILD_MODE__: "mock" | "dev" | "prod"; 11 + const IS_MOCK_MODE = __BUILD_MODE__ === "mock"; 12 + const IS_DEV_MODE = __BUILD_MODE__ === "dev"; 13 + const IS_PROD_MODE = __BUILD_MODE__ === "prod"; 12 14 13 15 /** 14 16 * DOM elements ··· 331 333 } 332 334 333 335 /** 334 - * Initialize dev mode 336 + * Initialize mock mode (UI testing with dev toolbar, no backend) 335 337 */ 336 - function initDevMode(): void { 337 - console.log("[Popup Dev] Initializing development mode..."); 338 + function initMockMode(): void { 339 + console.log("[Popup Mock] Initializing mock mode..."); 338 340 339 341 // Inject dev UI 340 342 injectDevBanner(); ··· 350 352 351 353 // Set up event listeners for dev mode 352 354 elements.btnStart.addEventListener("click", () => { 353 - console.log("[Popup Dev] Start scan clicked"); 355 + console.log("[Popup Mock] Start scan clicked"); 354 356 elements.btnStart.disabled = true; 355 357 devSimulateScraping(); 356 358 }); 357 359 358 360 elements.btnUpload.addEventListener("click", () => { 359 - console.log("[Popup Dev] Upload clicked"); 360 - alert("In dev mode - would open ATlast with results!"); 361 + console.log("[Popup Mock] Upload clicked"); 362 + alert("In mock mode - would open ATlast with results!"); 361 363 }); 362 364 363 365 elements.btnRetry.addEventListener("click", () => { 364 - console.log("[Popup Dev] Retry clicked"); 366 + console.log("[Popup Mock] Retry clicked"); 365 367 devState = { 366 368 status: "ready", 367 369 platform: "twitter", ··· 372 374 }); 373 375 374 376 elements.btnCheckServer.addEventListener("click", () => { 375 - console.log("[Popup Dev] Check server clicked"); 377 + console.log("[Popup Mock] Check server clicked"); 376 378 devState = { 377 379 status: "ready", 378 380 platform: "twitter", ··· 382 384 }); 383 385 384 386 elements.btnOpenAtlast.addEventListener("click", () => { 385 - console.log("[Popup Dev] Open ATlast clicked"); 387 + console.log("[Popup Mock] Open ATlast clicked"); 386 388 window.open("http://127.0.0.1:8888", "_blank"); 387 389 }); 388 390 389 391 elements.btnRetryLogin.addEventListener("click", () => { 390 - console.log("[Popup Dev] Retry login clicked"); 392 + console.log("[Popup Mock] Retry login clicked"); 391 393 devState = { 392 394 status: "ready", 393 395 platform: "twitter", ··· 396 398 updateUI(devState); 397 399 }); 398 400 399 - console.log("[Popup Dev] Ready"); 401 + console.log("[Popup Mock] Ready"); 400 402 } 401 403 402 404 // ============================================================================ ··· 418 420 pollForUpdates(); 419 421 } catch (error) { 420 422 console.error("[Popup] Error starting scrape:", error); 421 - alert("Error: Make sure you are on a Twitter/X Following page"); 423 + 424 + // Show error state instead of alert 425 + const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; 426 + const errorState: ExtensionState = { 427 + status: "error", 428 + error: errorMessage, 429 + errorCategory: "SCRAPE_START_ERROR", 430 + errorUserMessage: "Failed to start scan", 431 + errorTroubleshootingTips: [ 432 + "Make sure you're on a Twitter/X Following page", 433 + "Navigate to x.com/following or twitter.com/following", 434 + "Refresh the page and try again", 435 + ], 436 + }; 437 + 438 + updateUI(errorState); 422 439 elements.btnStart.disabled = false; 423 440 } 424 441 } ··· 470 487 browser.tabs.create({ url: resultsUrl }); 471 488 } catch (error) { 472 489 console.error("[Popup] Error uploading:", error); 473 - alert("Error uploading to ATlast. Please try again."); 490 + 491 + // Show error state instead of alert 492 + const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; 493 + const errorState: ExtensionState = { 494 + status: "error", 495 + error: errorMessage, 496 + errorCategory: "UPLOAD_ERROR", 497 + errorUserMessage: "Failed to upload results to ATlast", 498 + errorTroubleshootingTips: [ 499 + IS_PROD_MODE 500 + ? "Make sure you're logged in at atlast.byarielm.fyi" 501 + : "Make sure the dev server is running at http://127.0.0.1:8888", 502 + "Check your internet connection", 503 + "Try uploading again", 504 + ], 505 + }; 506 + 507 + updateUI(errorState); 474 508 elements.btnUpload.disabled = false; 475 - showState("complete"); 476 509 } 477 510 } 478 511 ··· 518 551 console.log("[Popup] Server is offline"); 519 552 showState("offline"); 520 553 521 - // Show appropriate message based on build mode 554 + // Show appropriate message - dev instructions only in dev mode 522 555 const apiUrl = getApiUrl(); 523 - const isDev = __BUILD_MODE__ === "development"; 524 556 525 - // Hide dev instructions in production 526 - if (!isDev) { 557 + // Hide dev instructions in non-dev modes 558 + if (!IS_DEV_MODE) { 527 559 elements.devInstructions.classList.add("hidden"); 528 560 } 529 561 530 - elements.serverUrl.textContent = isDev 562 + elements.serverUrl.textContent = IS_DEV_MODE 531 563 ? `Development server at ${apiUrl}` 532 564 : `Cannot reach ${apiUrl}`; 533 565 ··· 539 571 } 540 572 541 573 /** 542 - * Initialize production mode 574 + * Initialize real mode (dev or prod with actual backend) 543 575 */ 544 - async function initProdMode(): Promise<void> { 545 - console.log("[Popup] Initializing popup..."); 546 - 547 - // Check server health first (only in dev mode) 548 - const { getApiUrl } = await import("../lib/api-client.js"); 549 - const isDev = 550 - getApiUrl().includes("127.0.0.1") || getApiUrl().includes("localhost"); 576 + async function initRealMode(): Promise<void> { 577 + console.log(`[Popup] Initializing in ${__BUILD_MODE__} mode...`); 551 578 552 - if (isDev) { 579 + // Check server health only in dev mode 580 + if (IS_DEV_MODE) { 553 581 const serverOnline = await checkServer(); 554 582 if (!serverOnline) { 555 583 // Set up retry button ··· 560 588 const online = await checkServer(); 561 589 if (online) { 562 590 // Server is back online, re-initialize 563 - initProdMode(); 591 + initRealMode(); 564 592 } else { 565 593 elements.btnCheckServer.disabled = false; 566 594 elements.btnCheckServer.textContent = "Check Again"; ··· 572 600 573 601 // Check if user is logged in to ATlast 574 602 console.log("[Popup] Checking login status..."); 575 - const { checkSession } = await import("../lib/api-client.js"); 603 + const { checkSession, getApiUrl } = await import("../lib/api-client.js"); 576 604 const session = await checkSession(); 577 605 578 606 if (!session) { ··· 591 619 const newSession = await checkSession(); 592 620 if (newSession) { 593 621 // User is now logged in, re-initialize 594 - initProdMode(); 622 + initRealMode(); 595 623 } else { 596 624 elements.btnRetryLogin.disabled = false; 597 625 elements.btnRetryLogin.textContent = "Check Again"; ··· 643 671 // ============================================================================ 644 672 645 673 function init(): void { 646 - if (IS_DEV_MODE) { 647 - initDevMode(); 674 + if (IS_MOCK_MODE) { 675 + initMockMode(); 648 676 } else { 649 - initProdMode(); 677 + // Both dev and prod modes use real backend 678 + initRealMode(); 650 679 } 651 680 } 652 681